Komposition anstelle von Vererbung ist nicht immer ganz einfach
Zuerst einmal musst Du dir überlegen, warum Du die Vererbung brauchst.
Man kann sie nicht immer auflösen, z.B. wenn eine externe Funktion nur mit Hilfe einer Ableitung beeinflusst werden kann, dann ist das gut und auch richtig so.
Aber wenn Du die Basisklasse nur brauchst, um Funktionen der Basisklasse auch in der Ableitung zu nutzen oder weil Du faul bist, dann kann man das ganz gut aufteilen.
Es gibt aber auch komplexe Fälle, wo man ganz bewusst auf komplexe Vererbung setzt (z.B. UI-Frameworks), aber das hier einmal außenvor.
Der Grund, warum man auf Vererbung möglichst verzichten sollte:
Es ist die mit Abstand schlimmste Abhängigkeit, die Du dir in deinen Code prügeln kannst
Ich hatte den Fall schon, dass ich eine Vererbungs-Hierarchie wieder auflösen musste - ist kein Spaß, sag ich dir.
Wenn Du also eine Klasse hast, die Funktionen der Basis-Klasse brauchst, dann überlege dir, ob Du die Funktionen der Basisklasse auch auslagern kannst.
Z.B. beim Standard-Beispiel Auto: Ein Auto braucht Motor, Lenkung, Schaltung, viel Elektronik, etc. etc.
Nach dem Standard-Beispiel würde man hingehen und eine Basis-Klasse "Fahrzeug" mit einem dieser Funktionen schreiben und "Auto" leitet davon ab. Und dann hast Du ein Flugzeug - wie bringst Du das in deine Struktur ohne von den ganzen anderen Dingen, die in "Fahrzeug" definiert sind und die Du nicht brauchst, abhängig zu sein? Kurz: Gar nicht. Entweder Du akzeptierst das - was meistens eine dumme Idee ist - oder baust alles um - was meistens auch eine dumme Idee ist, aber zumindest etwas besser.
Oder Du greifst zur Alternative mit Hilfe von Komposition:
Viele verschiedene Klassen für jede Teil-Funktion, also "Motor", "Lenkung", "Schaltung", etc.
Die Klasse "Auto" hat dann keine Basis-Klasse mehr, aber erstellt Instanzen dieser Klassen für Teil-Funktionen und nutzt sie.
Auf diese Weise kannst dir ein Auto Teil für Teil im Code zusammen bauen und wenn Du ein Flugzeug hast, nimmst Du eben nur das, was Du brauchst und für den Rest entwickelst Du nach dem gleichen Konzept Klassen.
Folgendes ...
C#-Quelltext
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18:
| public abstract class Fahrzeug { public abstract void Fahre();
protected void StartMotor() { } } public class Auto : Fahrzeug { public override void Fahre() { StartMotor(); } } |
... wird zu ...
C#-Quelltext
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23:
| public class Motor { public void Start() { } } public class Auto { private readonly Motor _motor;
public Auto() { _motor = new Motor(); }
public void Fahre() { _motor.Start(); } } |
Und wenn Du die Vorteile der Vererbung (gemeinsamer Basis-Typ) nutzen willst, kannst Du auch ein Interface erfinden und das implementieren.
C#-Quelltext
1: 2: 3: 4:
| public interface IAuto { void Fahre(); } |
Und siehe da, Du hast die gleichen Vorteile, nur ohne die Nachteile
Es kann aber vorkommen, dass Du eine Funktion hast, die in sich zwar unabhängig ist (z.B. Motor), sich je Situation aber anders verhält (Auto-Motor, Flugzeug-Motor).
In dem Fall wäre Vererbung wieder OK, dann leitest Du in "AutoMotor" und "FlugzeugMotor" einfach von "Motor" ab und nutzt dann in "Auto" bzw. "Flugzeug" deine Ableitung.
Der nächste Schritt, wenn das einmal begriffen ist, wäre, wie Du auch noch diese Abhängigkeit (Auto->AutoMotor) auflösen kannst.
Das geht dann mit Hilfe von Interfaces und Dependency Injection, dann ist "Auto" nicht mehr direkt von "AutoMotor" abhängig, sondern nur noch indirekt von einem Interface "IMotor" und die Implementierung dazu (VerbrennerMotor oder Elektromotor) wird von außen über den Konstruktor sozusagen eingebaut.
Der Vorteil ist, dass Du die Auto-Klasse für viele verschiedene Dinge (Verbrenner-Auto, Elektro-Auto, Hybrid-Auto, Seifenkiste) nutzen kannst, ohne nur einmal den Code dafür anzupassen.
Aber wie gesagt: Eins nach dem Anderen