Módulos y organización del código: cuándo dividir un script
Cambiar el catálogo de códigos de puertas no debería obligarte a atravesar la lectura de archivos, la creación del informe y un pequeño museo de funciones llamadas auxiliar2. Si ocurre, el problema todavía no exige una arquitectura de doce capas. Exige saber qué parte decide, qué parte obtiene los datos y quién pone el proceso en marcha. Un árbol de carpetas muy solemne puede ocultar esa confusión con bastante elegancia. Yo prefiero que la elegancia llegue después de poder encontrar la regla.
Vamos a separar un validador en tres archivos y a comprobar algo concreto: importar su regla no debe leer una entrega ni imprimir resultados. Usaremos una lista de códigos ficticios en Python y C#, sin conectar un modelo. El objetivo no es repartir líneas a partes iguales; es permitir que cambies la entrada, pruebes el criterio o sustituyas la salida sin arrastrar las otras dos decisiones.
Divide cuando cambien las responsabilidades, no cuando se acabe la pantalla
Un script pequeño puede estar perfectamente organizado en un solo archivo. Si lees unas filas, aplicas una regla y muestras un resultado, mantenerlo junto ayuda a seguir el recorrido. Separarlo demasiado pronto añade nombres que recordar, importaciones y saltos entre pestañas. El número de archivos no mide la calidad del programa, del mismo modo que el número de disciplinas de un organigrama no demuestra que alguien vaya a contestar el correo.
La señal útil aparece cuando necesitas modificar una parte por un motivo distinto del resto. El formato del fichero puede cambiar aunque el catálogo siga igual. La regla de pertenencia al catálogo puede revisarse aunque la entrada no cambie. El resultado puede pasar de consola a informe sin que ninguna puerta haya adquirido una nueva identidad administrativa. Son tres causas de cambio que conviene reconocer antes de empezar a mover funciones.
En nuestro caso, datos.py conocerá el archivo y su codificación; reglas.py recibirá un texto y un catálogo; main.py coordinará ambas cosas y mostrará el resultado. La regla no recibirá una ruta porque no necesita saber dónde estaba el texto. Tampoco imprimirá nada: devolverá un estado para que otra parte decida cómo presentarlo. Esta separación permite comprobar el criterio con una llamada pequeña, sin preparar una carpeta de entregas para cada expectativa.
Esto desarrolla la separación que empezamos al escribir funciones con un contrato. El siguiente paso no consiste en inventar contratos nuevos, sino en dar un lugar reconocible a los que ya existen. Antes de extraer nada, conserva un caso de referencia y su resultado; si una reorganización cambia la decisión sobre los mismos datos, has hecho algo más que ordenar archivos.
Tres módulos de Python con un recorrido que se puede seguir
Crea una carpeta vacía para el ejemplo. Dentro guarda los tres archivos siguientes y un archivo codigos.txt con estas cuatro líneas; la tercera está vacía. Esa línea representa un registro sin código y debe aparecer en el resultado. No la descartaremos por resultar incómoda para la demostración.
En datos.py, la lectura devuelve una lista de textos. splitlines() separa los registros y conserva la línea vacía interior; no necesitamos eliminar espacios del contenido ni interpretar su significado aquí. Si el archivo no se puede abrir, la función deja que el error llegue al punto que coordina el proceso. No va a fingir una lista vacía y permitirnos celebrar que no hay incumplimientos.
En reglas.py, la pertenencia al catálogo se comprueba sobre el texto original. Un código con espacios exteriores no se normaliza silenciosamente. Este ejemplo recibe las líneas de un archivo, por lo que todas son texto: no pretende resolver el contrato de valores nulos de otro origen. La cadena inicial documenta el propósito del módulo; la función define el criterio y devuelve una respuesta.
Finalmente, guarda main.py. La función main recibe la ruta, pide los datos, construye el catálogo del ejemplo y presenta cada resultado con su número de línea. La comprobación de argumentos está en el arranque. No la escondemos dentro de la regla porque un código desconocido y un comando sin ruta son problemas distintos.
Desde esa carpeta ejecuta la siguiente orden. Con los datos indicados, las líneas 1 y 4 cumplen; la 2 y la 3 incumplen. La tercera se mostrará como '', lo que permite distinguir un registro vacío de uno que ha desaparecido del recorrido. Si quieres probar un catálogo distinto, la regla ya admite recibirlo sin editar su implementación.
El recorrido sigue siendo corto: el programa pide datos al módulo de lectura y pasa cada texto a la regla. Dividirlo no ha hecho que la validación sea más inteligente. Ha hecho visible dónde tomar cada decisión, que ya es una mejora considerable frente a exigirle al siguiente desarrollador facultades arqueológicas.
Importar una función no debería poner a trabajar al proceso entero
En Python, importar un módulo ejecuta sus instrucciones de nivel superior la primera vez que se carga normalmente en ese proceso. Las definiciones dejan disponibles las funciones, pero una llamada suelta a leer_codigos(...) también se ejecutaría. Por eso trasladar una función a otro archivo no basta para aislarla: si dejas la revisión completa al final del módulo, el primer test que importe una regla puede empezar a leer una entrega.
La condición if __name__ == "__main__": distingue el arranque directo del archivo de su importación. En nuestro ejemplo, import main deja disponible su función sin entrar en ese bloque. Eso no convierte la importación en una caja de seguridad: cualquier otra instrucción de nivel superior sigue pudiendo tener efectos. La guía de módulos de Python 3.12 explica esta distinción; conviene entenderla antes de repartir lecturas y escrituras por distintos archivos.
Puedes comprobar nuestro caso desde la misma carpeta con la orden siguiente. Debe terminar sin imprimir resultados incluso si temporalmente renombras codigos.txt. Haz la comprobación sin modificar el programa y devuelve después el nombre al archivo. Si falla porque intenta abrir la entrada, existe una llamada que está escapando del punto de arranque. Si únicamente no encuentra main, revisa desde qué carpeta estás ejecutando la orden y cómo se llama el archivo.
Los módulos del ejemplo dependen en una dirección: main conoce datos y reglas; estos dos no necesitan importar main ni conocerse entre sí. Cuando dos archivos empiezan a importarse mutuamente, revisa qué responsabilidad están compartiendo. A veces hay una operación común que separar; otras has cortado una responsabilidad por la mitad y ahora las dos mitades se envían recados. Añadir una importación dentro de cada función puede retrasar el síntoma sin aclarar el diseño.
En C# separas archivos y tipos, sin copiar el mecanismo de importación
La misma distribución de responsabilidades sirve en C#, pero using no equivale a ejecutar un archivo Python. En este ejemplo, el proyecto compila sus archivos y un espacio de nombres agrupa los tipos. El programa llamará a los métodos desde su punto de entrada. No necesitamos crear una biblioteca independiente para cada responsabilidad ni tres proyectos que mantengan una reunión antes de revisar cuatro códigos.
Con el SDK de .NET 8 instalado, crea el proyecto dentro de la carpeta del ejercicio con la orden siguiente. Se genera una subcarpeta Revision y su Program.cs. Sustituiremos ese archivo y añadiremos otros dos dentro del mismo proyecto. Conserva el archivo de datos en la carpeta del ejercicio, fuera de Revision; la orden final le pasará su ruta explícitamente.
Guarda Datos.cs dentro de Revision. La lectura conserva las líneas y especifica UTF-8. El método es estático porque no necesitamos construir un objeto con estado para realizar esta operación. RevisionCody es el espacio de nombres compartido por nuestras clases, no el nombre de una carpeta que el programa tenga que buscar al ejecutarse.
En Reglas.cs, el catálogo llega como argumento. ISet<string> describe las operaciones que necesitamos de un conjunto; el programa construirá un HashSet<string> concreto. No hay una lectura escondida para obtener ese catálogo y el método no modifica el recibido. La comparación elegida en el arranque será ordinal, para mantener la distinción entre mayúsculas y minúsculas del ejemplo Python.
Sustituye Program.cs por este contenido. args contiene los argumentos del comando, sin incluir el nombre del programa. Si falta la ruta, dejamos un mensaje y un código de salida de error. La lectura se completa antes de imprimir resultados, de modo que un archivo inexistente no genera un informe de aparente éxito con cero registros.
Vuelve a la carpeta del ejercicio y ejecuta la orden siguiente. El separador -- indica que la ruta posterior es un argumento de nuestra aplicación. La documentación de creación de proyectos con dotnet y la explicación de espacios de nombres de C# permiten ampliar estos dos mecanismos sin confundirlos con la lógica de validación.
Las cuatro decisiones deben coincidir con Python. En C# hemos añadido comillas simples al texto mostrado para reconocer el hueco; no es un formato de intercambio ni un escapado universal. Si más adelante exportas datos, necesitarás un formato definido. Aquí nos interesa que cambiar el modo de presentar el código no obligue a tocar el método que decide su pertenencia al catálogo.
Una buena separación se nota cuando necesitas cambiar algo
Prueba a añadir D03 al catálogo que construye el programa, sin tocar la regla. Después llama directamente a esa regla con un catálogo que solo contenga D02 y comprueba que D01 incumple. Esta segunda prueba detecta que alguien haya fijado los códigos dentro de la función, saltándose su argumento. También puedes leer un archivo distinto sin cambiar el criterio: lectura y decisión tienen entradas separadas, aunque el programa las conecte en cada ejecución.
La salida ofrece otra comprobación del diseño. Podrías sustituir las llamadas a print o Console.WriteLine por la construcción de una lista de resultados y mantener el criterio intacto. Si para mostrar un informe nuevo necesitas editar la condición de pertenencia, todavía hay responsabilidades mezcladas. Eso no obliga a construir una infraestructura de informes hoy: basta con reconocer dónde irá ese cambio cuando exista una necesidad real.
Los errores merecen la misma frontera. Una línea con X99 es un resultado de validación; un archivo que no se puede abrir impide empezar la revisión. En este ejercicio el error de lectura se propaga. Una aplicación destinada a otras personas debería convertirlo en un mensaje útil en el arranque, sin ocultar la causa, como explicamos al distinguir excepciones y errores de los datos. No repartiría bloques de captura por todos los módulos para que cada uno anunciara su versión de la misma avería.
Dividir un script merece la pena cuando reduce lo que necesitas entender para cambiar una parte y mantiene visible cómo colaboran las demás. Si acabas con veinte archivos llamados helpers, solo has conseguido que el desorden necesite un explorador. Cuéntame en los comentarios qué cambio te obliga hoy a tocar medio script: otra entrada, una regla o el informe. Y si tienes a alguien convencido de que organizar código consiste en abrir carpetas, pásale el ejemplo; las carpetas se crean solas, las responsabilidades todavía hay que pensarlas.
Respuestas