Arv (objektorienteret programmering) - Inheritance (object-oriented programming)

I objektorienteret programmering er arv mekanismen til at basere et objekt eller en klasse på et andet objekt ( prototype-baseret arv ) eller klasse ( klassebaseret arv ), idet den bevarer lignende implementering . Også defineret som at udlede nye klasser ( underklasser ) fra eksisterende, såsom superklasse eller basisklasse og derefter danne dem til et hierarki af klasser. I de fleste klassebaserede objektorienterede sprog erhverver et objekt, der er skabt gennem arv, et "underordnet objekt" alle egenskaber og adfærd for "overordnet objekt", med undtagelse af: konstruktører , destruktorer, overbelastede operatører og vennefunktioner af grundklassen. Arv giver programmerere mulighed for at oprette klasser, der er bygget på eksisterende klasser, at specificere en ny implementering og samtidig bevare den samme adfærd ( realisere et interface ), genbruge kode og uafhængigt udvide original software via offentlige klasser og grænseflader . Forholdet mellem objekter eller klasser gennem arv giver anledning til en rettet graf .

Arv blev opfundet i 1969 for Simula og bruges nu i mange objektorienterede programmeringssprog som Java , C ++ , PHP og Python .

En arvet klasse kaldes en underklasse af sin forældreklasse eller superklasse. Udtrykket "arv" bruges løst til både klassebaseret og prototypebaseret programmering, men i snæver brug er udtrykket forbeholdt klassebaseret programmering (en klasse arver fra en anden), med den tilsvarende teknik i prototypebaseret programmering i stedet kaldes delegation (et objekt delegerer til et andet).

Arv bør ikke forveksles med undertyper . På nogle sprog er arv og undertyper enige, mens de på andre er forskellige; generelt etablerer subtyping et is-a- forhold, hvorimod arv kun genbruger implementering og etablerer et syntaktisk forhold, ikke nødvendigvis et semantisk forhold (arv sikrer ikke adfærdsmæssig undertyping). For at skelne mellem disse begreber omtales undertypen undertiden som interface -arv (uden at anerkende, at specialiseringen af ​​typevariabler også fremkalder en subtyping -relation), mens arv som defineret her er kendt som implementeringsarv eller kode -arv . Alligevel er arv en almindeligt anvendt mekanisme til etablering af undertypeforhold.

Arv står i kontrast til objektsammensætning , hvor et objekt indeholder et andet objekt (eller objekter i en klasse indeholder objekter af en anden klasse); se sammensætning over arv . Sammensætning implementerer et has-a- forhold, i modsætning til subtyping-er-et-forholdet.

Typer

Image
Enkelt arv
Image
Flere arv

Der er forskellige former for arv, baseret på paradigme og specifikt sprog.

Enkelt arv
hvor underklasser arver funktionerne i en superklasse. En klasse erhverver egenskaberne af en anden klasse.
Flere arv
hvor en klasse kan have mere end én superklasse og arve funktioner fra alle forældreklasser.

"Multipel arv  ... var bredt formodes at være meget vanskeligt at gennemføre effektivt. For eksempel i en sammenfatning af C ++ i sin bog om Objective C , Brad Cox faktisk hævdede at tilføje multipel nedarvning til C ++ var umuligt. Således multipel nedarvning syntes mere af en udfordring. Da jeg allerede i 1982 havde overvejet flere arv og fandt en enkel og effektiv implementeringsteknik i 1984, kunne jeg ikke modstå udfordringen. Jeg formoder, at dette var det eneste tilfælde, hvor måde påvirkede hændelsesforløbet . "

Arv på flere niveauer
hvor en underklasse arves fra en anden underklasse. Det er ikke ualmindeligt, at en klasse stammer fra en anden afledt klasse som vist i figuren "Multilevel arv".
Image
Arv på flere niveauer
Klassen A fungerer som en base klasse for afledte klasse B , som igen tjener som en base klasse for afledte klasse C . Klassen B er kendt som mellemprodukt base klasse, fordi det giver et link for arv mellem A og C . Kæden ABC er kendt som arvevej .
En afledt klasse med multilevel -arv erklæres som følger:
Class A(...);      // Base class
Class B : public A(...);   // B derived from A
Class C : public B(...);   // C derived from B
Denne proces kan udvides til et vilkårligt antal niveauer.
Hierarkisk arv
Det er her, en klasse fungerer som en superklasse (basisklasse) for mere end en underklasse. For eksempel kan en forældreklasse, A, have to underklasser B og C. Både B og C's forældreklasse er A, men B og C er to separate underklasser.
Hybrid arv
Hybrid arv er, når der opstår en blanding af to eller flere af de ovennævnte arvstyper. Et eksempel på dette er, når klasse A har en underklasse B, som har to underklasser, C og D. Dette er en blanding af både multilevel arv og hierarkisk arv.

Underklasser og superklasser

Underklasser , afledte klasser , arving klasser , eller barn klasser er modulære afledte klasser, der arver en eller flere sprog enheder fra én eller flere andre klasser (kaldet superklasse , basisklasser eller forælder klasser ). Semantikken for klassearv varierer fra sprog til sprog, men almindeligvis arver underklassen automatisk forekomstvariablerne og medlemsfunktionerne i dens superklasser.

Den generelle form for definition af en afledt klasse er:

class SubClass: visibility SuperClass
{
    // subclass members
};
  • Tyktarmen angiver, at underklassen arver fra superklassen. Synligheden er valgfri og kan, hvis den findes, enten være privat eller offentlig . Standardsynligheden er privat . Synlighed angiver, om funktionerne i basisklassen er privatafledte eller offentligt afledte .

Nogle sprog understøtter også arv fra andre konstruktioner. For eksempel i Eiffel , kontrakter , der definerer specifikationerne for en klasse er også arvet af arvinger. Superklassen etablerer en fælles grænseflade og grundlæggende funktionalitet, som specialiserede underklasser kan arve, ændre og supplere. Softwaren, der arves af en underklasse, betragtes som genbrugt i underklassen. En henvisning til en forekomst af en klasse kan faktisk referere til en af ​​dens underklasser. Den faktiske klasse af objektet, der refereres til, er umulig at forudsige på kompileringstidspunktet . En ensartet grænseflade bruges til at påberåbe sig medlemsfunktionerne i objekter af en række forskellige klasser. Underklasser kan erstatte superklassefunktioner med helt nye funktioner, der skal dele den samme metodesignatur .

Ikke-underklassificerede klasser

På nogle sprog kan en klasse blive erklæret som ikke-underklassebar ved at tilføje bestemte klassemodifikatorer til klasseerklæringen. Eksempler omfatter finalsøgeordet i Java og C ++ 11 og fremefter eller sealedsøgeordet i C#. Sådanne modifikatorer føjes til klasseerklæringen før classsøgeordet og klasseidentifikationserklæringen. Sådanne ikke-underklassificerede klasser begrænser genanvendelighed , især når udviklere kun har adgang til forudkompilerede binære filer og ikke kildekode .

En klasse, der ikke kan underklasse, har ingen underklasser, så det kan let udledes på kompileringstidspunktet, at henvisninger eller henvisninger til objekter i den klasse faktisk refererer til forekomster af den klasse og ikke til forekomster af underklasser (de findes ikke) eller forekomster af superklasser ( upcasting af en referencetype krænker typesystemet ). Fordi den nøjagtige type af objektet, der refereres til, er kendt før udførelse, kan tidlig binding (også kaldet statisk afsendelse ) bruges i stedet for sen binding (også kaldet dynamisk afsendelse ), hvilket kræver et eller flere virtuelle metodeopslag, afhængigt af om flere arv eller kun enkelt arv understøttes i det programmeringssprog, der bruges.

Ikke-overridelige metoder

Ligesom klasser kan være ikke-underklassificerede, kan metodedeklarationer indeholde metodemodifikatorer, der forhindrer metoden i at blive tilsidesat (dvs. erstattet med en ny funktion med samme navn og typesignatur i en underklasse). En privat metode kan ikke overskrides, simpelthen fordi den ikke er tilgængelig for andre klasser end den klasse, den er en medlemsfunktion i (dette er dog ikke sandt for C ++). En finalmetode i Java, en sealedmetode i C# eller en frozenfunktion i Eiffel kan ikke tilsidesættes.

Virtuelle metoder

Hvis superklassemetoden er en virtuel metode , sendes påkaldelser af superklassemetoden dynamisk . Nogle sprog kræver, at metoder specifikt erklæres som virtuelle (f.eks. C ++), og på andre er alle metoder virtuelle (f.eks. Java). En påkaldelse af en ikke-virtuel metode vil altid blive sendt statisk (dvs. adressen på funktionsopkaldet bestemmes ved kompileringstidspunktet). Statisk forsendelse er hurtigere end dynamisk forsendelse og tillader optimeringer som f.eks. Inline -ekspansion .

Synlighed for arvede medlemmer

Følgende tabel viser, hvilke variabler og funktioner, der bliver arvet afhængigt af den synlighed, der gives, når man udleder klassen.

Grundklassens synlighed Afledt klassesynlighed
Offentlig afledning Privat afledning Beskyttet afledning
  • Privat →
  • Beskyttet →
  • Offentligt →
  • Ikke arvet
  • Beskyttet
  • Offentlig
  • Ikke arvet
  • Privat
  • Privat
  • Ikke arvet
  • Beskyttet
  • Beskyttet

Ansøgninger

Arv bruges til at forbinde to eller flere klasser med hinanden.

Overstyrende

Image
Illustration af metodeoverstyring

Mange objektorienterede programmeringssprog tillader en klasse eller et objekt at erstatte implementeringen af ​​et aspekt-typisk en adfærd-som det har arvet. Denne proces kaldes overstyring . Overstyring introducerer en komplikation: hvilken version af adfærden bruger en forekomst af den arvede klasse - den, der er en del af sin egen klasse, eller den fra forældreklassen (basis)? Svaret varierer mellem programmeringssprog, og nogle sprog giver mulighed for at angive, at en bestemt adfærd ikke skal tilsidesættes og bør opføre sig som defineret af basisklassen. For eksempel i C#kan basismetoden eller egenskaben kun tilsidesættes i en underklasse, hvis den er markeret med den virtuelle, abstrakte eller tilsidesatte modifikator, mens der i programmeringssprog som Java kan kaldes forskellige metoder til at tilsidesætte andre metoder. Et alternativ til tilsidesættelse er at skjule den nedarvede kode.

Genbrug af kode

Implementeringsarv er mekanismen, hvorved en underklasse genbruger kode i en basisklasse. Som standard beholder underklassen alle operationer i basisklassen, men underklassen kan tilsidesætte nogle eller alle operationer og erstatte basisklasseimplementeringen med sin egen.

I det følgende Python -eksempel tilsidesætter underklasserne SquareSumComputer og CubeSumComputer transform () -metoden for baseklassen SumComputer . Basisklassen omfatter operationer til beregning af summen af kvadraterne mellem to heltal. Underklassen genbruger al funktionaliteten i basisklassen med undtagelse af operationen, der omdanner et tal til sin firkant, og erstatter det med en operation, der omdanner et tal til henholdsvis sin firkant og terning . Underklasserne beregner derfor summen af ​​kvadraterne/terningerne mellem to heltal.

Nedenfor er et eksempel på Python.

class SumComputer:
    def __init__(self, a, b):
        self.a = a
        self.b = b

    def transform(self, x):
        raise NotImplementedError

    def inputs(self):
        return range(self.a, self.b)

    def compute(self):
        return sum(self.transform(value) for value in self.inputs())

class SquareSumComputer(SumComputer):
    def transform(self, x):
        return x * x

class CubeSumComputer(SumComputer):
    def transform(self, x):
        return x * x * x

I de fleste kvartaler er klassearv med det ene formål at genbruge kode faldet i unåde. Den primære bekymring er, at implementeringsarv ikke giver nogen sikkerhed for polymorf substituerbarhed - en forekomst af genbrugsklassen kan ikke nødvendigvis erstattes af en forekomst af den arvede klasse. En alternativ teknik, eksplicit delegering , kræver mere programmeringsindsats, men undgår problem med substituerbarhed. I C ++ kan privat arv bruges som en form for implementeringsarv uden substituerbarhed. Mens offentlig arv repræsenterer et "is-a" -forhold, og delegation repræsenterer et "has-a" -forhold, kan privat (og beskyttet) arv betragtes som et "er implementeret i form af" -forhold.

En anden hyppig brug af arv er at garantere, at klasser opretholder en bestemt fælles grænseflade; det vil sige, de implementerer de samme metoder. Forældreklassen kan være en kombination af implementerede operationer og operationer, der skal implementeres i barneklasserne. Ofte er der ingen grænsefladeændring mellem supertype og undertype- barnet implementerer den beskrevne adfærd i stedet for sin forældreklasse.

Arv vs subtyping

Arv ligner, men adskiller sig fra subtyping. Subtyping gør det muligt at erstatte en given type med en anden type eller abstraktion, og siges at etablere et is-a- forhold mellem undertypen og en eksisterende abstraktion, enten implicit eller eksplicit, afhængigt af sproglig støtte. Forholdet kan udtrykkes eksplicit via arv på sprog, der understøtter arv som en underskrivningsmekanisme. For eksempel etablerer følgende C ++ - kode et eksplicit arverelation mellem klasse B og A , hvor B både er en underklasse og en undertype af A , og kan bruges som A, hvor et B er angivet (via en reference, en markør eller selve objektet).

class A {
 public:
  void DoSomethingALike() const {}
};

class B : public A {
 public:
  void DoSomethingBLike() const {}
};

void UseAnA(const A& a) {
  a.DoSomethingALike();
}

void SomeFunc() {
  B b;
  UseAnA(b);  // b can be substituted for an A.
}

I programmeringssprog, der ikke understøtter arv som en undertypningsmekanisme , er forholdet mellem en basisklasse og en afledt klasse kun et forhold mellem implementeringer (en mekanisme til genbrug af kode), sammenlignet med et forhold mellem typer . Arv, selv i programmeringssprog, der understøtter arv som en undertypningsmekanisme, medfører ikke nødvendigvis adfærdsmæssig sub -typning . Det er helt muligt at udlede en klasse, hvis objekt vil opføre sig forkert, når det bruges i en kontekst, hvor forældreklassen forventes; se Liskov -substitutionsprincippet . (Sammenlign konnotation/betegnelse .) I nogle OOP -sprog falder begreberne genbrug af kode og undertypning sammen, fordi den eneste måde at erklære en undertype er at definere en ny klasse, der arver implementeringen af ​​en anden.

Designbegrænsninger

Brug af arv i vid udstrækning ved design af et program pålægger visse begrænsninger.

Overvej f.eks. En klasseperson, der indeholder en persons navn, fødselsdato, adresse og telefonnummer. Vi kan definere en underklasse af Person kaldet Student, der indeholder personens karaktergennemsnit og klasser taget, og en anden underklasse af Person kaldet Medarbejder, der indeholder personens jobtitel, arbejdsgiver og løn.

Ved definitionen af ​​dette arvshierarki har vi allerede defineret visse begrænsninger, som ikke alle er ønskelige:

Ensomhed
Ved hjælp af enkelt arv kan en underklasse arve fra kun en superklasse. I forlængelse af eksemplet ovenfor kan personen enten være en studerende eller en medarbejder , men ikke begge dele. Brug af flere arv løser delvist dette problem, da man derefter kan definere en StudentEmployee -klasse, der arver fra både studerende og medarbejder . I de fleste implementeringer kan den dog stadig kun arve fra hver superklasse én gang og understøtter således ikke tilfælde, hvor en studerende har to job eller deltager i to institutioner. Den arvsmodel, der er tilgængelig i Eiffel, gør dette muligt gennem understøttelse af gentagen arv .
Statisk
Arvshierarkiet for et objekt fastgøres ved instantiering, når objektets type vælges og ændres ikke med tiden. For eksempel tillader arvediagrammet ikke, at et elevobjekt kan blive et medarbejderobjekt, samtidig med at tilstanden for dens Person -superklasse bevares . (Denne form for adfærd kan imidlertid opnås med dekoratørmønsteret .) Nogle har kritiseret arv og hævdet, at det låser udviklere i deres originale designstandarder.
Sigtbarhed
Når klientkoden har adgang til et objekt, har den generelt adgang til alle objektets superklassedata. Selvom superklassen ikke er blevet erklæret offentlig, kan klienten stadig kaste objektet til sin superklassetype. For eksempel er der ingen måde at give en funktion en pointer til en Student 's kvotient og afskrift uden også at give denne funktion adgang til alle de personoplysninger, der er lagret i den studerendes Person superklasse. Mange moderne sprog, herunder C ++ og Java, giver en "beskyttet" adgangsmodifikator, der giver underklasser adgang til dataene, uden at kode uden for arvskæden får adgang til dem.

Princippet om sammensat genbrug er et alternativ til arv. Denne teknik understøtter polymorfisme og genbrug af kode ved at adskille adfærd fra primærklassens hierarki og inkludere specifikke adfærdsklasser som krævet i enhver virksomhedsdomæne klasse. Denne tilgang undgår den statiske karakter af et klassehierarki ved at tillade adfærdsændringer i løbetid og giver en klasse mulighed for at implementere adfærd i buffetstil, i stedet for at være begrænset til adfærden hos sine forfaderklasser.

Spørgsmål og alternativer

Implementeringsarv er kontroversiel blandt programmører og teoretikere inden for objektorienteret programmering siden mindst 1990'erne. Blandt dem er forfatterne af Design Patterns , der i stedet går ind for interface -arv og favoriserer sammensætning frem for arv. For eksempel er dekoratormønsteret (som nævnt ovenfor ) blevet foreslået for at overvinde den statiske karakter af arv mellem klasser. Som en mere grundlæggende løsning på det samme problem introducerer rolleorienteret programmering et særskilt forhold, der spilles af , og kombinerer arv og sammensætnings egenskaber til et nyt koncept.

Ifølge Allen Holub er hovedproblemet med implementeringsarv, at det introducerer unødvendig kobling i form af det "skrøbelige grundklasseproblem" : ændringer af basisklasseimplementeringen kan forårsage utilsigtede adfærdsændringer i underklasser. Brug af grænseflader undgår dette problem, fordi ingen implementering deles, kun API'en. En anden måde at angive dette på er, at "arv bryder indkapsling ". Problemet viser sig klart i åbne objektorienterede systemer såsom rammer , hvor klientkode forventes at arve fra systemleverede klasser og derefter erstatte systemets klasser i dets algoritmer.

Efter sigende har Java -opfinder James Gosling talt imod implementeringsarv og erklæret, at han ikke ville medtage det, hvis han skulle redesigne Java. Sprogdesign, der afkobler arv fra subtyping (interface -arv) dukkede op allerede i 1990; et moderne eksempel på dette er programmeringssproget Go .

Kompleks arv eller arv, der bruges inden for et utilstrækkeligt modent design, kan føre til jojo-problemet . Da arv blev brugt som en primær tilgang til strukturering af kode i et system i slutningen af ​​1990'erne, begyndte udviklere naturligvis at bryde koden i flere arvelag, efterhånden som systemfunktionaliteten voksede. Hvis et udviklingsteam kombinerede flere arvelag med princippet om enkeltansvar, skabte det mange supertynde kodelag, mange som kun ville have 1 eller 2 linjer kode i hvert lag. For mange lag gør fejlfinding til en væsentlig udfordring, da det bliver svært at afgøre, hvilket lag der skal debugges.

Et andet problem med arv er, at underklasser skal defineres i kode, hvilket betyder, at programbrugere ikke kan tilføje nye underklasser ved runtime. Andre designmønstre (f.eks. Entity -component -system ) gør det muligt for programbrugere at definere variationer af en enhed ved runtime.

Se også

Noter

Referencer

Yderligere læsning