Arv og sammensætning (Is-a-Has-a-forhold) i Java

1. Oversigt

Arv og komposition - sammen med abstraktion, indkapsling og polymorfisme - er hjørnestenene i objektorienteret programmering (OOP).

I denne vejledning dækker vi det grundlæggende i arv og komposition, og vi fokuserer stærkt på at spotte forskellene mellem de to typer relationer.

2. Arvets grundlæggende

Arv er en kraftfuld, men alligevel misbrugt og misbrugt mekanisme.

Kort sagt, med arv definerer en basisklasse (aka basistype) den tilstand og adfærd, der er fælles for en given type, og lader underklasserne (aka subtyper) levere specialversioner af denne tilstand og adfærd.

For at få en klar idé om, hvordan man arbejder med arv, lad os oprette et naivt eksempel: en basisklasse Person der definerer de fælles felter og metoder for en person, mens underklasserne Servitrice og Skuespillerinde give yderligere, finkornede metodeimplementeringer.

Her er den Person klasse:

offentlig klasse person {privat endelig strengnavn; // andre felter, standardkonstruktører, getters}

Og det er underklasserne:

offentlig klasse Servitrice udvider Person {public String serveStarter (String starter) {return "Serving a" + starter; } // yderligere metoder / konstruktører} 
offentlig klasse Skuespillerinde udvider Person {public String readScript (String movie) {return "Læser manuskriptet til" + film; } // yderligere metoder / konstruktører}

Lad os desuden oprette en enhedstest for at kontrollere, at forekomster af Servitrice og Skuespillerinde klasser er også forekomster af Personog viser således, at "is-a" -betingelsen er opfyldt på typeniveau:

@Test offentligt ugyldigt givetWaitressInstance_whenCheckedType_thenIsInstanceOfPerson () {assertThat (ny servitrice ("Mary", "[email protected]", 22)) .isInstanceOf (Person.class); } @Test offentlig ugyldighed givenActressInstance_whenCheckedType_thenIsInstanceOfPerson () {assertThat (ny skuespillerinde ("Susan", "[email protected]", 30)) .isInstanceOf (Person.class); }

Det er vigtigt at understrege den semantiske facet af arv her. Bortset fra at genbruge implementeringen af ​​programmet Person klasse, Vi har skabt et veldefineret ”is-a” forhold mellem basistypen Person og undertyperne Servitrice og Skuespillerinde. Servitricer og skuespillerinder er faktisk personer.

Dette kan få os til at spørge: i hvilke brugstilfælde er arv den rigtige tilgang at tage?

Hvis undertyper opfylder "is-a" -betingelsen og hovedsageligt giver additiv funktionalitet længere nede i klassens hierarki,så er arv vejen at gå.

Naturligvis er metodeoverstyring tilladt, så længe de tilsidesatte metoder bevarer basistypen / undertype-substituerbarheden, der fremmes af Liskov-substitutionsprincippet.

Derudover skal vi huske det undertyperne arver basistypens API, hvilket i nogle tilfælde kan være overdreven eller blot uønsket.

Ellers skal vi bruge komposition i stedet.

3. Arv i designmønstre

Mens konsensus er, at vi skal favorisere sammensætning frem for arv, når det er muligt, er der et par typiske brugssager, hvor arv har sin plads.

3.1. Layer Supertype Pattern

I dette tilfælde, vi brug arv til at flytte almindelig kode til en basisklasse (supertypen), pr. lag.

Her er en grundlæggende implementering af dette mønster i domænelaget:

public class Entity {beskyttet lang id; // settere} 
offentlig klasse bruger udvider enhed {// yderligere felter og metoder} 

Vi kan anvende den samme tilgang til de andre lag i systemet, såsom service- og persistenslagene.

3.2. Skabelonmetodemønsteret

I skabelonmetodemønsteret kan vi Brug en baseklasse til at definere de uforanderlige dele af en algoritme, og implementer derefter variantdelene i underklasserne:

offentlig abstrakt klasse ComputerBuilder {offentlig endelig Computer buildComputer () {addProcessor (); addMemory (); } offentlig abstrakt ugyldig addProcessor (); offentlig abstrakt ugyldigt addMemory (); } 
offentlig klasse StandardComputerBuilder udvider ComputerBuilder {@Override public void addProcessor () {// metodeimplementering} @Override public void addMemory () {// metodeimplementering}}

4. Sammensætningens grundlæggende

Sammensætningen er en anden mekanisme leveret af OOP til genbrug af implementering.

I en nøddeskal, komposition giver os mulighed for at modellere objekter, der består af andre objekterog definerer således et "has-a" forhold mellem dem.

Desuden, sammensætningen er den stærkeste form for tilknytning, hvilket betyder at objektet / objekterne, der komponerer eller er indeholdt i et objekt, ødelægges også, når objektet ødelægges.

For bedre at forstå hvordan komposition fungerer, lad os antage, at vi skal arbejde med objekter, der repræsenterer computere.

En computer er sammensat af forskellige dele, herunder mikroprocessoren, hukommelsen, et lydkort og så videre, så vi kan modellere både computeren og hver af dens dele som individuelle klasser.

Her er hvordan en simpel implementering af Computer klasse kan se ud:

offentlig klasse Computer {privat processor processor; privat hukommelse hukommelse; private SoundCard lydkort; // standard getters / setters / constructors public Optional getSoundCard () {return Optional.ofNullable (soundCard); }}

Følgende klasser modellerer en mikroprocessor, hukommelsen og et lydkort (grænseflader udelades for kortfattethedens skyld):

offentlig klasse StandardProcessor implementerer processor {privat strengmodel; // standard getters / setters}
offentlig klasse StandardMemory implementerer Memory {private String brand; privat streng størrelse; // standard konstruktører, getters, toString} 
offentlig klasse StandardSoundCard implementerer SoundCard {private String brand; // standard konstruktører, getters, toString} 

Det er let at forstå motivationen bag at skubbe komposition over arv. I ethvert scenarie, hvor det er muligt at etablere et semantisk korrekt “has-a” forhold mellem en given klasse og andre, er kompositionen det rigtige valg at tage.

I ovenstående eksempel Computer opfylder "has-a" -tilstanden med de klasser, der modellerer dens dele.

Det er også værd at bemærke, at i dette tilfælde den indeholdende Computer objekt har ejerskab af de indeholdte objekter hvis og kun hvis genstandene kan ikke genbruges i en anden Computer objekt. Hvis de kan, bruger vi aggregering snarere end sammensætning, hvor ejerskab ikke er underforstået.

5. Komposition uden abstraktion

Alternativt kunne vi have defineret sammensætningsforholdet ved at hårdkode afhængighederne af Computer klasse i stedet for at erklære dem i konstruktøren:

offentlig klasse Computer {privat StandardProcessor processor = ny StandardProcessor ("Intel I3"); privat StandardMemory-hukommelse = ny StandardMemory ("Kingston", "1TB"); // yderligere felter / metoder}

Selvfølgelig ville dette være et stift, tæt koblet design, som vi ville lave Computer stærkt afhængig af specifikke implementeringer af Processor og Hukommelse.

Vi ville ikke udnytte det abstraktionsniveau, der leveres af grænseflader og afhængighedsinjektion.

Med det oprindelige design baseret på grænseflader får vi et løst koblet design, som også er lettere at teste.

6. Konklusion

I denne artikel lærte vi det grundlæggende ved arv og komposition i Java, og vi undersøgte dybtgående forskellene mellem de to typer relationer (“is-a” vs. “has-a”).

Som altid er alle kodeeksempler vist i denne vejledning tilgængelige på GitHub.