Vererbung und Zusammensetzung (Is-a vs Has-a-Beziehung) in Java

1. Übersicht

Vererbung und Komposition - zusammen mit Abstraktion, Kapselung und Polymorphismus - sind Eckpfeiler der objektorientierten Programmierung (OOP).

In diesem Tutorial werden wir die Grundlagen der Vererbung und Komposition behandeln und uns stark darauf konzentrieren, die Unterschiede zwischen den beiden Arten von Beziehungen zu erkennen.

2. Grundlagen der Vererbung

Vererbung ist ein mächtiger, aber überstrapazierter und missbrauchter Mechanismus.

Einfach ausgedrückt, mit Vererbung definiert eine Basisklasse (auch als Basistyp bezeichnet) den Status und das Verhalten eines bestimmten Typs und lässt die Unterklassen (auch als Subtypen bezeichnet) spezielle Versionen dieses Status und Verhaltens bereitstellen.

Um eine klare Vorstellung davon zu haben, wie mit Vererbung gearbeitet wird, erstellen wir ein naives Beispiel: eine Basisklasse Person , die die allgemeinen Felder und Methoden für eine Person definiert, während die Unterklassen Waitress und Actress zusätzliche, feinkörnige Methodenimplementierungen bereitstellen.

Hier ist die Personenklasse :

public class Person { private final String name; // other fields, standard constructors, getters }

Und das sind die Unterklassen:

public class Waitress extends Person { public String serveStarter(String starter) { return "Serving a " + starter; } // additional methods/constructors } 
public class Actress extends Person { public String readScript(String movie) { return "Reading the script of " + movie; } // additional methods/constructors }

Lassen Sie uns außerdem einen Komponententest erstellen, um zu überprüfen, ob Instanzen der Klassen Waitress und Actress auch Instanzen von Person sind. Dies zeigt, dass die Bedingung "is-a" auf Typebene erfüllt ist:

@Test public void givenWaitressInstance_whenCheckedType_thenIsInstanceOfPerson() { assertThat(new Waitress("Mary", "[email protected]", 22)) .isInstanceOf(Person.class); } @Test public void givenActressInstance_whenCheckedType_thenIsInstanceOfPerson() { assertThat(new Actress("Susan", "[email protected]", 30)) .isInstanceOf(Person.class); }

Es ist wichtig, hier die semantische Facette der Vererbung hervorzuheben . Abgesehen von der Wiederverwendung der Implementierung der Person-Klasse haben wir eine genau definierte "is-a" -Beziehung zwischen dem Basistyp Person und den Subtypen Waitress und Actress erstellt . Kellnerinnen und Schauspielerinnen sind praktisch Personen.

Dies kann dazu führen, dass wir uns fragen: In welchen Anwendungsfällen ist Vererbung der richtige Ansatz?

Wenn Subtypen die Bedingung „is-a“ erfüllen und hauptsächlich additive Funktionen weiter unten in der Klassenhierarchie bereitstellen, ist die Vererbung der richtige Weg.

Das Überschreiben von Methoden ist natürlich zulässig, solange die überschriebenen Methoden die durch das Liskov-Substitutionsprinzip geförderte Substituierbarkeit des Basistyps / Subtyps beibehalten.

Darüber hinaus sollten wir berücksichtigen, dass die Subtypen die API des Basistyps erben. Dies kann in einigen Fällen übertrieben oder nur unerwünscht sein.

Andernfalls sollten wir stattdessen Komposition verwenden.

3. Vererbung in Entwurfsmustern

Während der Konsens darin besteht, dass wir die Zusammensetzung nach Möglichkeit der Vererbung vorziehen sollten, gibt es einige typische Anwendungsfälle, in denen die Vererbung ihren Platz hat.

3.1. Das Layer-Supertyp-Muster

In diesem Fall verwenden wir die Vererbung, um allgemeinen Code pro Schicht in eine Basisklasse (den Supertyp) zu verschieben .

Hier ist eine grundlegende Implementierung dieses Musters in der Domänenschicht:

public class Entity { protected long id; // setters } 
public class User extends Entity { // additional fields and methods } 

Wir können den gleichen Ansatz auf die anderen Ebenen im System anwenden, z. B. die Service- und die Persistenzschicht.

3.2. Das Muster der Vorlagenmethode

Im Muster der Vorlagenmethode können wir eine Basisklasse verwenden, um die invarianten Teile eines Algorithmus zu definieren und dann die Variantenteile in den Unterklassen zu implementieren :

public abstract class ComputerBuilder { public final Computer buildComputer() { addProcessor(); addMemory(); } public abstract void addProcessor(); public abstract void addMemory(); } 
public class StandardComputerBuilder extends ComputerBuilder { @Override public void addProcessor() { // method implementation } @Override public void addMemory() { // method implementation } }

4. Grundlagen der Komposition

Die Zusammensetzung ist ein weiterer von OOP bereitgestellter Mechanismus zur Wiederverwendung der Implementierung.

Kurz gesagt, die Komposition ermöglicht es uns, Objekte zu modellieren, die aus anderen Objekten bestehen , und so eine Beziehung zwischen ihnen zu definieren.

Darüber hinaus ist die Komposition die stärkste Form der Assoziation , was bedeutet, dass die Objekte , die ein Objekt bilden oder in einem Objekt enthalten sind, ebenfalls zerstört werden, wenn dieses Objekt zerstört wird .

Nehmen wir an, wir müssen mit Objekten arbeiten, die Computer darstellen, um besser zu verstehen, wie Komposition funktioniert .

Ein Computer besteht aus verschiedenen Teilen, einschließlich des Mikroprozessors, des Speichers, einer Soundkarte usw., sodass wir sowohl den Computer als auch jedes seiner Teile als einzelne Klassen modellieren können.

So könnte eine einfache Implementierung der Computerklasse aussehen:

public class Computer { private Processor processor; private Memory memory; private SoundCard soundCard; // standard getters/setters/constructors public Optional getSoundCard() { return Optional.ofNullable(soundCard); } }

Die folgenden Klassen modellieren einen Mikroprozessor, den Speicher und eine Soundkarte (Schnittstellen sind der Kürze halber weggelassen):

public class StandardProcessor implements Processor { private String model; // standard getters/setters }
public class StandardMemory implements Memory { private String brand; private String size; // standard constructors, getters, toString } 
public class StandardSoundCard implements SoundCard { private String brand; // standard constructors, getters, toString } 

Es ist leicht zu verstehen, welche Gründe es gibt, Komposition über Vererbung zu schieben. In jedem Szenario, in dem es möglich ist, eine semantisch korrekte Beziehung zwischen einer bestimmten Klasse und anderen herzustellen, ist die Komposition die richtige Wahl.

In the above example, Computer meets the “has-a” condition with the classes that model its parts.

It's also worth noting that in this case, the containing Computer object has ownership of the contained objects if and only if the objects can't be reused within another Computer object. If they can, we'd be using aggregation, rather than composition, where ownership isn't implied.

5. Composition Without Abstraction

Alternatively, we could've defined the composition relationship by hard-coding the dependencies of the Computer class, instead of declaring them in the constructor:

public class Computer { private StandardProcessor processor = new StandardProcessor("Intel I3"); private StandardMemory memory = new StandardMemory("Kingston", "1TB"); // additional fields / methods }

Dies wäre natürlich ein starres, eng gekoppeltes Design, da wir den Computer stark von bestimmten Implementierungen von Prozessor und Speicher abhängig machen würden .

Wir würden die Abstraktionsebene von Schnittstellen und Abhängigkeitsinjektion nicht nutzen.

Mit dem anfänglichen Design, das auf Schnittstellen basiert, erhalten wir ein lose gekoppeltes Design, das auch einfacher zu testen ist.

6. Fazit

In diesem Artikel haben wir die Grundlagen der Vererbung und Zusammensetzung in Java kennengelernt und die Unterschiede zwischen den beiden Arten von Beziehungen („is-a“ vs. „has-a“) eingehend untersucht.

Wie immer sind alle in diesem Tutorial gezeigten Codebeispiele auf GitHub verfügbar.