Encapsulación como protección de invariantes y reducción de acoplamiento
Encapsular no es “poner getters y setters”, sino proteger invariantes (reglas que siempre deben cumplirse) y reducir acoplamiento (que el resto del sistema dependa lo mínimo posible de detalles internos). Una clase bien encapsulada expone una API pública pequeña y con intención, y mantiene sus campos y estructuras internas bajo control para evitar estados inválidos.
Invariantes: la razón práctica para encapsular
Un invariante es una condición que debe ser verdadera antes y después de cualquier operación pública. Ejemplos típicos: saldo no negativo, lista de ítems no nula, cantidad positiva, fechas coherentes, límites de crédito, etc. La encapsulación permite que solo la clase pueda modificar su estado, y que cada modificación pase por validaciones.
- Regla: si una propiedad afecta un invariante, no debería ser un campo público ni tener un setter libre.
- Regla: valida invariantes en constructores y en métodos que cambian estado.
- Regla: si puedes, diseña objetos inmutables (menos estados intermedios, menos errores).
API pública: métodos con intención vs. setters genéricos
Un setter genérico suele filtrar detalles internos y permite cambios arbitrarios. En cambio, un método con intención expresa una operación del dominio y puede validar reglas.
setItems(lista)permite reemplazar todo sin control y expone estructura interna.agregarItem(item)yremoverItem(id)permiten validar cantidad, duplicados, stock, límites, etc.
Guía práctica paso a paso para encapsular bien
Paso 1: declara invariantes explícitos
Antes de escribir código, lista 3–6 reglas que siempre deben cumplirse. Ejemplo para un carrito: “no aceptar cantidades <= 0”, “no permitir ítems nulos”, “no exponer la lista interna”, “total calculado desde ítems”.
Paso 2: oculta campos y reduce superficie pública
Haz los campos privados y expón solo lo necesario. Evita exponer referencias directas a estructuras mutables.
- Escuche el audio con la pantalla apagada.
- Obtenga un certificado al finalizar.
- ¡Más de 5000 cursos para que explores!
Descargar la aplicación
Paso 3: valida en constructor y en métodos mutadores
El constructor debe impedir que el objeto nazca inválido. Los métodos deben impedir que el objeto llegue a un estado inválido.
Paso 4: evita exponer colecciones mutables
Si devuelves una lista interna, el consumidor puede modificarla sin pasar por validaciones. Soluciones: devolver una vista de solo lectura, una copia defensiva, o un iterador/stream.
Paso 5: prefiere inmutabilidad cuando sea viable
Si el objeto no necesita cambiar, hazlo inmutable: campos final en Java, propiedades solo lectura en C#. Si necesita cambiar, encapsula los cambios en métodos con intención.
Ejemplo en Java: invariantes, validación y colecciones seguras
Carrito con métodos con intención y lista no exponible
import java.math.BigDecimal;import java.util.ArrayList;import java.util.Collections;import java.util.List;import java.util.Objects;public final class Carrito { private final List<LineaCarrito> lineas = new ArrayList<>(); public Carrito() { // Invariante: lineas nunca es null (ya garantizado) } public void agregarItem(String sku, int cantidad, BigDecimal precioUnitario) { if (sku == null || sku.isBlank()) { throw new IllegalArgumentException("SKU requerido"); } if (cantidad <= 0) { throw new IllegalArgumentException("Cantidad debe ser > 0"); } if (precioUnitario == null || precioUnitario.signum() < 0) { throw new IllegalArgumentException("Precio unitario debe ser >= 0"); } lineas.add(new LineaCarrito(sku, cantidad, precioUnitario)); } public void removerPorSku(String sku) { Objects.requireNonNull(sku, "sku"); lineas.removeIf(l -> l.sku().equals(sku)); } public List<LineaCarrito> getLineas() { // No se expone la lista mutable interna return Collections.unmodifiableList(lineas); } public BigDecimal total() { return lineas.stream() .map(LineaCarrito::subtotal) .reduce(BigDecimal.ZERO, BigDecimal::add); }}record LineaCarrito(String sku, int cantidad, BigDecimal precioUnitario) { LineaCarrito { if (sku == null || sku.isBlank()) throw new IllegalArgumentException("SKU requerido"); if (cantidad <= 0) throw new IllegalArgumentException("Cantidad > 0"); if (precioUnitario == null || precioUnitario.signum() < 0) throw new IllegalArgumentException("Precio >= 0"); } BigDecimal subtotal() { return precioUnitario.multiply(BigDecimal.valueOf(cantidad)); }}Claves de encapsulación en este ejemplo: la lista interna es privada, no hay setLineas, y cualquier cambio pasa por agregarItem/removerPorSku. Además, el record valida en su constructor compacto, evitando líneas inválidas.
Patrones de acceso en Java: getters/setters con criterio
En Java es común ver getters/setters por defecto, pero conviene aplicar estas reglas:
- No crear setters si rompen invariantes o permiten estados intermedios inválidos.
- Si necesitas cambiar un valor, prefiere un método con intención:
cambiarEmail(...),activar(),marcarComoPagado(). - Para colecciones:
Collections.unmodifiableList, copias defensivas (new ArrayList<>(lineas)) o exponer operaciones (agregar/remover) en lugar de exponer la colección.
Ejemplo en C#: propiedades, validación y estados consistentes
Propiedades con set privado y métodos con intención
using System;using System.Collections.Generic;using System.Collections.ObjectModel;public class CuentaBancaria{ private readonly List<Movimiento> _movimientos = new(); public string Numero { get; } public decimal Saldo { get; private set; } public ReadOnlyCollection<Movimiento> Movimientos => _movimientos.AsReadOnly(); public CuentaBancaria(string numero, decimal saldoInicial) { if (string.IsNullOrWhiteSpace(numero)) throw new ArgumentException("Número requerido", nameof(numero)); if (saldoInicial < 0) throw new ArgumentOutOfRangeException(nameof(saldoInicial), "Saldo inicial no puede ser negativo"); Numero = numero; Saldo = saldoInicial; // Invariante: Saldo >= 0, _movimientos no null } public void Depositar(decimal monto) { if (monto <= 0) throw new ArgumentOutOfRangeException(nameof(monto), "Monto debe ser > 0"); Saldo += monto; _movimientos.Add(new Movimiento(DateTime.UtcNow, monto, "DEP")); } public void Retirar(decimal monto) { if (monto <= 0) throw new ArgumentOutOfRangeException(nameof(monto), "Monto debe ser > 0"); if (Saldo - monto < 0) throw new InvalidOperationException("Fondos insuficientes"); Saldo -= monto; _movimientos.Add(new Movimiento(DateTime.UtcNow, -monto, "RET")); }}public record Movimiento(DateTime FechaUtc, decimal Importe, string Tipo);En C#, las propiedades permiten expresar claramente el acceso controlado: Saldo tiene private set para impedir modificaciones externas, y los cambios pasan por Depositar/Retirar con validación. La colección se expone como solo lectura mediante AsReadOnly().
Comparación práctica: getters/setters vs. propiedades (C#) vs. acceso en Java
| Necesidad | Java (recomendación) | C# (recomendación) |
|---|---|---|
| Leer valor simple | getX() (ok) | public T X { get; } o { get; private set; } |
| Cambiar valor con reglas | Método con intención (cambiarX) | Método con intención; o private set + método |
| Evitar estado inválido | Validar en constructor y métodos; evitar setters públicos | Validar en constructor y métodos; evitar set público |
| Exponer colección | unmodifiableList / copia / operaciones | IReadOnlyList, ReadOnlyCollection, copia / operaciones |
Errores comunes y cómo corregirlos
Error 1: setters que permiten estados intermedios inválidos
Ejemplo típico: setPrecio y setDescuento por separado, donde durante un instante el objeto queda inconsistente. Solución: un método que aplique el cambio completo y valide todo junto.
// Java: mejor que setPrecio/setDescuento separadospublic void actualizarCondiciones(BigDecimal precio, BigDecimal descuento) { if (precio == null || precio.signum() < 0) throw new IllegalArgumentException("precio"); if (descuento == null || descuento.signum() < 0) throw new IllegalArgumentException("descuento"); if (descuento.compareTo(precio) > 0) throw new IllegalArgumentException("Descuento no puede superar el precio"); this.precio = precio; this.descuento = descuento;}Error 2: exponer la lista interna y perder control
Si haces getItems() y devuelves la lista real, alguien puede hacer getItems().clear() y romper invariantes. Solución: vista inmutable, copia defensiva o API de operaciones.
Error 3: validar solo en UI o en servicios externos
La validación en capas externas ayuda, pero la clase debe ser la última línea de defensa de sus invariantes. Si el objeto puede existir en memoria en estado inválido, el bug aparecerá tarde y será difícil de rastrear.
Checklist de encapsulación aplicada
- ¿Cuáles son los invariantes? ¿Están validados en constructor y métodos?
- ¿Hay setters públicos que permitan saltarse reglas? Si sí, ¿pueden reemplazarse por métodos con intención?
- ¿Se exponen colecciones mutables? Si sí, ¿pueden devolverse como solo lectura o mediante copias?
- ¿El objeto puede ser inmutable? Si sí, ¿puedes usar
final(Java) /inito solo getters (C#)? - ¿Las excepciones son específicas y comunican la causa? (
IllegalArgumentException,InvalidOperationException, etc.)