Interfaces y contratos: definir lo que una pieza debe ofrecer
«Tiene un método llamado leer» parece una garantía modesta. Lo es. Ese método podría devolver todas las puertas, solo las primeras cien o una colección vacía después de fallar la conexión. Los tres resultados cabrían bajo un nombre bastante convincente. Para sustituir una pieza de un programa necesitamos acordar algo más que la ortografía de sus métodos, aunque reconozco el avance que supone ponerse de acuerdo al menos en eso.
Vamos a definir qué espera un consumidor de un lector y a cumplirlo de dos maneras: con datos preparados y con texto CSV. Utilizaremos Protocol en Python y una interfaz en C#, distinguiendo lo que expresan sus tipos de lo que debemos comprobar con pruebas. El consumidor seleccionará identificadores con código vacío; la lectura tendrá que conservar las filas que hacen posible esa decisión. No conectaremos un modelo: el ejemplo trabaja en memoria para que puedas repetirlo sin depender de una instalación BIM.
El contrato empieza por lo que necesita quien utiliza la pieza
Nuestro consumidor necesita una operación sin argumentos que entregue una secuencia materializada de parejas: identificador y código, ambos textos. El identificador debe contener algún carácter distinto de espacio; el código puede estar vacío, porque precisamente queremos detectar ese caso. Los lectores conservarán los textos, el orden y las repeticiones. No harán limpieza de mayúsculas, ordenación ni deduplicación por su cuenta. Si el informe necesita otras políticas, tendrán que aparecer como decisiones visibles.
También hay un acuerdo sobre los fallos. El lector termina de preparar la colección antes de devolverla y comunica mediante una excepción lo que no puede interpretar. No entrega las primeras filas como si fueran la lectura completa ni traduce un error a una colección vacía. Esa colección vacía sí es un resultado válido cuando la lectura correcta no contiene registros. La diferencia decide si puedes afirmar que no hay incidencias o si ni siquiera has conseguido los datos para buscarlas.
La interfaz describe una parte de ese acuerdo: el nombre de la operación, sus argumentos y el tipo del resultado. El resto necesita documentación y pruebas. Un compilador puede comprobar una firma y seguir aceptando una implementación que devuelve siempre un array vacío. Yo no le pediría al compilador que adivinase la intención de un informe, pero tampoco utilizaría su silencio como certificado de que el informe está bien.
En composición y herencia ya separamos fuente y regla. Ahora vamos a dar nombre al contrato que permite esa colaboración. El objetivo no es añadir una interfaz delante de cada clase existente: es que quien consume el lector sepa qué puede exigirle sin conocer cómo obtiene las filas. Si solo necesitas una función, una firma de función puede seguir siendo suficiente.
Python: Protocol expresa la forma esperada, sin imponerla al ejecutar
Guarda este programa como contrato_lectores.py. Utiliza Python 3.10 o posterior y su biblioteca estándar. Lector describe una operación leer; las dos clases concretas la ofrecen sin heredar de ese protocolo. La función sin_codigo utiliza únicamente esa operación, de modo que no pregunta si recibe un CSV o datos preparados.
Ejecuta desde su carpeta:
La salida debe ser preparado | P01 y csv | P01. El lector CSV exige esa cabecera y dos campos por registro, e ignora líneas vacías. Después utiliza la comprobación de la representación común del lector preparado. La regla no sabe que antes hubo comas o comillas: recibe las parejas acordadas. Usamos el analizador de la biblioteca csv, no una separación manual por comas que rompería un campo entrecomillado con una coma dentro.
El protocolo permite expresar tipado estructural: una herramienta de análisis de tipos puede comprobar si una clase ofrece la operación requerida con una firma compatible. La documentación de typing explica también que Python no impone las anotaciones de funciones al ejecutar. Para verificar esas anotaciones necesitas además un comprobador de tipos configurado; ejecutar el programa no realiza esa revisión estática. La anotación ayuda a expresar el acuerdo, pero no inspecciona los datos que devuelve cada implementación.
Por eso las comprobaciones del lector son código real. Poner tuple[str, str] en una anotación no convierte un número en un texto ni impide que alguien entregue un identificador vacío. Tampoco basta añadir un método con el nombre correcto a un objeto que devuelve otra cosa. La sintaxis describe una expectativa; el comportamiento tiene que sostenerla. Es una diferencia pequeña en el documento y bastante grande cuando la descubre la persona que recibe la entrega.
C#: una interfaz comprueba la firma al compilar
Crea una aplicación de consola con .NET 8:
Sustituye ContratoLectores/Program.cs por el siguiente programa. ILector exige Leer con un resultado concreto y las dos clases declaran que implementan esa interfaz. Para interpretar CSV utilizaremos TextFieldParser, disponible en la biblioteca de .NET que se compila con este ejemplo; no hace falta descargar un paquete. Aunque su espacio de nombres incluya VisualBasic, se puede utilizar desde C#.
Ejecuta:
Las dos líneas coinciden con Python. TrimWhiteSpace = false evita recortar los campos automáticamente; el código vacío sigue siendo distinto de un código con un espacio. HasFieldsEnclosedInQuotes permite interpretar campos entrecomillados, como describe la documentación de TextFieldParser. Ambos analizadores tienen sus propias reglas: aquí acordamos y probamos unas entradas concretas, no proclamamos equivalencia para todos los dialectos que alguien haya decidido llamar CSV.
Si una clase declara ILector y omite Leer, el compilador rechaza la implementación. La documentación de interfaces de C# describe esa obligación. Sin embargo, devolver un array vacío desde Leer sí cumpliría la firma aunque el origen contuviera datos. Para detectar ese incumplimiento necesitamos conocer el resultado esperado. El compilador ya tiene bastante trabajo como para que además le encarguemos adivinar cuántas puertas había en el CSV.
Una misma prueba del consumidor debe servir para ambos lectores
Prepara dos registros, P01 sin código y P02 con D02, y pásalos a ambos lectores en sus respectivos formatos. La misma función sin_codigo o SinCodigo debe devolver solo P01. Repite con todos los códigos informados y con todos vacíos. El valor de esa prueba está en mantener el consumidor: si para cambiar de lector tienes que modificar también la selección, todavía hay una dependencia que el nombre de la interfaz no ha conseguido ocultar.
Prueba después duplicados y orden inverso. Si P01 aparece dos veces sin código, deben aparecer dos resultados; si P02 llega antes y ambos carecen de código, ese orden debe conservarse. Es lo que hemos acordado, aunque otro informe pueda necesitar una política diferente. Una implementación que deduplicase en silencio podría parecer más limpia y estar incumpliendo el contrato. Confundir «me gusta más esta salida» con «esta es la salida prometida» es una costumbre con bastante capacidad para sobrevivir a los lenguajes de programación.
El CSV necesita además pruebas propias de su adaptación. Un identificador entrecomillado que contenga una coma debe seguir siendo un solo campo. Una cabecera incorrecta o un registro con tres campos deben fallar. Una lectura con solo la cabecera debe devolver una colección vacía; un texto sin cabecera no es la misma entrada. Comprueba también que un identificador formado solo por espacios se rechaza. Estas pruebas pertenecen al lector CSV, aunque algunas garantías resultantes sean comunes a todos los lectores.
El lector preparado es útil para comprobar al consumidor sin preparar un archivo ni una conexión. No demuestra que el CSV real sea correcto: esa parte sigue necesitando sus casos. Tampoco convierte los datos inventados en una extracción BIM verificada. Aquí interpretamos texto CSV dentro del programa; abrir un archivo externo añade decisiones sobre ruta, codificación y disponibilidad que este ejemplo no está realizando.
Un contrato útil explica también lo que no se debe interpretar
Nuestra operación devuelve una colección preparada, de modo que un problema de lectura se detecta antes de que el consumidor empiece su selección. Si devolviese un iterador, el fallo podría aparecer durante el recorrido. Las dos opciones pueden ser válidas, pero cambiar entre ellas modifica cuándo puede fallar la colaboración. Ese momento también afecta a cómo muestras resultados parciales o decides si puedes continuar un proceso.
El resultado de estos lectores es una instantánea, no una conexión viva al origen. En C# se devuelve un array nuevo; modificarlo no cambia la colección guardada por el lector preparado. En Python se entregan tuplas de parejas de textos. Es una garantía acotada a estos datos: no sirve para afirmar que cualquier estructura anidada queda copiada por utilizar una interfaz. La forma del contrato y las pruebas deben hablar del dato real que intercambias.
Cuando añadas una fuente nueva, empieza por esas mismas pruebas del consumidor y después comprueba los riesgos propios de su lectura. Si necesita paginación, permisos o una conexión, habrá nuevas obligaciones que no existen en nuestro texto en memoria. Puedes conservar una operación parecida, pero no dar por resuelto el significado de «he leído todo» porque hayas conseguido implementar un método. La pieza será sustituible cuando mantenga las expectativas acordadas, no cuando el editor deje de subrayar su nombre.
Antes de añadir otra interfaz, escribe qué puede exigir quien la utiliza, qué resultado considera vacío y cómo sabrá que la lectura ha fallado. Si no puedes responder, la declaración está llegando antes que el acuerdo. Yo empezaría por esas tres preguntas: ocupan menos espacio que una jerarquía y suelen ahorrar bastante más discusión cuando el informe empieza a circular.
Respuestas