Del dominio al código: identificar entidades y responsabilidades
Un buen modelo orientado a objetos empieza antes de escribir código: en el dominio (el problema real). La meta no es “crear clases”, sino asignar responsabilidades a objetos que colaboran para cumplir reglas del negocio.
Guía práctica paso a paso para encontrar clases
- 1) Lee los requisitos y subraya sustantivos y verbos. Los sustantivos suelen sugerir entidades (Cliente, Pedido, Producto). Los verbos suelen sugerir comportamientos (calcular total, agregar línea, confirmar).
- 2) Separa “cosas” de “datos sueltos”. Si un concepto tiene reglas propias y cambia con el tiempo, probablemente merece una clase. Si solo es un valor (código postal, porcentaje), puede ser un tipo simple o un Value Object.
- 3) Pregunta: ¿quién es responsable de esta regla? La regla debe vivir donde tenga sentido. Ejemplo: “el total del pedido es la suma de sus líneas” suena a responsabilidad de
Pedido, no de una pantalla ni de un servicio genérico. - 4) Define límites: qué sabe y qué hace cada clase. Escribe 3–5 responsabilidades por clase. Si no puedes, quizá la clase no existe o está mal nombrada.
- 5) Revisa colaboraciones. Si una clase necesita datos de otra para cumplir una regla, define una relación clara (por ejemplo,
PedidocontieneLineaDePedidoy referencia aCliente).
Conceptos base: objeto, estado, comportamiento e identidad
Objeto
Un objeto es una instancia concreta de una clase que combina datos y operaciones para cumplir responsabilidades. En el dominio: “el pedido #1234 del cliente Ana”.
Estado
El estado son los datos internos que describen al objeto en un momento dado. Ejemplo: en un pedido, sus líneas, su estado (borrador/confirmado), su cliente.
Comportamiento
El comportamiento son las operaciones que el objeto ofrece para cumplir reglas del negocio y proteger su consistencia. Ejemplo: agregarLinea(), calcularTotal(), confirmar().
Identidad
La identidad distingue un objeto de otro aunque su estado sea igual. Dos pedidos pueden tener las mismas líneas y total, pero siguen siendo pedidos diferentes (por ejemplo, por su id).
- Escuche el audio con la pantalla apagada.
- Obtenga un certificado al finalizar.
- ¡Más de 5000 cursos para que explores!
Descargar la aplicación
Criterios prácticos: qué pertenece a una clase y qué no
Checklist para decidir si algo es una clase
- Tiene reglas propias (invariantes): “una línea debe tener cantidad > 0”.
- Tiene ciclo de vida: se crea, cambia, se confirma/cancela.
- Colabora con otros: interactúa con Cliente, Producto, etc.
- Necesita proteger consistencia: no quieres que cualquiera modifique sus datos sin pasar por validaciones.
Qué NO debería ir dentro de una entidad
- Detalles de UI: formateo, textos, colores, validaciones de pantalla.
- Persistencia directa (en modelos simples puede tolerarse, pero como regla general): SQL, llamadas HTTP, acceso a repositorios.
- Orquestación “de todo”: lógica que coordina múltiples agregados sin pertenecer claramente a uno (suele ir a un servicio de aplicación).
Evitar dos anti-patrones: clase “Dios” y clase anémica
Clase “Dios” (God Object)
Señales típicas:
- Demasiados campos y métodos, nombres genéricos (
Manager,Helper,System). - Conoce detalles de muchas partes del dominio.
- Hace cálculos, valida, persiste, envía emails, todo en el mismo lugar.
Corrección práctica: divide por responsabilidades. Si un método usa principalmente datos de otra clase, quizá esa otra clase debería tener el método.
Clase anémica
Señales típicas:
- Solo tiene getters/setters y ninguna regla.
- Las reglas viven en servicios externos que manipulan datos “crudos”.
Corrección práctica: mueve reglas e invariantes al objeto que posee los datos. Por ejemplo, si Pedido tiene líneas, Pedido debe controlar cómo se agregan y cómo se calcula el total.
Ejemplo paralelo (misma idea) en Java y C#: Cliente, Pedido y LíneaDePedido
Modelaremos un escenario mínimo: un cliente crea un pedido, agrega líneas (producto, cantidad, precio unitario) y el pedido calcula su total. La regla: no se puede agregar una línea con cantidad menor o igual a cero.
Equivalencias sintácticas básicas (Java vs C#)
| Concepto | Java | C# |
|---|---|---|
| Constructor | public Pedido(...) | public Pedido(...) |
| Campos | private final String id; | private readonly string _id; |
| Métodos | public void agregarLinea(...) | public void AgregarLinea(...) |
| Propiedades | No nativas; se usan getters (getId()) | public string Id { get; } |
| Listas | List<T> (java.util) | List<T> (System.Collections.Generic) |
Java: modelo con responsabilidades
import java.math.BigDecimal;import java.util.ArrayList;import java.util.Collections;import java.util.List;import java.util.Objects;class Cliente { private final String id; private final String nombre; public Cliente(String id, String nombre) { this.id = Objects.requireNonNull(id); this.nombre = Objects.requireNonNull(nombre); } public String getId() { return id; } public String getNombre() { return nombre; }}class LineaDePedido { private final String producto; private final int cantidad; private final BigDecimal precioUnitario; public LineaDePedido(String producto, int cantidad, BigDecimal precioUnitario) { this.producto = Objects.requireNonNull(producto); if (cantidad <= 0) throw new IllegalArgumentException("cantidad debe ser > 0"); this.cantidad = cantidad; this.precioUnitario = Objects.requireNonNull(precioUnitario); if (precioUnitario.signum() < 0) throw new IllegalArgumentException("precioUnitario no puede ser negativo"); } public BigDecimal subtotal() { return precioUnitario.multiply(BigDecimal.valueOf(cantidad)); } public String getProducto() { return producto; } public int getCantidad() { return cantidad; } public BigDecimal getPrecioUnitario() { return precioUnitario; }}class Pedido { private final String id; private final Cliente cliente; private final List<LineaDePedido> lineas = new ArrayList<>(); public Pedido(String id, Cliente cliente) { this.id = Objects.requireNonNull(id); this.cliente = Objects.requireNonNull(cliente); } public void agregarLinea(String producto, int cantidad, BigDecimal precioUnitario) { lineas.add(new LineaDePedido(producto, cantidad, precioUnitario)); } public BigDecimal total() { BigDecimal total = BigDecimal.ZERO; for (LineaDePedido l : lineas) { total = total.add(l.subtotal()); } return total; } public String getId() { return id; } public Cliente getCliente() { return cliente; } public List<LineaDePedido> getLineas() { return Collections.unmodifiableList(lineas); }}Observa la distribución de responsabilidades:
LineaDePedidovalida su propia consistencia y calcula susubtotal().Pedidocontrola cómo se agregan líneas y calcula eltotal()a partir de ellas.Clienterepresenta identidad y datos del cliente; no calcula totales ni valida líneas.
C#: el mismo modelo con propiedades
using System;using System.Collections.Generic;public class Cliente{ public string Id { get; } public string Nombre { get; } public Cliente(string id, string nombre) { Id = id ?? throw new ArgumentNullException(nameof(id)); Nombre = nombre ?? throw new ArgumentNullException(nameof(nombre)); }}public class LineaDePedido{ public string Producto { get; } public int Cantidad { get; } public decimal PrecioUnitario { get; } public LineaDePedido(string producto, int cantidad, decimal precioUnitario) { Producto = producto ?? throw new ArgumentNullException(nameof(producto)); if (cantidad <= 0) throw new ArgumentException("Cantidad debe ser > 0", nameof(cantidad)); if (precioUnitario < 0) throw new ArgumentException("PrecioUnitario no puede ser negativo", nameof(precioUnitario)); Cantidad = cantidad; PrecioUnitario = precioUnitario; } public decimal Subtotal() => PrecioUnitario * Cantidad;}public class Pedido{ public string Id { get; } public Cliente Cliente { get; } private readonly List<LineaDePedido> _lineas = new(); public IReadOnlyList<LineaDePedido> Lineas => _lineas.AsReadOnly(); public Pedido(string id, Cliente cliente) { Id = id ?? throw new ArgumentNullException(nameof(id)); Cliente = cliente ?? throw new ArgumentNullException(nameof(cliente)); } public void AgregarLinea(string producto, int cantidad, decimal precioUnitario) { _lineas.Add(new LineaDePedido(producto, cantidad, precioUnitario)); } public decimal Total() { decimal total = 0m; foreach (var l in _lineas) total += l.Subtotal(); return total; }}Equivalencias clave:
- En Java se exponen datos con getters; en C# es común usar propiedades de solo lectura.
- En ambos, el constructor es el punto natural para asegurar invariantes básicas.
- En ambos, se protege la colección interna exponiendo una vista de solo lectura (
unmodifiableList/IReadOnlyList).
Mini-guía: repartir responsabilidades sin caer en extremos
Regla de oro: “los datos y las reglas viajan juntos”
Si una regla usa principalmente el estado de un objeto, esa regla debería vivir en ese objeto. Ejemplo: LineaDePedido.subtotal() usa cantidad y precio unitario, por eso está en la línea.
Preguntas rápidas para refactorizar el modelo
- ¿Este método usa más datos de otra clase que de la suya? Considera moverlo.
- ¿Esta clase tiene más de 5–7 responsabilidades? Probablemente está creciendo hacia “Dios”. Divide por conceptos del dominio.
- ¿Esta clase solo transporta datos? Busca una regla que le pertenezca (validación, cálculo, transición de estado). Si no existe, quizá no sea una entidad sino un DTO.
- ¿Hay invariantes sin proteger? Si cualquiera puede modificar campos libremente, el objeto no puede garantizar consistencia.
Ejercicios guiados de modelado (con solución sugerida)
Ejercicio 1: Biblioteca
Requisito: “Un usuario puede tomar prestado un libro por 14 días. No se puede prestar un libro si ya está prestado. Se puede devolver antes de la fecha.”
Paso 1 (sustantivos): Usuario, Libro, Préstamo, Fecha.
Paso 2 (verbos/reglas): prestar, devolver, calcular vencimiento, validar disponibilidad.
Propuesta de clases y responsabilidades:
Libro: identidad del libro; conoce si está disponible (o referencia al préstamo activo).Prestamo: fecha de inicio, fecha de vencimiento (inicio + 14 días), fecha de devolución (opcional); comportamientodevolver().Usuario: identidad del usuario; opcionalmente lista de préstamos activos (según el alcance).ServicioDePrestamos(si necesitas orquestar): crea préstamos verificando disponibilidad del libro.
Ejercicio 2: Carrito de compras
Requisito: “Un carrito permite agregar productos con cantidad. Si agregas el mismo producto, se acumula la cantidad. El total es la suma de subtotales. No se aceptan cantidades negativas.”
Propuesta de clases y responsabilidades:
Carrito: agregar ítems, acumular cantidades, calcular total, exponer ítems de solo lectura.ItemCarrito: producto, cantidad, precio unitario; validar cantidad > 0; calcular subtotal.Producto: identidad, nombre, precio actual (según el dominio).
Ejercicio 3: Reservas de sala
Requisito: “Una sala se puede reservar por franjas horarias. No se permiten reservas solapadas. Una reserva tiene organizador y participantes.”
Propuesta de clases y responsabilidades:
Sala: identidad y capacidad; conoce sus reservas o consulta disponibilidad.Reserva: franja horaria (inicio/fin), organizador, participantes; validar que inicio < fin.AgendaDeSalaoCalendario: regla de no solapamiento (si quieres separar la colección y su lógica).
Plantilla reutilizable para tus propios requisitos
- Lista de conceptos: ________
- Reglas del negocio: ________
- Clases candidatas: ________
- Responsabilidades (3–5) por clase: ________
- Colaboraciones: A usa B para ________
- Invariantes a proteger: ________