UML - Diagrama de Clases

Un diagrama de clases describe los tipos de objetos (sus atributos, operaciones y restricciones) que hay en el sistema y las relaciones estáticas que existen entre ellos.

Básicamente hay dos tipos de relaciones:

  • Asociaciones: Un Cliente hace un Pedido
  • Subtipos: Un Profesor es un subtipo de Persona

Componentes

Un diagrama de clases consta de los siguientes elementos clave:

Clases

Representadas por rectángulos divididos en tres secciones:

    • Nombre de la Clase: La parte superior muestra el nombre de la clase.
    • Atributos: La sección del medio lista las propiedades o datos que la clase mantiene.
    • Métodos: La sección inferior detalla las operaciones o funciones que la clase puede realizar.
singleton

En código:


  public class Pedido {
    // Atributos privados
    private Date fecha;
    private int cantidad;
    private double precio;

    // Constructor
    public Pedido(Date fecha, int cantidad, double precio) {
        this.fecha = fecha;
        this.cantidad = cantidad;
        this.precio = precio;
    }

    // Métodos públicos
    public void despacha() {
        // Implementación del método despacha
    }

    public void cierra() {
        // Implementación del método cierra
    }

    // Podríamos agregar los métodos de acceso (getters) y
    // modificación (setters) de los atributos privados
  }
                

Relaciones

Diferentes tipos de líneas y conectores representan distintas relaciones entre las clases:

  • Asociación: Una línea simple que conecta dos clases y representa una relación entre ellas, como la colaboración o interacción. La multiplicidad indica la cantidad de objetos que participan en la relación, en general indica los límites inferior y superior de los objetos participantes. El “*” entre Cliente y Pedido indica que Cliente puede tener muchos Pedidos asociados a él; el “1” indica que un Pedido viene de un solo Cliente. En la relación, Cliente participa con una instancia, Pedido con 0 a muchas.
singleton

Creamos el código equivalente:


  public class Pedido {
    private Date fecha;
    private int cantidad;
    private double precio;
    private Cliente cliente; // Asociación con Cliente

    public Pedido(Date fecha, int cantidad, double precio, Cliente cliente) {
        this.fecha = fecha;
        this.cantidad = cantidad;
        this.precio = precio;
        this.cliente = cliente;
    }

    public void despacha() {
        // Implementación del método despacha
    }

    public void cierra() {
        // Implementación del método cierra
    }

    // Getters y setters para los atributos, incluyendo cliente
    // ...

    public Cliente getCliente() {
        return cliente;
    }

    public void setCliente(Cliente cliente) {
        this.cliente = cliente;
    }
  }

  public class Cliente {
      private String nombre;
      private String direccion;
      private List<Pedido> pedidos; // Lista de pedidos asociados con el cliente

      public Cliente(String nombre, String direccion) {
          this.nombre = nombre;
          this.direccion = direccion;
          this.pedidos = new ArrayList<>();
      }

      public String calificacionCredito() {
          // Implementación del método calificacionCredito
          return "Calificación del crédito";
      }

      // Getters y setters para nombre, dirección y pedidos
      // ...

      public void addPedido(Pedido pedido) {
          pedidos.add(pedido);
      }

      public List<Pedido> getPedidos() {
          return pedidos;
      }
  }   
            

Si agregamos Navegabilidad, con Pedido apuntando a Cliente:

singleton

Decimos que Pedido tiene la responsabilidad de decir a qué Cliente corresponde, pero un Cliente no tiene la capacidad para decir cuáles son los pedidos que tiene. O sea, Pedido tiene referencia a Cliente, Cliente no tiene referencia a Pedido. El código quedaría así:


  public class Pedido {
      private Date fecha;
      private int cantidad;
      private double precio;
      private Cliente cliente; // Asociación con Cliente

      public Pedido(Date fecha, int cantidad, double precio, Cliente cliente) {
          this.fecha = fecha;
          this.cantidad = cantidad;
          this.precio = precio;
          this.cliente = cliente;
      }

      public void despacha() {
          // Implementación del método despacha
      }

      public void cierra() {
          // Implementación del método cierra
      }

      // Getters y setters para los atributos, incluyendo cliente
      // ...

      public Cliente getCliente() {
          return cliente;
      }

      public void setCliente(Cliente cliente) {
          this.cliente = cliente;
      }
  }

  public class Cliente {
      private String nombre;
      private String direccion;

      public Cliente(String nombre, String direccion) {
          this.nombre = nombre;
          this.direccion = direccion;
      }

      public String calificacionCredito() {
          // Implementación del método calificacionCredito
          return "Calificación del crédito";
      }

      // Getters y setters para nombre, dirección
      // ...
  }              
                
                
  • Herencia: Una línea que termina en un triángulo apuntando hacia una superclase, indicando que una clase hereda de otra.
singleton

Lo convertimos a código:


  // Clase base Cliente
  public class Cliente {
      private String nombre;
      private String direccion;
  
      public Cliente(String nombre, String direccion) {
          this.nombre = nombre;
          this.direccion = direccion;
      }
  
      public String calificacionCredito(){
          return "calificacion cliente";
      }
  
      // Getters y setters para nombre y dirección
      public String getNombre() {
          return nombre;
      }
  
      public void setNombre(String nombre) {
          this.nombre = nombre;
      }
  
      public String getDireccion() {
          return direccion;
      }
  
      public void setDireccion(String direccion) {
          this.direccion = direccion;
      }
  }
  
  // Subclase Cliente Corporativo
  public class ClienteCorporativo extends Cliente {
      private String nombreContacto;
      private String calificacionCredito;
      private double limiteCredito;
  
      public ClienteCorporativo(String nombre, String direccion, String nombreContacto, double limiteCredito) {
          super(nombre, direccion);
          this.nombreContacto = nombreContacto;
          this.limiteCredito = limiteCredito;
      }
  
      @Override
      public String calificacionCredito() {
          // Implementación específica para cliente corporativo, podría usar la de la clase base
          return this.calificacionCredito;
      }
  
      // Implementación de otros métodos como recuerda() y facturacionMes()
      public void recuerda() {
          // Implementación del método recuerda
      }
  
      public void facturacionMes(Integer mes) {
          // Implementación del método facturacionMes
      }
  
      // Getters y setters para los atributos de Cliente Corporativo
      // ...
  }
  
  // Subclase Cliente Personal
  public class ClientePersonal extends Cliente {
      private String numeroTarjetaCredito;
  
      public ClientePersonal(String nombre, String direccion, String numeroTarjetaCredito) {
          super(nombre, direccion);
          this.numeroTarjetaCredito = numeroTarjetaCredito;
      }
  
      // Getters y setters para el número de tarjeta de crédito
      // ...
  }
                  
                
  • Agregación: Una línea que termina en un diamante abierto en el lado de la clase contenedora. Un objeto está relacionado con otros objetos, teniendo los segundos una existencia propia. Si el sistema borra la librería, los libros pueden seguir existiendo, por eso esta relación se considera débil en comparación con la composición.
singleton

De diagrama a código:


  public class Libreria {
    // Lista que representa la agregación de libros
    private List <Libro> libros;

    public Libreria() {
        this.libros = new ArrayList<>();
    }

    // Método para agregar un libro a la librería
    // Como es una agregación, no establecemos la librería dentro del libro
    public void addLibro(Libro libro) {
        if (libro != null && !libros.contains(libro)) {
            libros.add(libro);
        }
    }

    // Getter para libros
    public List<Libro> getLibros() {
        return libros;
    }
}

// Definición de la clase Libro
public class Libro {
    // Atributos del libro, como título, autor, etc.

    // No hay necesidad de mantener una referencia a Librería en el Libro para una agregación
    // Constructor simple sin argumentos
    public Libro() {
        // Atributos del libro se inicializarían aquí
    }

    // Otros métodos y atributos específicos del libro
}
                                  
                

La clase Libro no tiene un atributo que referencie a Librería, reflejando que la agregación no implica una dependencia de vida entre el Libro y la Librería, un Libro puede existir sin estar asociado a una Librería.

  • Composición: Similar a la agregación, pero con un diamante relleno, indicando que ambos están al mismo nivel. La existencia de uno depende del otro, la parte no puede existir sin el todo. Se borra el polígono, se borran todos los puntos.
singleton

En código:


  // Definición de la clase Punto
  public class Punto {
      private double x;
      private double y;
  
      // Constructor
      public Punto(double x, double y) {
          this.x = x;
          this.y = y;
      }
  
      // Getters y setters para X e Y
      public double getX() {
          return x;
      }
  
      public void setX(double x) {
          this.x = x;
      }
  
      public double getY() {
          return y;
      }
  
      public void setY(double y) {
          this.y = y;
      }
  }
  
  // Definición de la clase Poligono
  public class Poligono {
      // Una lista que mantiene la composición de puntos
      private List<Punto> puntos;
  
      // Constructor
      public Poligono() {
          this.puntos = new ArrayList<>();
      }
  
      // Método para agregar un punto al polígono
      public void addPunto(Punto punto) {
          if (punto != null) {
              puntos.add(punto);
          }
      }
  
      // Método para remover un punto del polígono
      public void removePunto(Punto punto) {
          puntos.remove(punto);
      }
  
      // Getters para la lista de puntos
      public List<Punto> getPuntos() {
          return puntos;
      }
  
      // Otros métodos relacionados con el polígono
      // Por ejemplo, calcular el área, el perímetro, etc.
  }
                                    
                

En este código, la clase Poligono gestiona una lista de Punto. La composición se refleja en que los Punto son creados y gestionados por la clase Poligono. No hay métodos para obtener o establecer la lista completa de Punto directamente para evitar romper el encapsulamiento de la composición; en lugar de eso, se proveen métodos para agregar o remover puntos individuales.

Además, no hay un método setPoligono en la clase Punto, ya que en una relación de composición el objeto compuesto (en este caso Poligono) es responsable de la creación y eliminación de las partes (los Punto), y estas partes no deben existir de manera independiente, fuera del todo.

Interfaces

Una interfaz es una clase sin implementación, tiene declaraciones de operaciones pero no atributos ni cuerpos de métodos.

Se representan como clases pero con la palabra <<interface>> sobre el nombre, y pueden estar conectadas a las clases que las implementan mediante líneas punteadas. Se suelen escribir en itálica los métodos abstractos.

singleton

Podemos escribirlo así:


  // Definición de la interfaz Imprimible
  public interface Imprimible {
      void imprimir();
  }
  
  // Fecha que implementa la interfaz Imprimible
  public class Fecha implements Imprimible {
      private int dia;
      private int mes;
      private int anio;
  
      // Constructor
      public Fecha(int dia, int mes, int anio) {
          this.dia = dia;
          this.mes = mes;
          this.anio = anio;
      }
  
      // Método de la interfaz Imprimible
      @Override
      public void imprimir() {
          System.out.println(dia + "/" + mes + "/" + anio);
      }
  
      // Getters y setters para dia, mes y anio
      // ...
  }
  
  // Empleado que implementa la interfaz Imprimible
  public class Empleado implements Imprimible {
      private String nombre;
      private String legajo;
  
      // Constructor
      public Empleado(String nombre, String legajo) {
          this.nombre = nombre;
          this.legajo = legajo;
      }
  
      // Método de la interfaz Imprimible
      @Override
      public void imprimir() {
          System.out.println("Nombre: " + nombre + ", Legajo: " + legajo);
      }
  
      // Getters y setters para nombre y legajo
      // ...
  }
                    
                

Visibilidad

Los símbolos como + (público), - (privado) y # (protegido) indican el nivel de acceso de atributos y métodos.

singleton

Clases de Asociación

Una clase de asociación permite modelar una relación (asociación) entre dos o más clases, y además tiene sus propios atributos o métodos, es decir, conecta la asociación mediante una entidad independiente.

singleton

Pasado a código:


                
  public class Compania {
      private String id;
      private String nombre;
      // Otros atributos y métodos...
  }
  
  public class Persona {
      private String nombre;
      private String documento;
      // Otros atributos y métodos...
  }
  
  public class Empleo {
      private Persona persona;
      private Compania compania;
      private Date fechaInicio;
      private Date fechaFin;
  
      public Empleo(Persona persona, Compania compania, Date fechaInicio, Date fechaFin) {
          this.persona = persona;
          this.compania = compania;
          this.fechaInicio = fechaInicio;
          this.fechaFin = fechaFin;
      }
  
      // Otros métodos...
  }
  
                

Diagrama Completo

Un diagrama de clases completo se puede ver así:

singleton

Navegabilidad

Para llevar este modelo conceptual a un modelo apuntado a la implementación, debemos agregarle la navegabilidad. Con Pedido apuntando a Cliente decimos que Pedido tiene la responsabilidad de decir a qué Cliente corresponde, pero un Cliente no tiene la capacidad para decir cuáles son los pedidos que tiene.

La navegabilidad es importante en los diagramas de implementación y especificación, sin embargo, no es fundamental en los diagramas conceptuales.

singleton

Propósito y Uso

  • Diseño de Software: Los diagramas de clases son esenciales durante la fase de diseño, ayudando a los desarrolladores a definir la estructura necesaria del código antes de su implementación.

  • Documentación: Proveen una excelente herramienta de documentación que detalla cómo está construido el sistema, útil para el mantenimiento y la comprensión futura del código.

  • Comunicación: Facilitan la comunicación entre diferentes miembros del equipo de desarrollo, así como con partes interesadas técnicas y analistas.

Conclusión

El diagrama de clases es un instrumento vital en UML y en la ingeniería de software, no solo ayuda a los desarrolladores a pensar y diseñar de manera estructurada y organizada, sino que también asegura que el equipo de desarrollo comparta una comprensión común del diseño del sistema.

Al dominar los diagramas de clases, se fortalece la base para un desarrollo de software más eficiente y menos propenso a errores.