Overerving en samenstelling (Is-a versus Has-a-relatie) in Java

1. Overzicht

Overerving en compositie - samen met abstractie, inkapseling en polymorfisme - zijn hoekstenen van objectgeoriënteerd programmeren (OOP).

In deze tutorial behandelen we de basisprincipes van overerving en compositie, en we zullen ons sterk concentreren op het ontdekken van de verschillen tussen de twee soorten relaties.

2. Basisprincipes van overerving

Overerving is een krachtig maar toch te veel gebruikt en misbruikt mechanisme.

Simpel gezegd, met overerving, definieert een basisklasse (ook bekend als basistype) de toestand en het gedrag dat gebruikelijk is voor een bepaald type en laat de subklassen (ook bekend als subtypen) gespecialiseerde versies van die toestand en dat gedrag leveren.

Om een ​​duidelijk idee te hebben over het werken met overerving, laten we een naïef voorbeeld maken: een basisklasse Persoon die de gemeenschappelijke velden en methoden voor een persoon definieert, terwijl de subklassen Serveerster en Actrice bieden aanvullende, fijnmazige methode-implementaties.

Hier is de Persoon klasse:

openbare klasse Persoon {privé finale Tekenreeksnaam; // andere velden, standaard constructors, getters}

En dit zijn de subklassen:

public class Serveerster verlengt Persoon {public String serveStarter (String starter) {return "Serving a" + starter; } // aanvullende methoden / constructors} 
public class Actrice verlengt Persoon {public String readScript (String movie) {return "Reading the script of" + movie; } // aanvullende methoden / constructors}

Laten we daarnaast een eenheidstest maken om te verifiëren dat instanties van het Serveerster en Actrice klassen zijn ook voorbeelden van Persoon, waarmee wordt aangetoond dat aan de "is-a" -voorwaarde is voldaan op het typeniveau:

@Test openbare ongeldig gegevenWaitressInstance_whenCheckedType_thenIsInstanceOfPerson () {assertThat (nieuwe serveerster ("Mary", "[email protected]", 22)) .isInstanceOf (Person.class); } @Test openbare ongeldig gegevenActressInstance_whenCheckedType_thenIsInstanceOfPerson () {assertThat (nieuwe actrice ("Susan", "[email protected]", 30)) .isInstanceOf (Person.class); }

Het is belangrijk om hier het semantische facet van overerving te benadrukken. Afgezien van het hergebruiken van de implementatie van het Persoon klasse, we hebben een goed gedefinieerde "is-a" -relatie gecreëerd tussen het basistype Persoon en de subtypen Serveerster en Actrice. Serveersters en actrices zijn in feite personen.

Dit kan ertoe leiden dat we vragen: in welke use-cases is overerving de juiste aanpak?

Als subtypen voldoen aan de 'is-a'-voorwaarde en voornamelijk aanvullende functionaliteit bieden verderop in de klassenhiërarchie,dan is erfenis de juiste keuze.

Natuurlijk is overschrijven van methoden toegestaan ​​zolang de overschreven methoden de substitueerbaarheid van het basistype / subtype behouden die wordt gepromoot door het Liskov-substitutieprincipe.

Bovendien moeten we daar rekening mee houden de subtypen erven de API van het basistype, wat in sommige gevallen overdreven of alleen maar ongewenst kan zijn.

Anders zouden we in plaats daarvan compositie moeten gebruiken.

3. Overerving in ontwerppatronen

Hoewel de consensus is dat we waar mogelijk de voorkeur moeten geven aan compositie boven overerving, zijn er een paar typische gebruikssituaties waarin overerving zijn plaats heeft.

3.1. Het Layer Supertype Pattern

In dit geval zijn we gebruik overerving om gemeenschappelijke code naar een basisklasse (het supertype) te verplaatsen, per laag.

Hier is een basisimplementatie van dit patroon in de domeinlaag:

public class Entity {beschermde lange id; // setters} 
openbare klasse Gebruiker breidt Entiteit uit {// aanvullende velden en methoden} 

We kunnen dezelfde aanpak toepassen op de andere lagen in het systeem, zoals de service- en persistentielagen.

3.2. Het patroon van de sjabloonmethode

In het patroon van de sjabloonmethode kunnen we gebruik een basisklasse om de onveranderlijke delen van een algoritme te definiëren en implementeer vervolgens de variantonderdelen in de subklassen:

openbare abstracte klasse ComputerBuilder {openbare definitieve Computer buildComputer () {addProcessor (); addMemory (); } public abstract void addProcessor (); openbare abstracte leegte addMemory (); } 
public class StandardComputerBuilder breidt ComputerBuilder uit {@Override public void addProcessor () {// methode implementatie} @Override public void addMemory () {// methode implementatie}}

4. De basisprincipes van de compositie

De samenstelling is een ander mechanisme dat door OOP wordt geboden voor het hergebruiken van de implementatie.

In een notendop, compositie stelt ons in staat objecten te modelleren die uit andere objecten bestaan, waarmee ze een 'heeft-een'-relatie tussen hen definiëren.

Verder de compositie is de sterkste vorm van associatie, wat betekent dat het (de) object (en) die samen een object vormen of zich daarin bevinden, worden ook vernietigd wanneer dat object wordt vernietigd.

Laten we aannemen dat we, om beter te begrijpen hoe compositie werkt, moeten werken met objecten die computers vertegenwoordigen.

Een computer is samengesteld uit verschillende onderdelen, waaronder de microprocessor, het geheugen, een geluidskaart enzovoort, zodat we zowel de computer als elk van zijn onderdelen als individuele klassen kunnen modelleren.

Hier is hoe een eenvoudige implementatie van het Computer klasse zou eruit kunnen zien:

openbare klasse Computer {privé Processor processor; privé geheugengeheugen; privé SoundCard-geluidskaart; // standard getters / setters / constructors public Optional getSoundCard () {return Optional.ofNullable (soundCard); }}

De volgende klassen modelleren een microprocessor, het geheugen en een geluidskaart (interfaces zijn kortheidshalve weggelaten):

openbare klasse StandardProcessor implementeert Processor {privé String-model; // standaard getters / setters}
public class StandardMemory implementeert Memory {private String brand; private String-grootte; // standard constructors, getters, toString} 
openbare klasse StandardSoundCard implementeert SoundCard {private String-merk; // standard constructors, getters, toString} 

Het is gemakkelijk om de motivaties te begrijpen om compositie boven overerving te stellen. In elk scenario waarin het mogelijk is om een ​​semantisch correcte "heeft-een" -relatie tot stand te brengen tussen een bepaalde klasse en andere, is de compositie de juiste keuze om te maken.

In het bovenstaande voorbeeld Computer voldoet aan de "has-a" -voorwaarde met de klassen die de onderdelen modelleren.

Het is ook vermeldenswaard dat in dit geval de met Computer object heeft het eigendom van de ingesloten objecten als en alleen als de objecten kunnen niet binnen een ander worden hergebruikt Computer voorwerp. Als ze kunnen, zouden we aggregatie gebruiken in plaats van compositie, waarbij eigendom niet geïmpliceerd is.

5. Compositie zonder abstractie

Als alternatief hadden we de samenstellingsrelatie kunnen definiëren door de afhankelijkheden van het Computer class, in plaats van ze in de constructor te declareren:

openbare klasse Computer {privé StandardProcessor-processor = nieuwe StandardProcessor ("Intel I3"); privé StandardMemory-geheugen = nieuwe StandardMemory ("Kingston", "1TB"); // aanvullende velden / methoden}

Dit zou natuurlijk een rigide, nauw gekoppeld ontwerp zijn, zoals we zouden maken Computer sterk afhankelijk van specifieke implementaties van Verwerker en Geheugen.

We zouden geen gebruik maken van het abstractieniveau dat wordt geboden door interfaces en afhankelijkheidsinjectie.

Met het eerste ontwerp op basis van interfaces, krijgen we een losjes gekoppeld ontwerp, dat ook gemakkelijker te testen is.

6. Conclusie

In dit artikel hebben we de grondbeginselen van overerving en compositie in Java geleerd, en hebben we de verschillen tussen de twee soorten relaties ("is-a" versus "has-a") grondig onderzocht.

Zoals altijd zijn alle codevoorbeelden die in deze tutorial worden getoond, beschikbaar op GitHub.