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
-
Was ist der Unterschied zwischen Kapselung und Abstraktion?
- Kapselung verbirgt Implementierungsdetails, Abstraktion reduziert Komplexität
-
Wann sind Getter und Setter sinnvoll?
- Nur bei fachlicher Notwendigkeit, nicht für jeden privaten Wert
-
Warum sind öffentliche Felder problematisch?
- Sie umgehen Validierung und verletzen Invarianten
-
Wie unterstützt Immutabilität die Kapselung?
- Garantiert stabile Invarianten und vereinfacht nebenläufige Nutzung
-
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.