Arv og komposisjon (Is-a vs Has-a relationship) i Java

1. Oversikt

Arv og komposisjon - sammen med abstraksjon, innkapsling og polymorfisme - er hjørnesteinene i objektorientert programmering (OOP).

I denne opplæringen vil vi dekke det grunnleggende om arv og komposisjon, og vi vil fokusere sterkt på å oppdage forskjellene mellom de to typer forhold.

2. Arvets grunnleggende

Arv er en kraftig, men for mye og misbrukt mekanisme.

Enkelt sagt, med arv, definerer en basisklasse (aka basistype) tilstanden og oppførselen som er vanlig for en gitt type, og lar underklassene (aka subtyper) gi spesialversjoner av denne tilstanden og atferden.

For å få en klar ide om hvordan du kan arbeide med arv, la oss lage et naivt eksempel: en basisklasse Person som definerer de vanlige feltene og metodene for en person, mens underklassene Servitør og Skuespillerinne gi ytterligere, finkornede metodeimplementeringer.

Her er Person klasse:

offentlig klasse Person {privat slutt Strengnavn; // andre felt, standardkonstruktører, getters}

Og dette er underklassene:

offentlig klasse Servitrisen utvider Person {public String serveStarter (String starter) {retur "Servering a" + starter; } // tilleggsmetoder / konstruktører} 
offentlig klasse Skuespillerinne utvider Person {public String readScript (String movie) {retur "Leser manus til" + film; } // tilleggsmetoder / konstruktører}

I tillegg, la oss lage en enhetstest for å verifisere at forekomster av Servitør og Skuespillerinne klasser er også forekomster av Person, og viser dermed at "is-a" -betingelsen er oppfylt på typenivå:

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

Det er viktig å understreke den semantiske fasetten til arv. Bortsett fra å gjenbruke implementeringen av Personklasse, Vi har skapt et veldefinert “is-a” forhold mellom basistypen Person og undertypene Servitør og Skuespillerinne. Servitriser og skuespillerinner er faktisk personer.

Dette kan føre til at vi spør: i hvilke brukssaker er arv riktig tilnærming å ta?

Hvis undertyper oppfyller "is-a" -betingelsen og hovedsakelig gir additiv funksjonalitet lenger nede i klassens hierarki,da er arv veien å gå.

Selvfølgelig er metodeoverstyring tillatt så lenge de overstyrte metodene bevarer basistypen / undertypesubstituerbarheten som fremmes av Liskov-substitusjonsprinsippet.

I tillegg bør vi huske på det undertypene arver basistypens API, som i noen tilfeller kan være for mye eller bare uønsket.

Ellers bør vi bruke komposisjon i stedet.

3. Arv i designmønstre

Mens det er enighet om at vi bør favorisere sammensetning fremfor arv når det er mulig, er det noen få typiske brukssaker der arv har sin plass.

3.1. The Layer Supertype Pattern

I dette tilfellet, vi bruk arv til å flytte vanlig kode til en basisklasse (supertypen), per lag.

Her er en grunnleggende implementering av dette mønsteret i domenelaget:

offentlig klasse Enhet {beskyttet lang id; // setters} 
offentlig klasse bruker utvider enhet {// tilleggsfelt og -metoder} 

Vi kan bruke samme tilnærming til de andre lagene i systemet, for eksempel service- og utholdenhetslagene.

3.2. Malmetodemønsteret

I malmetodemønsteret kan vi bruk en baseklasse til å definere de uforanderlige delene av en algoritme, og implementer deretter variantdelene i underklassene:

offentlig abstrakt klasse ComputerBuilder {offentlig slutt Computer buildComputer () {addProcessor (); addMemory (); } offentlig abstrakt ugyldig addProcessor (); offentlig abstrakt ugyldig addMemory (); } 
offentlig klasse StandardComputerBuilder utvider ComputerBuilder {@Override public void addProcessor () {// method implementation} @Override public void addMemory () {// method implementation}}

4. Sammensetningens grunnleggende

Sammensetningen er en annen mekanisme levert av OOP for gjenbruk av implementering.

I et nøtteskall, komposisjon lar oss modellere objekter som består av andre objekter, og dermed definere et "has-a" forhold mellom dem.

Dessuten, sammensetningen er den sterkeste form for tilknytning, som betyr at objektet (e) som komponerer eller er inneholdt av ett objekt blir ødelagt også når objektet blir ødelagt.

For å bedre forstå hvordan komposisjon fungerer, la oss anta at vi trenger å jobbe med objekter som representerer datamaskiner.

En datamaskin består av forskjellige deler, inkludert mikroprosessoren, minnet, et lydkort og så videre, slik at vi kan modellere både datamaskinen og hver av dens deler som individuelle klasser.

Slik får du en enkel implementering av Datamaskin klassen kan se ut:

offentlig klasse datamaskin {privat prosessor prosessor; privat minne; private SoundCard lydkort; // standard getters / setters / constructors public Optional getSoundCard () {return Optional.ofNullable (soundCard); }}

Følgende klasser modellerer en mikroprosessor, minnet og et lydkort (grensesnitt er utelatt for kortfattede skyld):

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

Det er lett å forstå motivasjonene bak å skyve komposisjon over arv. I hvert scenario der det er mulig å etablere et semantisk korrekt “has-a” forhold mellom en gitt klasse og andre, er sammensetningen det riktige valget å ta.

I eksemplet ovenfor, Datamaskin oppfyller "has-a" -tilstanden med klassene som modellerer delene.

Det er også verdt å merke seg at i dette tilfellet, inneholder Datamaskin objektet har eierskap til de inneholdte objektene hvis og bare hvis gjenstandene kan ikke brukes på nytt i en annen Datamaskin gjenstand. Hvis de kan, bruker vi aggregering i stedet for sammensetning der eierskap ikke er underforstått.

5. Komposisjon uten abstraksjon

Alternativt kunne vi ha definert sammensetningsforholdet ved å hardkode avhengighetene til Datamaskin klasse, i stedet for å erklære dem i konstruktøren:

offentlig klasse datamaskin {privat StandardProsessor prosessor = ny StandardProsessor ("Intel I3"); privat StandardMemory-minne = nytt StandardMemory ("Kingston", "1TB"); // flere felt / metoder}

Selvfølgelig ville dette være en stiv, tett koblet design, slik vi ville lage Datamaskin sterkt avhengig av spesifikke implementeringer av Prosessor og Hukommelse.

Vi ville ikke utnytte nivået av abstraksjon som tilbys av grensesnitt og avhengighetsinjeksjon.

Med den første designen basert på grensesnitt, får vi et løst koblet design, som også er lettere å teste.

6. Konklusjon

I denne artikkelen lærte vi det grunnleggende om arv og komposisjon i Java, og vi utforsket inngående forskjellene mellom de to typer relasjoner (“is-a” vs. “has-a”).

Som alltid er alle kodeeksemplene som vises i denne opplæringen tilgjengelig på GitHub.


$config[zx-auto] not found$config[zx-overlay] not found