Saltar a contenido

Relaciones entre Clases

Antes de continuar

Esta sección asume que sabés definir clases con atributos y métodos. Si necesitás repasar, empezá por Clases y Objetos.

Hasta acá diseñamos clases de forma individual. En cualquier sistema real, las clases no existen aisladas: colaboran, se agrupan y se especializan. Esta sección introduce la taxonomía de relaciones — el vocabulario que te permite leer y diseñar diagramas de clases con precisión.

Un sistema real no se compone de clases aisladas: las clases se relacionan entre sí para colaborar. Entender qué tipo de relación existe entre dos clases determina cómo las vas a implementar en código y cuán flexibles o rígidas van a ser esas decisiones a futuro.

Existen cuatro tipos de relaciones fundamentales en el diseño orientado a objetos. Las tres primeras (asociación, agregación, composición) responden a la pregunta "¿tiene-un / usa-un?"; la cuarta (herencia) responde a "¿es-un?".

Asociación

La asociación es la relación más general: un objeto conoce o usa a otro, pero sin que ninguno sea dueño del otro. Los objetos asociados pueden existir de forma completamente independiente.

Test: ¿Puede cada objeto existir sin el otro? Si la respuesta es sí para ambos, es asociación.

class Operario:
    """
    Operario de planta que puede ser asignado a distintas estaciones.

    Args:
        nombre: Nombre completo del operario.
        legajo: Número de legajo.
    """

    def __init__(self, nombre: str, legajo: str) -> None:
        self.nombre = nombre
        self.legajo = legajo

    def __repr__(self) -> str:
        return f"Operario({self.nombre!r}, legajo={self.legajo!r})"


class EstacionTrabajo:
    """
    Estación de trabajo con operario asignado.

    Args:
        nombre: Nombre de la estación.
    """

    def __init__(self, nombre: str) -> None:
        self.nombre = nombre
        self._operario_asignado: Operario | None = None

    def asignar_operario(self, operario: Operario) -> None:
        """Asigna un operario a esta estación."""
        self._operario_asignado = operario

    def liberar_operario(self) -> None:
        """Libera al operario de esta estación."""
        self._operario_asignado = None

    def operario_actual(self) -> str:
        if self._operario_asignado:
            return self._operario_asignado.nombre
        return "Sin asignar"


# El Operario puede existir sin la Estación y viceversa → asociación
juan = Operario("Juan Pérez", "OP-001")
maria = Operario("María López", "OP-002")

corte = EstacionTrabajo("Corte CNC")
corte.asignar_operario(juan)
print(corte.operario_actual())  # Juan Pérez

# El operario puede reasignarse a otra estación
corte.liberar_operario()
soldadura = EstacionTrabajo("Soldadura MIG")
soldadura.asignar_operario(juan)
print(soldadura.operario_actual())  # Juan Pérez
# Si la estación se cierra, Juan sigue existiendo

Agregación

La agregación es una asociación con matiz: un objeto contiene una colección de otros, pero los objetos contenidos pueden existir de forma independiente. El contenedor "agrupa" elementos que tienen vida propia.

Test: Si destruís el contenedor, ¿los elementos siguen existiendo? Si sí, es agregación.

class EstacionTrabajo:
    """
    Estación de trabajo de la planta.

    Args:
        nombre: Nombre de la estación.
        tiempo_ciclo: Tiempo de ciclo en segundos.
    """

    def __init__(self, nombre: str, tiempo_ciclo: float) -> None:
        self.nombre = nombre
        self.tiempo_ciclo = tiempo_ciclo

    def __repr__(self) -> str:
        return f"EstacionTrabajo({self.nombre!r})"


class LineaDeMontaje:
    """
    Línea de montaje. Agrega estaciones que existen independientemente.

    Args:
        nombre: Nombre de la línea.
    """

    def __init__(self, nombre: str) -> None:
        self.nombre = nombre
        self._estaciones: list[EstacionTrabajo] = []

    def agregar_estacion(self, estacion: EstacionTrabajo) -> None:
        """Agrega una estación a la línea."""
        self._estaciones.append(estacion)

    def total_estaciones(self) -> int:
        """Retorna la cantidad de estaciones en la línea."""
        return len(self._estaciones)


corte = EstacionTrabajo("Corte CNC", tiempo_ciclo=12.0)
soldadura = EstacionTrabajo("Soldadura MIG", tiempo_ciclo=30.0)

linea_a = LineaDeMontaje("Línea A")
linea_a.agregar_estacion(corte)
linea_a.agregar_estacion(soldadura)

# Si la Línea A se cierra, las estaciones pueden pasar a otra línea
# Una estación no es propiedad exclusiva de una línea → no es composición

Composición

La composición es la relación más fuerte de propiedad: el objeto compuesto crea y posee a sus partes, y esas partes no tienen sentido ni existencia fuera del todo. Si el compuesto se destruye, sus partes se destruyen con él.

Test: ¿Tiene sentido la parte sin el todo? Si no, es composición.

class Sensor:
    """
    Sensor de temperatura interno de una máquina.
    No tiene sentido fuera de la máquina que lo contiene.

    Args:
        id_sensor: Identificador del sensor.
        rango_max: Temperatura máxima operativa en °C.
    """

    def __init__(self, id_sensor: str, rango_max: float) -> None:
        self._id = id_sensor
        self._rango_max = rango_max
        self._lectura_actual = 0.0

    def leer(self) -> float:
        """Retorna la lectura actual del sensor."""
        return self._lectura_actual

    def actualizar(self, temperatura: float) -> None:
        """Actualiza la lectura del sensor."""
        self._lectura_actual = temperatura

    def esta_en_rango(self) -> bool:
        """Indica si la temperatura está dentro del rango operativo."""
        return self._lectura_actual <= self._rango_max


class Maquina:
    """
    Máquina industrial con sensor de temperatura integrado.
    El sensor se crea y vive dentro de la máquina — composición.

    Args:
        nombre: Nombre de la máquina.
        temp_max: Temperatura máxima operativa en °C.
    """

    def __init__(self, nombre: str, temp_max: float = 80.0) -> None:
        self.nombre = nombre
        # El Sensor se crea DENTRO de la Maquina → composición
        self.__sensor = Sensor(f"SEN-{nombre}", rango_max=temp_max)

    @property
    def temperatura(self) -> float:
        """Temperatura actual según el sensor interno."""
        return self.__sensor.leer()

    def actualizar_temperatura(self, temp: float) -> None:
        """Actualiza la lectura del sensor interno."""
        self.__sensor.actualizar(temp)

    def esta_operativa(self) -> bool:
        """Indica si la temperatura está dentro del rango seguro."""
        return self.__sensor.esta_en_rango()


maquina = Maquina("Torno-01", temp_max=75.0)
maquina.actualizar_temperatura(60.0)
print(maquina.temperatura)     # 60.0
print(maquina.esta_operativa()) # True

# El Sensor no existe por sí solo: vive y muere con la Maquina

Herencia

La herencia establece una relación "es-un" entre clases: la subclase es una versión especializada de la clase padre. Hereda todos sus atributos y métodos, y puede agregar los propios o redefinir los heredados.

Test: ¿Podés decir con naturalidad que "B es un A"? Solo entonces usá herencia.

class EstacionTrabajo:
    """
    Abstracción base para cualquier estación de la línea de montaje.

    Args:
        nombre: Nombre de la estación.
        tiempo_ciclo: Tiempo de ciclo en segundos.
    """

    def __init__(self, nombre: str, tiempo_ciclo: float) -> None:
        self.nombre = nombre
        self.tiempo_ciclo = tiempo_ciclo

    def describir(self) -> str:
        """Descripción genérica de la estación."""
        return f"{self.nombre} (ciclo: {self.tiempo_ciclo}s)"


class EstacionCorte(EstacionTrabajo):
    """
    Estación de corte CNC. ES UNA EstacionTrabajo.

    Args:
        nombre: Nombre de la estación.
        tiempo_ciclo: Tiempo de ciclo en segundos.
        velocidad_corte: Velocidad de corte en mm/min.
    """

    def __init__(self, nombre: str, tiempo_ciclo: float, velocidad_corte: float) -> None:
        super().__init__(nombre, tiempo_ciclo)
        self.velocidad_corte = velocidad_corte

    def describir(self) -> str:
        return f"Corte {self.nombre} a {self.velocidad_corte} mm/min (ciclo: {self.tiempo_ciclo}s)"


est = EstacionCorte("CNC-01", tiempo_ciclo=15.0, velocidad_corte=200.0)
print(est.describir())            # Corte CNC-01 a 200.0 mm/min (ciclo: 15.0s)
print(isinstance(est, EstacionTrabajo))  # True — una EstacionCorte ES UNA EstacionTrabajo

Multiplicidad

Más allá del tipo de relación, importa cuántos objetos participan de cada lado:

Multiplicidad Implementación en Python Ejemplo
Uno a uno (1:1) Atributo simple Una Maquina tiene un Sensor
Uno a muchos (1:N) list[Tipo] Una EstacionCorte tiene N Pieza en cola
Muchos a muchos (N:M) list[Tipo] en ambos lados Operario ↔ EstacionTrabajo
class Pieza:
    """
    Pieza en proceso.

    Args:
        numero_serie: Número de serie de la pieza.
        material: Material de la pieza.
    """

    def __init__(self, numero_serie: str, material: str) -> None:
        self.numero_serie = numero_serie
        self.material = material

    def __repr__(self) -> str:
        return f"Pieza({self.numero_serie!r})"


class EstacionCorte:
    """
    Estación de corte con cola de piezas pendientes.

    Args:
        nombre: Nombre de la estación.
    """

    def __init__(self, nombre: str) -> None:
        self.nombre = nombre
        self._cola: list[Pieza] = []   # 1:N

    def encolar_pieza(self, pieza: Pieza) -> None:
        """Agrega una pieza a la cola de procesamiento."""
        self._cola.append(pieza)

    def piezas_en_cola(self) -> int:
        """Retorna la cantidad de piezas pendientes."""
        return len(self._cola)


estacion = EstacionCorte("CNC-02")
estacion.encolar_pieza(Pieza("P-0001", "acero"))
estacion.encolar_pieza(Pieza("P-0002", "aluminio"))
print(f"Piezas en cola: {estacion.piezas_en_cola()}")  # Piezas en cola: 2

Regla práctica

En la mayoría de los sistemas reales, las relaciones más frecuentes son:

  • Composición: cuando un objeto crea y posee a sus partes (fuerte ownership).
  • Asociación: cuando un objeto recibe una referencia a otro y lo usa sin poseerlo.

La herencia es menos frecuente de lo que parece al principio. Antes de usarla, aplicá el test "es-un". Si el test falla o generás dudas, la composición suele ser la alternativa más flexible y mantenible.

En la práctica: uno de los errores más costosos en proyectos reales es modelar como herencia lo que en realidad es composición. Heredar por conveniencia (para reutilizar un método) en lugar de por relación "es-un" produce jerarquías frágiles que se rompen cuando los requerimientos cambian. El tema se desarrolla en profundidad en Composición vs. Herencia.

Ver también

Con el mapa de relaciones claro, los siguientes temas profundizan en las relaciones más importantes y sus implicancias de diseño:

  • Herencia — la relación "es-un" en detalle: sintaxis, super() y sobreescritura de métodos
  • Composición vs. Herencia — cómo decidir entre las dos relaciones más frecuentes