Ordenador con código fuente rodeado de infinidad de objetos.

Relaciones entre clases: herencia, dependencia y asociación

En la Programación Orientada a Objetos (POO), las clases son los bloques fundamentales que representan entidades o conceptos del mundo real. Sin embargo, estas clases no existen de forma aislada. Para construir sistemas complejos y funcionales, las clases deben relacionarse entre sí. Las relaciones entre clases definen la forma en que interactúan y colaboran para construir aplicaciones modulares y reutilizables. Comprender estos conceptos es clave para diseñar software coherente, bien estructurado, eficiente y escalable.

Como herramienta para generar y visualizar los diagramas de clases usaremos PlantUML. Esta herramienta puede instalarse en prácticamente cualquier entorno de desarrollo (IDE).

¿Qué son las Relaciones entre Clases?

Las relaciones entre clases describen cómo una clase se conecta con otra, cómo interactúan las clases y se asocian entre sí. Estas conexiones pueden ser de diferentes tipos, dependiendo de la naturaleza de la interacción. Las relaciones más comunes son:

  1. Herencia: Una clase hereda atributos y métodos de otra.
  2. Dependencia: Una clase usa temporalmente a otra.
  3. Asociación: Una clase tiene una relación más permanente con otra.

1. Herencia

La herencia es una relación de tipo «es-un». Las clases que integran una herencia juegan uno de estos papeles:

  • Subclase, también llamada clase hija o clase derivada. Esta clase hereda atributos y métodos de la otra clase.
  • Superclase, también llamada ancestro o clase base). Esta clase propaga sus atributos y métodos a la otra clase.

La herencia permite reutilizar código y crear jerarquías de clases.

Diagrama generado con PlantUML

@startuml
class Animal {
    #nombre : String
    +void comer()
}

class Perro {
    +void ladrar()
}

Animal <|-- Perro
@enduml

Ejemplo en Java

// Superclase
class Animal {
    protected String nombre;
    public Animal(String nombre) {
        this.nombre = nombre;
    }
    void comer() {
        System.out.println("El animal " + nombre + " está comiendo.");
    }
}

// Subclase
class Perro extends Animal {
    void ladrar() {
        System.out.println("El perro " + nombre + " está ladrando.");
    }
}

public class Main {
    public static void main(String[] args) {
        Perro miPerro = new Perro("Jacinto");
        miPerro.comer();  // Método heredado
        miPerro.ladrar(); // Método propio
    }
}

Explicación:

  • La clase Perro hereda de Animal, por lo que tiene un nombre y puede usar el método comer().
  • La flecha con triángulo (<|--) indica herencia.

Puedes ver más información concreta sobre la herencia en Herencia en Java: la magia de la reutilización. Si tienes dudas sobre los modificadores de acceso te recomiendo que consultes Comprendiendo la Visibilidad de Atributos y Métodos en Java.

2. Dependencia

La dependencia es una relación temporal donde una clase usa a otra, generalmente para realizar alguna acción específica, pero no la contiene como parte de su estructura. Por ejemplo, un método de una clase puede recibir un objeto de otra clase como parámetro.

Se suele describir como una relación «usa a»: un Conductor usa un Coche, una Impresora imprime un Documento.

Diagrama generado con PlantUML

@startuml
class Coche {
    +encender() : void
}

class Conductor {
    +conducir(coche : Coche) : void
}

Conductor ..> Coche : usa
@enduml

Ejemplo en Java

class Coche {
    void encender() {
        System.out.println("El coche está encendido.");
    }
}

class Conductor {
    void conducir(Coche coche) {
        coche.encender();
        System.out.println("El conductor está conduciendo.");
    }
}

public class Main {
    public static void main(String[] args) {
        Coche miCoche = new Coche();
        Conductor miConductor = new Conductor();
        miConductor.conducir(miCoche);
    }
}

Explicación:

  • La clase Conductor depende de Coche porque usa un objeto de tipo Coche en su método conducir().
  • La flecha discontinua (-->) indica dependencia.

3. Asociación

La asociación es una relación estructural, más permanente entre dos clases. Indica que una clase está conectada a otra porque colaboran o se comunican entre sí. Se da cuando una clase tiene una referencia a otra como parte de su estructura.

La relación puede ser unidireccional, en la que solo una de las clases mantiene referencia a la otra, o bidireccional, en las que ambas clases se referencian mutuamente.

Asociación Unidireccional

En una asociación unidireccional, solo una clase tiene una referencia a la otra. La clase que tiene la referencia puede acceder a los métodos y atributos de la otra, pero no al revés.

Supongamos que tenemos una clase Persona y una clase Coche. Un Coche pertenece a una Persona, pero la Persona no tiene una referencia directa a su Coche.

Diagrama generado con PlantUML

@startuml
class Persona {
    -nombre : String
    +getNombre() : String
}

class Coche {
    -propietario : Persona
    +mostrarPropietario() : void
}

Persona "1" --> "*" Coche : posee
@enduml
class Persona {
    String nombre;
    
    public Persona(String nombre) {
        this.nombre = nombre;
    }
    
    public String getNombre() {
        return nombre;
    }
}

class Coche {
    private Persona propietario;
    
    public Coche(Persona propietario) {
        this.propietario = propietario;
    }
    
    public void mostrarPropietario() {
        System.out.println("Propietario: " + propietario.getNombre());
    }
}

public class Main {
    public static void main(String[] args) {
        Persona juan = new Persona("Juan");
        Coche cocheDeJuan = new Coche(juan);
        cocheDeJuan.mostrarPropietario(); // Salida: Propietario: Juan
    }
}

Asociación Bidireccional

En una asociación bidireccional, ambas clases tienen una referencia entre sí. Esto permite que ambas clases interactúen directamente.

Supongamos que tenemos una clase Autor y una clase Libro. Un Autor puede escribir varios Libros, y un Libro tiene un Autor.

Diagrama generado con PlantUML

@startuml
class Autor {
    -nombre : String
    -libros : List<Libro>
    +agregarLibro(Libro libro) : void
    +mostrarLibros() : void
}

class Libro {
    -titulo : String
    -autor : Autor
    +setAutor(Autor autor) : void
    +getTitulo() : String
    +mostrarAutor() : void
}

Autor "1" -- "0..*" Libro : escribe
@enduml
import java.util.ArrayList;
import java.util.List;

class Autor {
    private String nombre;
    private List<Libro> libros;

    Autor(String nombre) {
        this.nombre = nombre;
        this.libros = new ArrayList<>();
    }

    void agregarLibro(Libro libro) {
        libros.add(libro);
        libro.setAutor(this);
    }

    void mostrarLibros() {
        System.out.println("Libros escritos por " + nombre + ":");
        for (Libro libro : libros) {
            System.out.println("- " + libro.getTitulo());
        }
    }
}

class Libro {
    private String titulo;
    private Autor autor;

    Libro(String titulo) {
        this.titulo = titulo;
    }

    void setAutor(Autor autor) {
        this.autor = autor;
    }

    String getTitulo() {
        return titulo;
    }

    void mostrarAutor() {
        System.out.println("El libro '" + titulo + "' fue escrito por " + autor.getNombre());
    }
}

public class Main {
    public static void main(String[] args) {
        Autor autor = new Autor("Gabriel García Márquez");
        Libro libro1 = new Libro("Cien años de soledad");
        Libro libro2 = new Libro("El amor en los tiempos del cólera");

        autor.agregarLibro(libro1);
        autor.agregarLibro(libro2);

        autor.mostrarLibros();
        libro1.mostrarAutor();
    }
}

Explicación:

  • La clase Autor tiene una lista de Libros, y la clase Libro tiene una referencia a un Autor.
  • Las flechas (-->) en ambas direcciones indican una asociación bidireccional.
  • La multiplicidad 1 y 0..* indica que un Autor puede escribir muchos Libros, pero cada Libro tiene un solo Autor.

Importancia de Comprender estas Relaciones

Entender y aplicar correctamente las relaciones entre clases permite:

  • Diseño coherente: Facilita la creación de sistemas donde los componentes interactúan de manera lógica y estructurada.
  • Mantenimiento simplificado: Un diseño claro reduce la complejidad al actualizar o modificar el código.
  • Reutilización de código: Promueve la creación de componentes modulares que pueden ser reutilizados en diferentes partes del sistema o en proyectos futuros.

Conclusión

Las relaciones entre clases son fundamentales en la programación orientada a objetos. En esta primera parte hemos visto las relaciones básicas entre clases en POO. Estas permiten estructurar el código de manera clara y modular:

RelaciónDescripciónSímbolo en PlantUML
HerenciaUna clase hereda de otra atributos y comportamientos.<|– o –|>
DependenciaUna clase usa temporalmente a otra sin que una haya relación permanente.<.. o ..> 
AsociaciónUna o ambas clases tienen una referencia a la otra para interactuar.<-- o --> (unidireccional)
–(bidireccional)

También hemos visto cómo con PlantUML, podemos visualizar estas relaciones de manera clara y sencilla.

En la siguiente parte exploraremos relaciones más complejas como realización/implementación, agregación y composición.


Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.