Objektorientierung ist ein Denkmodell, um Software in Verantwortlichkeiten zu schneiden: Daten (Zustand) und Verhalten (Logik) werden in Objekten gekapselt. Das Ziel ist nicht „mehr Klassen“, sondern verständliche Module, die sich erweitern, testen und sicher betreiben lassen – auch in großen Projekten (Desktop, Web, APIs, KI-Systeme).
Merksatz: Eine gute Klasse ist eine kleine „Mini‑Dienstleistung“: sie hat einen klaren Zweck, eine stabile Schnittstelle und verbirgt Details.
Eine Klasse ist der Bauplan: sie definiert Eigenschaften (Felder/Properties) und Methoden (Funktionen). Ein Objekt ist eine konkrete Instanz dieser Klasse zur Laufzeit – mit eigenen Zustandswerten.
| Begriff | Was ist das? | Wozu? | Typisches Beispiel |
|---|---|---|---|
| Klasse | Bauplan / Typdefinition | Wiederverwendbare Struktur & Verhalten beschreiben | class User { ... } |
| Objekt | Instanz zur Laufzeit | Konkrete Daten + Verhalten nutzen | var u = new User(...) |
// C# Kurzbeispiel public class User { public string Email { get; } public User(string email) => Email = email; } var user = new User("a@b.de"); // user ist ein Objekt (Instanz)
Kapselung schützt Invarianten: du verhinderst, dass fremder Code deinen Zustand beliebig kaputt macht. Statt „alles public“ gibst du nur die erlaubten Operationen frei.
// Invariante: Kontostand darf nicht negativ werden public class Account { public decimal Balance { get; private set; } public void Deposit(decimal amount) { if (amount <= 0) throw new ArgumentException("amount"); Balance += amount; } public void Withdraw(decimal amount) { if (amount <= 0) throw new ArgumentException("amount"); if (Balance - amount < 0) throw new InvalidOperationException("Insufficient funds"); Balance -= amount; } }
Abstraktion blendet Details aus, damit du auf einer höheren Ebene denken kannst: „Sende Nachricht“ statt „öffne Socket, serialisiere JSON, handle Retries …“. Gute Abstraktion ist klein, eindeutig und braucht keine Dokument‑Romanlänge.
Vererbung modelliert „ist‑ein“ Beziehungen – aber: in der Praxis ist sie schnell überstrapaziert. Verwende sie vorsichtig, besonders bei tiefen Hierarchien. Häufig ist Komposition (Zusammenbauen) stabiler als Vererbung.
Polymorphie bedeutet: du programmierst gegen eine Schnittstelle, nicht gegen eine konkrete Klasse. Dadurch kannst du Implementierungen austauschen (z.B. echte DB vs. Mock) ohne den Aufrufer zu ändern.
// Polymorphie via Interface public interface IClock { DateTime UtcNow { get; } } public sealed class SystemClock : IClock { public DateTime UtcNow => DateTime.UtcNow; } public sealed class FakeClock : IClock { public DateTime UtcNow { get; set; } } public sealed class TokenService { private readonly IClock _clock; public TokenService(IClock clock) => _clock = clock; public string IssueToken() => $"tok_{_clock.UtcNow:yyyyMMddHHmmss}"; }
SOLID ist kein Dogma, sondern eine „Stabilitäts‑Checkliste“: Sie hilft, dass Änderungen lokal bleiben und nicht das ganze System zerreißen.
| Prinzip | Worum geht’s? | Typische Frage im Review | Warnsignal |
|---|---|---|---|
| SRP | Eine Klasse hat genau eine Verantwortung | „Warum ändert sich diese Klasse?“ | Viele Gründe/Features in einer Klasse |
| OCP | Erweiterbar ohne bestehendes Verhalten umzuschreiben | „Kann ich neue Variante hinzufügen ohne überall if/else?“ | Wachsende Switch/If‑Ketten |
| LSP | Subtypen müssen überall wie Basistyp funktionieren | „Überrascht die Subklasse?“ | Subklasse wirft „NotSupported“ für geerbte Methoden |
| ISP | Kleine Interfaces statt „Monster‑Interfaces“ | „Muss ich Methoden implementieren, die ich nicht brauche?“ | Interfaces mit 15+ Methoden |
| DIP | Abhängigkeiten auf Abstraktionen, nicht auf Konkretes | „Kann ich austauschen/testen ohne Umbau?“ | Überall new SqlConnection() im Code |
DI trennt Erzeugung von Nutzung. Deine Business‑Logik bekommt ihre Abhängigkeiten von außen und ist dadurch leichter testbar und austauschbar.
Praxis-Regel: „new“ gehört in die Composition Root (Startup/Program), nicht quer im Code.
// C# Beispiel (Konzept, unabhängig vom konkreten DI-Container) public interface IEmailSender { Task SendAsync(string to, string subject, string body); } public sealed class OrderNotifier { private readonly IEmailSender _email; public OrderNotifier(IEmailSender email) => _email = email; public Task NotifyAsync(string email) => _email.SendAsync(email, "Bestellung", "Danke für deine Bestellung!"); }
OOP entfaltet ihren Vorteil erst mit sauberer Schichtung. Ein bewährtes Muster:
| Schicht | Darf abhängen von | Darf NICHT | Beispiel‑Klassen |
|---|---|---|---|
| Domain | Nur Domain | DB/HTTP/UI | Order, Money, DiscountPolicy |
| Application | Domain + Abstraktionen | Konkrete DB‑Treiber direkt | CreateOrderHandler, PricingService |
| Infrastructure | Application/Domain (Implementiert Ports) | Business‑Regeln „erfinden“ | SqlOrderRepository, HttpPaymentClient |
| UI | Application | Domain‑Invarianten umgehen | ViewModels, Views, Commands |
Diese Muster machen Projekte schnell teuer. Wenn du sie erkennst, kannst du früh gegensteuern:
Performanceprobleme entstehen meist durch I/O, ineffiziente Datenzugriffe oder schlechte Algorithmen – nicht durch „Klassen“. Dennoch helfen ein paar Regeln:
Wenn du eine komplexe Domäne modellierst, mehrere Implementierungen brauchst (z.B. verschiedene Speicher), und wenn Wartbarkeit/Erweiterbarkeit wichtiger sind als „schnellster Prototyp“.
Nicht grundsätzlich. Funktionale Techniken sind oft hervorragend für Datenpipelines und Nebenwirkungs‑Kontrolle. In der Praxis kombinieren Teams häufig beides: OOP für Domänenmodell & Module, funktionale Patterns für Transformationen.
Es gibt keine magische Zahl. Ziel ist: Verantwortlichkeiten klein halten und Änderungen lokal machen. Viele kleine, saubere Klassen sind oft besser als wenige riesige.
Klassen und OOP sind am stärksten, wenn sie Verantwortung klar schneiden: Kapselung schützt Invarianten, Polymorphie ermöglicht Austauschbarkeit, SOLID stabilisiert Evolution – und Architektur sorgt dafür, dass Änderungen nicht chaotisch durchs System diffundieren.