Comprender cómo se conectan las clases entre sí es fundamental para diseñar software robusto y mantenible. Más allá de saber qué es una clase o cómo crear objetos, lo que realmente marca la diferencia es entender las relaciones que existen entre ellas: dependencia, asociación, implementación, herencia, agregación y composición. Cada una define un nivel distinto de acoplamiento y responsabilidad.
¿Qué es UML y cómo representa las clases?
Antes de analizar las relaciones, es importante conocer la herramienta que permite visualizarlas. UML (Unified Modeling Language) es un lenguaje unificado de modelaje que describe de forma gráfica diagramas de clases y diagramas de procesos [0:08]. Su especificación es amplia, pero lo esencial para trabajar con patrones de diseño es entender la estructura básica de un diagrama de clase.
Un diagrama de clase se compone de tres secciones:
- Nombre de la clase: aparece en la parte superior.
- Propiedades (atributos): representan el estado interno de las instancias.
- Métodos: definen el comportamiento de los objetos.
La visibilidad se indica con símbolos: el signo menos (-) para elementos privados y el signo más (+) para elementos públicos [1:22]. Los atributos privados solo son accesibles dentro de la definición de la clase, mientras que los métodos públicos conforman la interfaz de la clase, es decir, el comportamiento expuesto hacia el resto de la aplicación.
Existe además el concepto de elementos estáticos, representados con una línea bajo el nombre del método o atributo [1:48]. Son aquellos que no requieren la creación de una instancia para ser utilizados. Se acceden directamente con el nombre de la clase, por ejemplo Person.race. Se usan para almacenar información o implementar comportamiento común a todas las instancias.
¿Cuáles son las relaciones entre clases?
¿Qué diferencia a la dependencia de la asociación?
La relación de dependencia es la más general de todas [2:28]. Existe cuando un cambio en una clase provoca modificaciones en otra. Un ejemplo claro: una clase Builder tiene un método build que recibe un parámetro de tipo Tool. Si la interfaz pública de Tool cambia, Builder también debe modificarse [3:02]. El martillo se pasa como parámetro, y si se necesita en otros métodos, habría que pasarlo nuevamente.
La relación de asociación eleva este vínculo a algo permanente [3:36]. En lugar de recibir el objeto como parámetro, la clase lo declara como atributo. La clase Builder define una propiedad ownHammer de tipo Tool, lo que permite utilizar esa instancia a lo largo de todos los métodos sin necesidad de recibirla cada vez [3:55]. La diferencia es sutil pero importante: el acceso es permanente, no temporal.
¿Cómo funcionan la implementación y la herencia?
La relación de implementación ocurre cuando una clase define su comportamiento con base en una interfaz, un contrato [4:32]. Una interfaz TextFormatter declara un método format. Las clases PlainTextFormatter y ShortTextFormatter son ambas text formatters porque implementan ese contrato, aunque cada una lo hace de manera diferente. Aquí aparece el polimorfismo: un mismo contrato, múltiples implementaciones [5:10].
La relación de herencia permite que una clase utilice la misma interfaz e implementación de otra, con la posibilidad de extender su comportamiento [5:22]. El ejemplo utiliza una clase LinkedList con métodos append y remove, y una clase QLinkedList que extiende de ella para agregar los métodos enqueue y dequeue, modelando una cola (queue). Internamente, los métodos heredados siguen disponibles a través del objeto parent [6:13].
¿Cuál es la diferencia entre agregación y composición?
Esta es una de las confusiones más comunes en programación orientada a objetos [7:30].
En la agregación, un objeto tiene uno o más objetos de otra clase, pero no gestiona su ciclo de vida [6:40]. Un vehículo recibe en su constructor un motor y una lista de llantas ya creados. El coche no fabrica el motor ni las llantas, simplemente se las agregamos desde fuera.
En la composición, un objeto consiste de uno o más objetos de otra clase, y sí gestiona su ciclo de vida [7:38]. En el constructor del vehículo se crean directamente el motor y las llantas. Si la instancia del vehículo se destruye, esos elementos también desaparecen.
La elección entre una y otra depende del modelaje que necesite tu aplicación:
- ¿El motor puede existir fuera del vehículo?
- ¿Las instancias del motor pueden reutilizarse en otros vehículos?
- ¿El ciclo de vida de un objeto está ligado al del otro?
Estas preguntas guían la decisión entre agregación y composición [8:15].
¿Son estos conceptos exclusivos de la programación orientada a objetos?
Aunque estos conceptos se explican comúnmente dentro del paradigma orientado a objetos, son aplicables a cualquier paradigma [8:48]. Solo es necesario cambiar el enfoque y dar una interpretación adecuada a la idea central. Dependencia, asociación, herencia, composición y agregación son formas de pensar las relaciones entre componentes de software, sin importar si trabajas con clases, funciones o módulos.
¿Ya conocías estas relaciones? ¿Has trabajado con UML para modelar tus aplicaciones? Comparte tu experiencia en los comentarios.