Fundamentos de Encapsulación en OOP: Information Hiding y Visibilidad
La encapsulación es uno de los cuatro principios fundamentales de la programación orientada a objetos. Oculta los estados internos y detalles de implementación de un objeto, exponiendo solo interfaces bien definidas hacia el exterior.
¿Qué es la encapsulación?
La encapsulación agrupa datos y comportamiento en una unidad única, protegiendo la representación interna contra accesos no autorizados. El objetivo es crear software robusto, mantenible y seguro mediante límites claros de responsabilidad.
Principios clave de la encapsulación
- Information Hiding: Los detalles internos permanecen ocultos
- Control de interfaz: Solo los accesos definidos son permitidos
- Preservación de invariantes: El estado del objeto se mantiene consistente
- Reducción de acoplamiento: Menos dependencias entre componentes
Modificadores de visibilidad
Niveles de visibilidad en Java
public class VisibilityDemo {
// public: accesible desde cualquier lugar
public String publicField = "öffentlich";
// protected: accesible dentro de la clase y subclases
protected String protectedField = "geschützt";
// package-private: accesible solo dentro del paquete
String packageField = "paket-privat";
// private: accesible solo dentro de la clase
private String privateField = "privat";
// Método privado - lógica interna
private void validateInput(String input) {
if (input == null || input.trim().isEmpty()) {
throw new IllegalArgumentException("Input darf nicht leer sein");
}
}
// Método público con validación
public void processData(String data) {
validateInput(data); // Utiliza el método privado
// Verarbeitung...
}
}
Modificadores de acceso en C#
public class BankAccount
{
// public: accesible desde cualquier lugar
public string AccountNumber { get; }
// private: accesible solo dentro de la clase
private decimal balance;
// protected: accesible dentro de la clase y clases derivadas
protected string AccountType { get; set; }
// internal: accesible solo dentro del assembly
internal string BankCode { get; set; }
// protected internal: accesible dentro del assembly o en clases derivadas
protected internal string BranchCode { get; set; }
// Propiedad pública con setter privado
public decimal Balance
{
get { return balance; }
private set { balance = value; }
}
}
Control de acceso en Python
class BankAccount:
def __init__(self, account_number: str):
# Atributo público
self.account_number = account_number
# Atributo protegido (por convención)
self._balance = 0.0
# Atributo privado (Name Mangling)
self.__transaction_history = []
def deposit(self, amount: float):
"""Método público con validación"""
if amount <= 0:
raise ValueError("Amount must be positive")
self._balance += amount
self.__add_transaction("deposit", amount)
def _validate_amount(self, amount: float):
"""Método protegido para subclases"""
return amount > 0
def __add_transaction(self, transaction_type: str, amount: float):
"""Método privado - solo para uso interno"""
self.__transaction_history.append({
'type': transaction_type,
'amount': amount,
'timestamp': datetime.now()
})
Getters y Setters
Implementación sensata de Getters/Setters
public class BankAccount {
private String iban;
private int balanceInCents;
private boolean isActive = true;
// Constructor con validación
public BankAccount(String iban) {
if (iban == null || !isValidIban(iban)) {
throw new IllegalArgumentException("Ungültige IBAN");
}
this.iban = iban;
this.balanceInCents = 0;
}
// Getter para acceso de lectura
public int getBalanceInCents() {
return balanceInCents;
}
// Getter con formato
public String getFormattedBalance() {
return String.format("€%.2f", balanceInCents / 100.0);
}
// Setter con validación y lógica de negocio
public void setBalanceInCents(int balanceInCents) {
if (!isActive) {
throw new IllegalStateException("Konto ist deaktiviert");
}
if (balanceInCents < 0) {
throw new IllegalArgumentException("Negativer Kontostand nicht erlaubt");
}
this.balanceInCents = balanceInCents;
}
// Método de negocio en lugar de un setter simple
public void deposit(int cents) {
if (cents <= 0) {
throw new IllegalArgumentException("Betrag muss positiv sein");
}
this.balanceInCents += cents;
}
public boolean withdraw(int cents) {
if (cents <= 0) {
throw new IllegalArgumentException("Betrag muss positiv sein");
}
if (cents > balanceInCents) {
return false; // Fondos insuficientes
}
this.balanceInCents -= cents;
return true;
}
// Método privado de validación
private boolean isValidIban(String iban) {
// Lógica de validación de IBAN
return iban != null && iban.matches("[A-Z]{2}[0-9]{20}");
}
}
Properties en Python
class BankAccount:
def __init__(self, iban: str):
self._iban = iban
self._balance = 0.0
@property
def balance(self) -> float:
"""Getter para el saldo de la cuenta"""
return self._balance
@property
def iban(self) -> str:
"""Propiedad de solo lectura para IBAN"""
return self._iban
@balance.setter
def balance(self, value: float):
"""Setter con validación"""
if value < 0:
raise ValueError("Kontostand darf nicht negativ sein")
self._balance = value
@property
def formatted_balance(self) -> str:
"""Propiedad computada"""
return f"€{self._balance:.2f}"
def deposit(self, amount: float):
"""Método de negocio en lugar de un setter directo"""
if amount <= 0:
raise ValueError("Betrag muss positiv sein")
self._balance += amount
Inmutabilidad
Objetos inmutables en Java
// Clase inmutable con campos final
public final class ImmutablePerson {
private final String name;
private final int age;
private final List<String> hobbies; // ¡Copia defensiva!
public ImmutablePerson(String name, int age, List<String> hobbies) {
this.name = Objects.requireNonNull(name, "Name darf nicht null sein");
this.age = age;
// Copia defensiva para parámetros mutables
this.hobbies = List.copyOf(Objects.requireNonNull(hobbies));
}
// Getters - ¡sin Setters!
public String getName() {
return name;
}
public int getAge() {
return age;
}
// Copia defensiva para retornos mutables
public List<String> getHobbies() {
return new ArrayList<>(hobbies);
}
// Los métodos crean nuevas instancias
public ImmutablePerson withAge(int newAge) {
return new ImmutablePerson(this.name, newAge, this.hobbies);
}
public ImmutablePerson addHobby(String hobby) {
List<String> newHobbies = new ArrayList<>(this.hobbies);
newHobbies.add(hobby);
return new ImmutablePerson(this.name, this.age, newHobbies);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ImmutablePerson that = (ImmutablePerson) o;
return age == that.age &&
Objects.equals(name, that.name) &&
Objects.equals(hobbies, that.hobbies);
}
@Override
public int hashCode() {
return Objects.hash(name, age, hobbies);
}
@Override
public String toString() {
return "ImmutablePerson{name='" + name + "', age=" + age + ", hobbies=" + hobbies + "}";
}
}
Dataclasses de Python para Inmutabilidad
from dataclasses import dataclass
from typing import List
import copy
@dataclass(frozen=True)
class ImmutablePerson:
name: str
age: int
hobbies: List[str] # Achtung: Liste ist trotzdem veränderlich!
def __post_init__(self):
# Validierung nach Initialisierung
if self.age < 0:
raise ValueError("Alter darf nicht negativ sein")
if not self.name:
raise ValueError("Name darf nicht leer sein")
# Defensive Copy für veränderliche Felder
object.__setattr__(self, 'hobbies', tuple(self.hobbies))
def with_age(self, new_age: int) -> 'ImmutablePerson':
"""Erstellt neue Instanz mit geändertem Alter"""
return ImmutablePerson(self.name, new_age, list(self.hobbies))
def add_hobby(self, hobby: str) -> 'ImmutablePerson':
"""Erstellt neue Instanz mit zusätzlichem Hobby"""
new_hobbies = list(self.hobbies) + [hobby]
return ImmutablePerson(self.name, self.age, new_hobbies)
Copias Defensivas
Protección contra mutaciones externas
public class ShoppingCart {
private final List<Item> items = new ArrayList<>();
private final Customer customer;
public ShoppingCart(Customer customer) {
this.customer = Objects.requireNonNull(customer);
}
// Defensive Copy bei Rückgabe
public List<Item> getItems() {
return new ArrayList<>(items); // Kopie zurückgeben
}
// Defensive Copy bei Parameter
public void addItems(List<Item> newItems) {
if (newItems != null) {
this.items.addAll(new ArrayList<>(newItems)); // Kopie speichern
}
}
// Unveränderliche Sicht
public List<Item> getItemsUnmodifiable() {
return Collections.unmodifiableList(items);
}
// Stream API für sicheren Zugriff
public Stream<Item> itemStream() {
return items.stream();
}
}
Copias defensivas en Python
class ShoppingCart:
def __init__(self, customer):
self._customer = customer
self._items = []
def get_items(self):
"""Defensive copy zurückgeben"""
return self._items.copy()
def add_items(self, items):
"""Defensive copy speichern"""
if items:
self._items.extend(items.copy())
def get_items_immutable(self):
"""Unveränderliche Sicht zurückgeben"""
return tuple(self._items)
def items_iterator(self):
"""Iterator für sicheren Zugriff"""
return iter(self._items)
Law of Demeter
Evitar llamadas en cascada
// Schlecht - Verletzt Law of Demeter
public void processOrder(Order order) {
// Zu viele Punkte - enge Kopplung
String city = order.getCustomer().getAddress().getCity();
double tax = order.getCustomer().getAddress().getTaxRate();
// ...
}
// Gut - Entkoppelt
public void processOrder(Order order) {
String city = order.getCustomerCity();
double tax = order.getCustomerTaxRate();
// ...
}
// Bessere Implementierung
public class Order {
private Customer customer;
public String getCustomerCity() {
return customer.getAddress().getCity();
}
public double getCustomerTaxRate() {
return customer.getAddress().getTaxRate();
}
}
Validación e Invariantes
Estrategia robusta de validación
public class EmailAddress {
private final String value;
private static final Pattern EMAIL_PATTERN =
Pattern.compile("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$");
public EmailAddress(String email) {
String normalized = normalize(email);
validate(normalized);
this.value = normalized;
}
private String normalize(String email) {
if (email == null) {
throw new IllegalArgumentException("Email darf nicht null sein");
}
return email.trim().toLowerCase();
}
private void validate(String email) {
if (email.isEmpty()) {
throw new IllegalArgumentException("Email darf nicht leer sein");
}
if (!EMAIL_PATTERN.matcher(email).matches()) {
throw new IllegalArgumentException("Ungültiges Email-Format");
}
if (email.length() > 254) {
throw new IllegalArgumentException("Email zu lang");
}
}
public String getValue() {
return value;
}
@Override
public String toString() {
return value;
}
}
Design by Contract
Precondiciones y postcondiciones
public class BankTransfer {
private final BankAccount fromAccount;
private final BankAccount toAccount;
public BankTransfer(BankAccount fromAccount, BankAccount toAccount) {
this.fromAccount = Objects.requireNonNull(fromAccount);
this.toAccount = Objects.requireNonNull(toAccount);
}
/**
* Überweist einen Betrag von einem Konto zum anderen
*
* @param amount Überweisungsbetrag in Cents
* @throws IllegalArgumentException wenn amount <= 0
* @throws IllegalStateException wenn Quellkonto nicht ausreichend gedeckt ist
* @throws IllegalStateException wenn eines der Konten deaktiviert ist
* @post fromAccount.getBalance() == old(fromAccount.getBalance()) - amount
* @post toAccount.getBalance() == old(toAccount.getBalance()) + amount
*/
public void transfer(int amount) {
// Vorbedingungen prüfen
if (amount <= 0) {
throw new IllegalArgumentException("Betrag muss positiv sein");
}
if (!fromAccount.isActive() || !toAccount.isActive()) {
throw new IllegalStateException("Beide Konten müssen aktiv sein");
}
if (fromAccount.getBalanceInCents() < amount) {
throw new IllegalStateException("Nicht genug Guthaben auf Quellkonto");
}
// Alte Zustände für Nachbedingungen speichern
int oldFromBalance = fromAccount.getBalanceInCents();
int oldToBalance = toAccount.getBalanceInCents();
try {
// Atomare Operation
fromAccount.withdraw(amount);
toAccount.deposit(amount);
// Nachbedingungen verifizieren
assert fromAccount.getBalanceInCents() == oldFromBalance - amount;
assert toAccount.getBalanceInCents() == oldToBalance + amount;
} catch (Exception e) {
// Rollback bei Fehlern
throw new RuntimeException("Überweisung fehlgeschlagen", e);
}
}
}
Encapsulación en la práctica
Ejemplo: Sistema de pedidos en e-commerce
public class Order {
private final String orderId;
private final Customer customer;
private final List<OrderItem> items;
private OrderStatus status;
private final LocalDateTime createdAt;
private LocalDateTime shippedAt;
// Private Konstruktor - Factory Method verwenden
private Order(String orderId, Customer customer) {
this.orderId = Objects.requireNonNull(orderId);
this.customer = Objects.requireNonNull(customer);
this.items = new ArrayList<>();
this.status = OrderStatus.PENDING;
this.createdAt = LocalDateTime.now();
}
// Factory Method für Objekterstellung
public static Order create(Customer customer) {
if (customer == null || !customer.isActive()) {
throw new IllegalArgumentException("Ungültiger Kunde");
}
String orderId = generateOrderId();
return new Order(orderId, customer);
}
// Business Methode mit Statusübergängen
public void addItem(Product product, int quantity) {
if (product == null) {
throw new IllegalArgumentException("Produkt darf nicht null sein");
}
if (quantity <= 0) {
throw new IllegalArgumentException("Menge muss positiv sein");
}
if (status != OrderStatus.PENDING) {
throw new IllegalStateException("Bestellung kann nicht mehr geändert werden");
}
OrderItem item = new OrderItem(product, quantity);
items.add(item);
}
// Zustandsübergang mit Validierung
public void ship() {
if (status != OrderStatus.CONFIRMED) {
throw new IllegalStateException("Bestellung muss bestätigt sein");
}
if (items.isEmpty()) {
throw new IllegalStateException("Bestellung ist leer");
}
this.status = OrderStatus.SHIPPED;
this.shippedAt = LocalDateTime.now();
// Event auslösen
EventPublisher.publish(new OrderShippedEvent(orderId));
}
// Sichere Getter mit defensiven Kopien
public List<OrderItem> getItems() {
return new ArrayList<>(items);
}
public Customer getCustomer() {
return customer; // Unveränderlich, keine Kopie nötig
}
// Berechnete Eigenschaft
public BigDecimal getTotalAmount() {
return items.stream()
.map(OrderItem::getTotalPrice)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
// Status-Getter - unveränderlich
public OrderStatus getStatus() {
return status;
}
// Private Hilfsmethode
private static String generateOrderId() {
return "ORD-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
}
}
Ventajas de la encapsulación
1. Menor acoplamiento
- Los componentes son menos dependientes entre sí
- Los cambios tienen un impacto reducido
- El refactoring es más simple
2. Mayor cohesión
- La funcionalidad relacionada está agrupada
- Responsabilidades claras
- Mejor comprensibilidad
3. Seguridad mejorada
- Acceso controlado a los datos
- Validación en los límites
- Protección contra estados inconsistentes
4. Mejor testabilidad
- Interfaces claras para Unit Tests
- Objetos mock más simples
- Enfoque en el comportamiento en lugar de la implementación
Desventajas y desafíos
1. Esfuerzo adicional
- Más código para getters y setters
- Boilerplate en clases de datos simples
- Mayor costo de implementación
2. Posible sobre-encapsulación
- Demasiados métodos pequeños
- Capas de abstracción innecesarias
- Complejidad sin beneficio
3. Curva de aprendizaje
- Se requiere comprensión de buena encapsulación
- Equilibrio entre apertura y cierre
- Experiencia para la granularidad correcta
Preguntas relevantes para exámenes
Preguntas típicas de la Cámara
-
¿Cuál es la diferencia entre encapsulación y abstracción?
- La encapsulación oculta detalles de implementación, la abstracción reduce la complejidad
-
¿Cuándo tienen sentido los getters y setters?
- Solo cuando hay una necesidad funcional, no para cada valor privado
-
¿Por qué son problemáticos los campos públicos?
- Eluden la validación e infringen invariantes
-
¿Cómo respalda la inmutabilidad la encapsulación?
- Garantiza invariantes estables y simplifica el uso concurrente
-
¿Qué establece la Ley de Demeter?
- Habla solo con amigos directos, evita llamadas en cascada
Resumen
La encapsulación es un principio fundamental para software robusto y mantenible. Protege estados internos, define interfaces claras y permite refactorings seguros. Una buena encapsulación requiere:
- Planificación cuidadosa de visibilidad
- Validación en los límites
- Programación defensiva
- Inmutabilidad intencional
- Responsabilidades claras
El equilibrio entre encapsulación suficiente y usabilidad práctica es decisivo para una arquitectura de software exitosa.
Continuando en la ruta de aprendizaje de OOP
Todos los artículos de OOP están completos. Vuelve al primer artículo: Fundamentos de Programación Orientada a Objetos OOP.

