Skip to content
IRC-Coding IRC-Coding
OOP Kapselung Encapsulation Information Hiding Sichtbarkeitsmodifizierer Getter Setter Immutabilität

OOP Kapselung Grundlagen: Information Hiding & Sichtbarkeit

OOP Kapselung mit Information Hiding, Sichtbarkeitsmodifizierer, Getter/Setter und Immutabilität. Java Beispiele für Encapsulation.

S

schutzgeist

1 min read

OOP Kapselung Grundlagen: Information Hiding & Sichtbarkeit

Kapselung (Encapsulation) ist eines der vier fundamentalen Prinzipien der objektorientierten Programmierung. Sie verbirgt interne Zustände und Implementierungsdetails eines Objekts und stellt nur wohldefinierte Schnittstellen nach außen zur Verfügung.

Was ist Kapselung?

Kapselung bündelt Daten und Verhalten zu einer Einheit und schützt die interne Repräsentation vor unbefugtem Zugriff. Ziel ist robuste, wartbare und sichere Software durch klare Zuständigkeitsgrenzen.

Kernprinzipien der Kapselung

  • Information Hiding: Interne Details werden verborgen
  • Schnittstellenkontrolle: Nur definierte Zugriffe sind möglich
  • Invariantenwahrung: Objektzustand bleibt konsistent
  • Kopplungsreduktion: Geringere Abhängigkeiten zwischen Komponenten

Sichtbarkeitsmodifizierer

Java Sichtbarkeitsstufen

public class VisibilityDemo {
    
    // public: von überall zugreifbar
    public String publicField = "öffentlich";
    
    // protected: innerhalb der Klasse und Unterklassen
    protected String protectedField = "geschützt";
    
    // package-private: nur innerhalb des Pakets
    String packageField = "paket-privat";
    
    // private: nur innerhalb der Klasse
    private String privateField = "privat";
    
    // Private Methode - interne Logik
    private void validateInput(String input) {
        if (input == null || input.trim().isEmpty()) {
            throw new IllegalArgumentException("Input darf nicht leer sein");
        }
    }
    
    // Öffentliche Methode mit Validierung
    public void processData(String data) {
        validateInput(data); // Private Methode nutzen
        // Verarbeitung...
    }
}

C# Zugriffsmodifizierer

public class BankAccount
{
    // public: von überall zugreifbar
    public string AccountNumber { get; }
    
    // private: nur innerhalb der Klasse
    private decimal balance;
    
    // protected: innerhalb der Klasse und abgeleiteten Klassen
    protected string AccountType { get; set; }
    
    // internal: nur innerhalb der Assembly
    internal string BankCode { get; set; }
    
    // protected internal: innerhalb Assembly oder abgeleitete Klassen
    protected internal string BranchCode { get; set; }
    
    // Public Property mit privatem Setter
    public decimal Balance 
    { 
        get { return balance; }
        private set { balance = value; }
    }
}

Python Access Control

class BankAccount:
    def __init__(self, account_number: str):
        # Public attribute
        self.account_number = account_number
        
        # Protected attribute (Konvention)
        self._balance = 0.0
        
        # Private attribute (Name Mangling)
        self.__transaction_history = []
    
    def deposit(self, amount: float):
        """Public method with validation"""
        if amount <= 0:
            raise ValueError("Amount must be positive")
        self._balance += amount
        self.__add_transaction("deposit", amount)
    
    def _validate_amount(self, amount: float):
        """Protected method for subclasses"""
        return amount > 0
    
    def __add_transaction(self, transaction_type: str, amount: float):
        """Private method - internal use only"""
        self.__transaction_history.append({
            'type': transaction_type,
            'amount': amount,
            'timestamp': datetime.now()
        })

Getter und Setter

Sinnvolle Getter/Setter Implementierung

public class BankAccount {
    private String iban;
    private int balanceInCents;
    private boolean isActive = true;
    
    // Konstruktor mit Validierung
    public BankAccount(String iban) {
        if (iban == null || !isValidIban(iban)) {
            throw new IllegalArgumentException("Ungültige IBAN");
        }
        this.iban = iban;
        this.balanceInCents = 0;
    }
    
    // Getter für lesenden Zugriff
    public int getBalanceInCents() {
        return balanceInCents;
    }
    
    // Getter mit Formatierung
    public String getFormattedBalance() {
        return String.format("€%.2f", balanceInCents / 100.0);
    }
    
    // Setter mit Validierung und Business Logik
    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;
    }
    
    // Business Methode statt einfachem Setter
    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; // Nicht genug Guthaben
        }
        this.balanceInCents -= cents;
        return true;
    }
    
    // Private Validierungsmethode
    private boolean isValidIban(String iban) {
        // IBAN Validierungslogik
        return iban != null && iban.matches("[A-Z]{2}[0-9]{20}");
    }
}

Python Properties

class BankAccount:
    def __init__(self, iban: str):
        self._iban = iban
        self._balance = 0.0
    
    @property
    def balance(self) -> float:
        """Getter für den Kontostand"""
        return self._balance
    
    @property
    def iban(self) -> str:
        """Read-only property für IBAN"""
        return self._iban
    
    @balance.setter
    def balance(self, value: float):
        """Setter mit Validierung"""
        if value < 0:
            raise ValueError("Kontostand darf nicht negativ sein")
        self._balance = value
    
    @property
    def formatted_balance(self) -> str:
        """Computed property"""
        return f"€{self._balance:.2f}"
    
    def deposit(self, amount: float):
        """Business Methode statt direktem Setter"""
        if amount <= 0:
            raise ValueError("Betrag muss positiv sein")
        self._balance += amount

Immutabilität

Unveränderliche Objekte in Java

// Immutable Klasse mit final Feldern
public final class ImmutablePerson {
    private final String name;
    private final int age;
    private final List<String> hobbies; // Defensive Copy!
    
    public ImmutablePerson(String name, int age, List<String> hobbies) {
        this.name = Objects.requireNonNull(name, "Name darf nicht null sein");
        this.age = age;
        // Defensive Copy für veränderliche Parameter
        this.hobbies = List.copyOf(Objects.requireNonNull(hobbies));
    }
    
    // Getter - keine Setter!
    public String getName() {
        return name;
    }
    
    public int getAge() {
        return age;
    }
    
    // Defensive Copy für veränderliche Rückgaben
    public List<String> getHobbies() {
        return new ArrayList<>(hobbies);
    }
    
    // Methoden erstellen neue Instanzen
    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 + "}";
    }
}

Python Dataclasses für Immutabilität

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)

Defensive Kopien

Schutz vor externer Mutation

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();
    }
}

Python Defensive Copies

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

Vermeidung von Kaskadenaufrufen

// 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();
    }
}

Validierung und Invarianten

Robuste Validierungsstrategie

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

Vor- und Nachbedingungen

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);
        }
    }
}

Kapselung in der Praxis

Beispiel: E-Commerce Bestellsystem

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();
    }
}

Vorteile von Kapselung

1. Geringere Kopplung

  • Komponenten sind weniger voneinander abhängig
  • Änderungen haben weniger Auswirkungen
  • Einfacheres Refactoring möglich

2. Höhere Kohäsion

  • Zusammengehörige Funktionalität ist gebündelt
  • Klare Verantwortlichkeiten
  • Bessere Verständlichkeit

3. Verbesserte Sicherheit

  • Kontrollierter Zugriff auf Daten
  • Validierung an den Grenzen
  • Schutz vor inkonsistenten Zuständen

4. Bessere Testbarkeit

  • Klare Schnittstellen für Unit Tests
  • Einfachere Mock-Objekte möglich
  • Fokus auf Verhalten statt Implementierung

Nachteile und Herausforderungen

1. Zusätzlicher Aufwand

  • Mehr Code für Getter/Setter
  • Boilerplate bei einfachen Datenklassen
  • Höherer Implementierungsaufwand

2. Mögliche Überkapselung

  • Zu viele kleine Methoden
  • Unnötige Abstraktionsschichten
  • Komplexität ohne Nutzen

3. Lernkurve

  • Verständnis für gute Kapselung nötig
  • Balance zwischen Offenheit und Geschlossenheit
  • Erfahrung für richtige Granularität

Prüfungsrelevante Fragen

Typische IHK Fragen

  1. Was ist der Unterschied zwischen Kapselung und Abstraktion?

    • Kapselung verbirgt Implementierungsdetails, Abstraktion reduziert Komplexität
  2. Wann sind Getter und Setter sinnvoll?

    • Nur bei fachlicher Notwendigkeit, nicht für jeden privaten Wert
  3. Warum sind öffentliche Felder problematisch?

    • Sie umgehen Validierung und verletzen Invarianten
  4. Wie unterstützt Immutabilität die Kapselung?

    • Garantiert stabile Invarianten und vereinfacht nebenläufige Nutzung
  5. Was besagt das Law of Demeter?

    • Sprich nur mit direkten Freunden, vermeide Kaskadenaufrufe

Zusammenfassung

Kapselung ist ein fundamentales Prinzip für robuste und wartbare Software. Sie schützt interne Zustände, definiert klare Schnittstellen und ermöglicht sichere Refactorings. Gute Kapselung erfordert:

  • Sorgfältige Sichtbarkeitsplanung
  • Validierung an den Grenzen
  • Defensive Programmierung
  • Gewollte Immutabilität
  • Klare Verantwortlichkeiten

Die Balance zwischen ausreichender Kapselung und praktischer Nutzbarkeit ist entscheidend für erfolgreiche Softwarearchitektur.

Zurück zum Blog
Share:

Ähnliche Beiträge