Encapsulación aplicada: invariantes, acceso controlado y propiedades

Capítulo 2

Tiempo estimado de lectura: 7 minutos

+ Ejercicio

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) y removerItem(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.

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

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

NecesidadJava (recomendación)C# (recomendación)
Leer valor simplegetX() (ok)public T X { get; } o { get; private set; }
Cambiar valor con reglasMétodo con intención (cambiarX)Método con intención; o private set + método
Evitar estado inválidoValidar en constructor y métodos; evitar setters públicosValidar en constructor y métodos; evitar set público
Exponer colecciónunmodifiableList / copia / operacionesIReadOnlyList, 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) / init o solo getters (C#)?
  • ¿Las excepciones son específicas y comunican la causa? (IllegalArgumentException, InvalidOperationException, etc.)

Ahora responde el ejercicio sobre el contenido:

¿Cuál diseño protege mejor los invariantes y reduce el acoplamiento al manejar una colección interna en una clase?

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

¡Tú error! Inténtalo de nuevo.

Encapsular implica que solo la clase controle cambios que afectan invariantes. Por eso se evitan setters genéricos y la exposición de colecciones mutables; en su lugar se usan métodos con intención y se devuelve una vista de solo lectura o inmutable.

Siguiente capítulo

Abstracción útil: contratos, cohesión y diseño orientado al cambio

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

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.