Skip to content
IRC-CodingIRC-Coding
OOPEncapsulationInformation HidingModificadores de visibilidadGettersSettersInmutabilidadJava

Encapsulation OOP: Information Hiding y Visibilidad

Aprende encapsulation en OOP con information hiding, modificadores de visibilidad, getters/setters e inmutabilidad. Ejemplos en Java.

S

schutzgeist

10 min read
Encapsulation OOP: Information Hiding y Visibilidad

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

  1. ¿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
  2. ¿Cuándo tienen sentido los getters y setters?

    • Solo cuando hay una necesidad funcional, no para cada valor privado
  3. ¿Por qué son problemáticos los campos públicos?

    • Eluden la validación e infringen invariantes
  4. ¿Cómo respalda la inmutabilidad la encapsulación?

    • Garantiza invariantes estables y simplifica el uso concurrente
  5. ¿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.

Volver al blog
Share:

Nächster Artikel in Programación Orientada a Objetos

Weiterlesen
Herencia OOP: Inheritance y Polimorfismo

Entradas relacionadas