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:
Un diagrama de clases consta de los siguientes elementos clave:
Representadas por rectángulos divididos en tres secciones:
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
}
Diferentes tipos de líneas y conectores representan distintas relaciones entre las clases:
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:
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
// ...
}
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
// ...
}
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.
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.
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.
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
// ...
}
Los símbolos como +
(público), -
(privado) y #
(protegido) indican el nivel de acceso de atributos y métodos.
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.
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...
}
Un diagrama de clases completo se puede ver así:
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.
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.
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.