Clases y objetos: representar conceptos sin crear una clase para todo
Voy a concederle una clase a un informe. No porque «informe» sea un sustantivo, que sería una manera muy cara de analizar una frase, sino porque quiero reunir sus resultados y las reglas para añadirlos. El informe tendrá un nombre, rechazará identificadores repetidos y calculará su total a partir del detalle. Si alguien intenta añadir un resultado incompleto, los anteriores deben seguir intactos. Esa es la promesa que vamos a comprobar en Python y C#.
También veremos una alternativa con una función y un diccionario. Las clases son una herramienta de diseño; aprender a escribir class no obliga a convertir cada columna de una tabla en un objeto con departamento propio. Lo interesante es decidir cuándo un conjunto de datos necesita una forma estable de comportarse y cuándo solo estás añadiendo una capa para que un programa pequeño parezca una organización muy importante.
El informe necesita conservar unas reglas, no parecerse a un edificio
Usaremos resultados didácticos de una revisión: P01 tiene el código vacío y P04 también. El identificador permitirá volver al elemento y el motivo explicará qué se ha encontrado. No leeremos un modelo ni verificaremos una norma: ya damos por obtenidos esos resultados y nos concentramos en cómo conservarlos. Para este informe el identificador será único dentro de una fuente conocida. Mezclar modelos sin incorporar su identidad exigiría otro contrato.
Hay tres reglas que deben mantenerse siempre que utilicemos la interfaz del informe: nombre y resultados con textos no vacíos, un resultado por identificador y un total que coincida con el número de resultados almacenados. A una regla que debe seguir cumpliéndose durante la vida del objeto se la suele llamar invariante. El término parece pedir una pizarra, pero aquí se puede comprobar contando y tratando de introducir un duplicado. Si el contador dice tres y solo hay dos resultados, no hemos descubierto una sutileza de la orientación a objetos: tenemos dos versiones del mismo dato que ya no se ponen de acuerdo.
Por eso el total se calculará al consultarlo, en vez de guardarlo en una variable que haya que incrementar por otro camino. También comprobaremos todos los datos antes de añadirlos. Una operación rechazada no debe dejar medio resultado escrito. No resolveremos concurrencia entre hilos ni persistencia en una base de datos; son responsabilidades diferentes de este objeto en memoria. La garantía de este ejemplo se refiere a llamadas ordinarias en un mismo proceso, con ese alcance bien definido.
Una clase permite reunir el estado —los resultados que guarda cada informe— y el comportamiento —cómo admite otro resultado o entrega una consulta—. Un objeto es una instancia concreta de esa clase. Dos informes pueden compartir las mismas reglas y conservar resultados distintos. La documentación de clases de Python presenta esa relación entre datos y funcionalidad; la decisión práctica consiste en identificar qué merece permanecer unido, no en encontrar todos los sustantivos del proyecto.
Una función con un diccionario puede ser suficiente
Si el programa prepara un informe una vez, lo imprime y termina, una función puede proteger la operación de añadir sin crear una clase propia. Este programa independiente utiliza Python 3.10 o posterior. Puedes guardarlo como informe_simple.py y ejecutarlo con python informe_simple.py:
La salida será Resultados: 2. La función comprueba las entradas antes de modificar el diccionario y rechaza un identificador ya utilizado. Esta solución tiene una responsabilidad fácil de seguir, especialmente si vienes del artículo sobre funciones y resultados. No es una versión vergonzosa de una clase: puede ser la opción adecuada para un recorrido pequeño y controlado.
La diferencia aparece en los demás accesos al dato. Cualquier parte del programa que conserve ese diccionario puede escribir directamente resultados["P01"] y sustituir el motivo sin pasar por la función. En un script corto puedes mantener esa disciplina con claridad. Si varias partes añaden, consultan y muestran el informe, una interfaz que reúna esos accesos empieza a resultar útil. La clase no hará más inteligente la regla; puede hacer más difícil saltársela por descuido y más fácil encontrar dónde se decide.
Tampoco necesitas una clase solo para transportar tres campos. Una tupla, un diccionario o un tipo de datos sencillo pueden expresar un resultado de revisión. La clase que construiremos ahora representa la colección y su comportamiento, no cada puerta del edificio. Si para entender un informe de dos filas hay que abrir seis archivos con nombres solemnes, yo revisaría primero el diseño y después la cantidad de café disponible para mantenerlo.
Python: cada objeto conserva sus propios resultados
Guarda el siguiente programa completo como informe_objetos.py. __init__ prepara un informe nuevo; self identifica la instancia sobre la que trabaja cada método. El diccionario se crea dentro de ese inicializador: cada informe recibe el suyo. Colocarlo como una colección mutable compartida en el cuerpo de la clase cambiaría esa independencia.
Ejecuta:
Verás Modelo-A | 2, seguido de P01 | codigo vacio y P04 | codigo vacio. Las propiedades permiten consultar nombre, total y resultados. La última devuelve una tupla nueva con parejas de textos: quien la recibe no obtiene el diccionario interno que utiliza agregar. Además, esa consulta es una instantánea; si después añades otro resultado, la tupla que ya habías recibido conserva su contenido anterior. No estamos devolviendo una vista que cambie por debajo de quien prepara la salida.
El prefijo _ comunica que un atributo es interno por convención. Python no lo convierte en una caja fuerte: un programa puede acceder deliberadamente a _resultados y romper las reglas. La documentación lo explica en su apartado de variables privadas. La interfaz organiza el uso correcto y permite revisar sus garantías; no es una frontera de seguridad contra código hostil. Confundir ambas cosas produciría una protección bastante teatral, incluso para mis estándares.
Conservamos los textos originales. Rechazamos una cadena formada solo por espacios, pero no recortamos ni cambiamos de mayúsculas los identificadores aceptados. P01 y p01 son claves distintas, igual que P01 y P01 con un espacio al final. Si la fuente establece equivalencias, añade esa política de forma explícita. La clase sirve para mantener una decisión; no para esconder una limpieza de datos que nadie haya acordado.
C#: una interfaz pública y una colección privada
En .NET 8 crea un proyecto de consola:
Sustituye InformeObjetos/Program.cs por este programa. Las instrucciones que utilizan el informe aparecen antes de la declaración de la clase, como corresponde a esta forma de programa con instrucciones de nivel superior. Nombre solo se asigna al construir el objeto, Total se calcula y Resultados devuelve un array nuevo.
Ejecuta:
La salida coincide con la del objeto Python. En C#, private impide el acceso ordinario desde otras clases al diccionario. readonly evita reasignar ese campo después de su inicialización permitida; no vuelve inmutable al diccionario. El método sigue añadiendo resultados a la misma colección. Es una distinción que merece entenderse antes de colocar la palabra como un amuleto sobre cualquier dato que preferiríamos que no cambiase.
El array devuelto sí puede modificarse, pero es una copia: sustituir una de sus parejas no modifica el informe. Aquí los contenidos son textos, así que basta esa copia de parejas para la consulta que ofrecemos. Con objetos mutables anidados habría que revisar qué referencias se entregan. La documentación de clases de C# explica también que asignar una variable de clase a otra copia la referencia al mismo objeto. Ese comportamiento sigue existiendo aunque su colección interna sea privada.
La prueba útil intenta incumplir la promesa del objeto
Añade P01 otra vez con otro motivo. El método debe lanzar una excepción y mantener el motivo anterior y el total dos. Repite con identificador vacío, motivo vacío y textos formados solo por espacios. Una operación inválida no debería añadir un resultado ni borrar uno previo. Prueba también construir un informe sin nombre válido: el rechazo debe ocurrir antes de disponer de un objeto que incumpla esa regla desde su nacimiento.
Crea un segundo informe y añade allí un único resultado. El primero debe seguir con dos y el segundo con uno. Después asigna el primero a otra variable y añade un resultado a través de esa variable: ambas referencias deben observar el mismo total actualizado. La primera prueba comprueba independencia entre instancias; la segunda demuestra que dos nombres pueden apuntar a una sola instancia. Es el mismo asunto de mutabilidad y copias que ya aparece con las colecciones, ahora aplicado a un tipo propio.
Consulta los resultados, añade otro y revisa la consulta anterior: debe conservar su contenido original. En C# puedes además sustituir una posición del array recibido y comprobar que no cambia el informe. Esta prueba no demuestra una copia profunda universal; demuestra la protección concreta que hemos diseñado para parejas de textos. Los ejemplos se comprueban en consola, con datos sintéticos, y no dicen nada por sí solos sobre el acceso simultáneo o el guardado de una herramienta BIM.
La clase compensa cuando el nombre del concepto ayuda a localizar unas reglas y su interfaz mantiene el estado con menos puntos de modificación. Para una transformación breve puede seguir bastando la función del principio. Yo elegiría la solución que permita señalar dónde se cumple cada promesa sin perseguir escrituras por todo el programa. Si tienes una clase que solo guarda campos o una función que recibe diez fragmentos del mismo estado, ahí puede estar la próxima decisión interesante. Cuéntala en los comentarios: discutir un caso concreto suele enseñarnos más que decidir, en abstracto, que todo debería ser un objeto.
Respuestas