Modelo mental de POO: clases, objetos y responsabilidades

Capítulo 1

Tiempo estimado de lectura: 9 minutos

+ Ejercicio

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, Pedido contiene LineaDePedido y referencia a Cliente).

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).

Continúa en nuestra aplicación.
  • Escuche el audio con la pantalla apagada.
  • Obtenga un certificado al finalizar.
  • ¡Más de 5000 cursos para que explores!
O continúa leyendo más abajo...
Download App

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#)

ConceptoJavaC#
Constructorpublic Pedido(...)public Pedido(...)
Camposprivate final String id;private readonly string _id;
Métodospublic void agregarLinea(...)public void AgregarLinea(...)
PropiedadesNo nativas; se usan getters (getId())public string Id { get; }
ListasList<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:

  • LineaDePedido valida su propia consistencia y calcula su subtotal().
  • Pedido controla cómo se agregan líneas y calcula el total() a partir de ellas.
  • Cliente representa 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); comportamiento devolver().
  • 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.
  • AgendaDeSala o Calendario: 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: ________

Ahora responde el ejercicio sobre el contenido:

Al repartir responsabilidades en un modelo orientado a objetos, ¿dónde debería ubicarse una regla que usa principalmente el estado de un objeto (por ejemplo, calcular un subtotal con cantidad y precio unitario)?

¡Tienes razón! Felicitaciones, ahora pasa a la página siguiente.

¡Tú error! Inténtalo de nuevo.

Si una regla depende principalmente del estado de un objeto, debe vivir en ese objeto. Así se protegen invariantes y se evita una clase anémica (solo datos) o una clase “Dios” que concentra toda la lógica.

Siguiente capítulo

Encapsulación aplicada: invariantes, acceso controlado y propiedades

Arrow Right Icon
Portada de libro electrónico gratuitaPOO práctica y clara: clases, encapsulación, herencia y polimorfismo (con ejemplos en Java y C#)
10%

POO práctica y clara: clases, encapsulación, herencia y polimorfismo (con ejemplos en Java y C#)

Nuevo curso

10 páginas

Descarga la aplicación para obtener una certificación gratuita y escuchar cursos en segundo plano, incluso con la pantalla apagada.