Programmering

Hvorfor utvider er ondt

De strekker nøkkelord er ondt; kanskje ikke på Charles Manson-nivå, men ille nok til at den skal unngås når det er mulig. The Four of Gang Design mønstre boken diskuterer utførlig og erstatter implementeringsarv (strekker) med grensesnittarv (redskaper).

Gode ​​designere skriver det meste av koden sin når det gjelder grensesnitt, ikke konkrete baseklasser. Denne artikkelen beskriver Hvorfor designere har så rare vaner, og introduserer også noen få grensesnittbaserte grunnleggende programmeringsprogrammer.

Grensesnitt mot klasser

Jeg deltok en gang på et Java-brukergruppemøte der James Gosling (Java's oppfinner) var den fremste høyttaleren. Under den minneverdige spørsmål og svar spurte noen ham: "Hvis du kunne gjøre Java på nytt, hva ville du endre?" "Jeg vil utelate kurs," svarte han. Etter at latteren la seg, forklarte han at det virkelige problemet ikke var klasser per se, men heller implementeringsarv (den strekker forhold). Grensesnittarv ( redskaper forhold) er å foretrekke. Du bør unngå implementeringsarv når det er mulig.

Mister fleksibilitet

Hvorfor bør du unngå arv ved implementering? Det første problemet er at eksplisitt bruk av konkrete klassenavn låser deg i bestemte implementeringer, noe som gjør endringer utenom det vanlige vanskelig.

Kjernen i moderne Agile utviklingsmetoder er begrepet parallell design og utvikling. Du begynner å programmere før du spesifiserer programmet helt. Denne teknikken flyr i møte med tradisjonell visdom - at et design skal være komplett før programmering starter - men mange vellykkede prosjekter har bevist at du kan utvikle kode av høy kvalitet raskere (og kostnadseffektivt) på denne måten enn med den tradisjonelle rørledningsmetoden. Kjernen i parallell utvikling er imidlertid begrepet fleksibilitet. Du må skrive koden din på en slik måte at du kan innlemme nyoppdagede krav i den eksisterende koden så smertefritt som mulig.

I stedet for å implementere funksjoner du kanskje trenger, implementerer du bare funksjonene du har helt sikkert behov, men på en måte som imøtekommer forandring. Hvis du ikke har denne fleksibiliteten, er parallell utvikling ganske enkelt ikke mulig.

Programmering til grensesnitt er kjernen i fleksibel struktur. For å se hvorfor, la oss se på hva som skjer når du ikke bruker dem. Vurder følgende kode:

f () {LinkedList list = new LinkedList (); //... g (liste); } g (LinkedList-liste) {list.add (...); g2 (liste)} 

Anta at et nytt krav for rask oppslag har dukket opp, så LinkedList fungerer ikke. Du må erstatte den med en HashSet. I den eksisterende koden er ikke endringen lokalisert, siden du ikke bare må endre f () men også g () (som tar en LinkedList argument), og hva som helst g () overfører listen til.

Omskrive koden slik:

f () {Samlingsliste = ny LinkedList (); //... g (liste); } g (Samlingsliste) {list.add (...); g2 (liste)} 

gjør det mulig å endre den koblede listen til en hash-tabell ved å erstatte ny LinkedList () med en nytt HashSet (). Det er det. Ingen andre endringer er nødvendige.

Som et annet eksempel, sammenlign denne koden:

f () {Collection c = new HashSet (); //... g (c); } g (Collection c) {for (Iterator i = c.iterator (); i.hasNext ();) do_something_with (i.next ()); } 

til dette:

f2 () {Collection c = new HashSet (); //... g2 (c.iterator ()); } g2 (Iterator i) {while (i.hasNext ();) do_something_with (i.next ()); } 

De g2 () metoden kan nå krysse Samling derivater samt nøkkel- og verdilister du kan få fra a Kart. Faktisk kan du skrive iteratorer som genererer data i stedet for å krysse en samling. Du kan skrive iteratorer som mater informasjon fra et teststillas eller en fil til programmet. Det er enorm fleksibilitet her.

Kobling

Et mer avgjørende problem med implementeringsarv er kobling—En uønsket avhengighet av en del av et program på en annen del. Globale variabler er det klassiske eksemplet på hvorfor sterk kobling gir problemer. Hvis du for eksempel endrer typen av den globale variabelen, er alle funksjonene som bruker variabelen (dvs. koblet til variabelen) kan bli påvirket, så all denne koden må undersøkes, modifiseres og testes på nytt. Videre er alle funksjoner som bruker variabelen koblet til hverandre gjennom variabelen. Det vil si at en funksjon kan påvirke atferdene til en annen funksjon feil hvis verdien til en variabel endres på et vanskelig tidspunkt. Dette problemet er spesielt avskyelig i flertrådede programmer.

Som designer bør du strebe etter å minimere koblingsforhold. Du kan ikke eliminere kobling helt fordi en metodeanrop fra et objekt av en klasse til et objekt av en annen er en form for løs kobling. Du kan ikke ha et program uten noen kobling. Ikke desto mindre kan du minimere kobling betraktelig ved å følge OO (objektorienterte) forskrifter (det viktigste er at implementeringen av et objekt skal være helt skjult for objektene som bruker det). For eksempel bør et objekts forekomstvariabler (medlemsfelt som ikke er konstanter) alltid være privat. Periode. Ingen unntak. Noen gang. Jeg mener det. (Du kan av og til bruke beskyttet metoder effektivt, men beskyttet forekomstvariabler er en styggelse.) Du bør aldri bruke get / set-funksjoner av samme grunn - de er bare altfor kompliserte måter å gjøre et felt offentlig på (selv om tilgangsfunksjoner som returnerer fullverdige objekter i stedet for en grunnleggende verdi er rimelig i situasjoner der det returnerte objektets klasse er en nøkkelabstraksjon i designet).

Jeg er ikke pedantisk her. Jeg har funnet en direkte sammenheng i mitt eget arbeid mellom strenghet i min OO-tilnærming, rask kodeutvikling og enkelt kodevedlikehold. Når jeg bryter et sentralt OO-prinsipp som å gjemme meg, ender jeg opp med å skrive om koden (vanligvis fordi koden er umulig å feilsøke). Jeg har ikke tid til å skrive om programmer, så jeg følger reglene. Min bekymring er helt praktisk - jeg har ingen interesse for renhet for renhets skyld.

Det skjøre basisklasseproblemet

La oss nå bruke begrepet kobling til arv. I et implementeringsarvsystem som bruker strekker, er de avledede klassene veldig tett koblet til basisklassene, og denne nære forbindelsen er uønsket. Designere har brukt monikeren "det skjøre baseklasseproblemet" for å beskrive denne oppførselen. Baseklasser betraktes som skjøre fordi du kan endre en basisklasse på en tilsynelatende sikker måte, men denne nye oppførselen, når den arves av de avledede klassene, kan føre til at de avledede klassene ikke fungerer. Du kan ikke fortelle om en endring i baseklassen er trygg bare ved å undersøke baseklassens metoder isolert; du må se på (og teste) alle avledede klasser også. Videre må du sjekke all kode som bruker begge baseklassen og avledede objekter også, siden denne koden også kan bli ødelagt av den nye oppførselen. En enkel endring av en nøkkelbaseklasse kan gjøre et helt program ubrukelig.

La oss undersøke de skjøre baseklasse- og baseklassekoblingsproblemene sammen. Følgende klasse utvider Java ArrayList klasse for å få den til å oppføre seg som en stabel:

klasse Stack utvider ArrayList {private int stack_pointer = 0; public void push (Objektartikkel) {add (stack_pointer ++, artikkel); } public Object pop () {return remove (--stack_pointer); } offentlig ugyldig push_many (Object [] artikler) {for (int i = 0; i <articles.length; ++ i) push (articles [i]); }} 

Selv en klasse så enkel som denne har problemer. Tenk på hva som skjer når en bruker utnytter arv og bruker ArrayLists klar() metode for å poppe alt av bunken:

Stack a_stack = new Stack (); a_stack.push ("1"); a_stack.push ("2"); a_stack.clear (); 

Koden kompileres vellykket, men siden baseklassen ikke vet noe om stabelpekeren, er Stable objektet er nå i en udefinert tilstand. Neste samtale til trykk() setter den nye varen i indeks 2 ( stack_pointernåværende verdi), så stabelen har tre elementer effektivt - de to nederste er søppel. (Java Stable klassen har akkurat dette problemet; ikke bruk den.)

En løsning på det uønskede metodearvproblemet er for Stable å overstyre alle ArrayList metoder som kan endre matrisens tilstand, slik at overstyringene enten manipulerer stabelpekeren riktig eller kaster et unntak. (De removeRange () metoden er en god kandidat for å kaste et unntak.)

Denne tilnærmingen har to ulemper. For det første, hvis du overstyrer alt, bør baseklassen virkelig være et grensesnitt, ikke en klasse. Det er ingen vits i implementeringsarv hvis du ikke bruker noen av de arvede metodene. For det andre, og enda viktigere, vil du ikke ha en bunke som støtter alle ArrayList metoder. Det irriterende removeRange () metoden er ikke nyttig, for eksempel. Den eneste rimelige måten å implementere en ubrukelig metode på er å få den til å kaste et unntak, siden den aldri skal kalles. Denne tilnærmingen flytter effektivt det som vil være en kompilasjonsfeil til kjøretiden. Ikke bra. Hvis metoden rett og slett ikke blir deklarert, sparker kompilatoren ut en metode som ikke ble funnet. Hvis metoden er der, men kaster et unntak, vil du ikke finne ut om samtalen før programmet faktisk kjører.

En bedre løsning på basisklasseproblemet er å kapsle inn datastrukturen i stedet for å bruke arv. Her er en ny og forbedret versjon av Stable:

klasse Stack {private int stack_pointer = 0; private ArrayList the_data = nye ArrayList (); public void push (Objektartikkel) {the_data.add (stack_pointer ++, artikkel); } public Object pop () {return the_data.remove (--stack_pointer); } public void push_many (Object [] articles) {for (int i = 0; i <o.length; ++ i) push (articles [i]); }} 

Så langt så bra, men vurder det skjøre basisklassesaken. La oss si at du vil lage en variant på Stable som sporer maksimal stabelstørrelse over en viss tidsperiode. En mulig implementering kan se slik ut:

klasse Monitorable_stack utvider Stack {private int high_water_mark = 0; privat int gjeldende størrelse; public void push (Objektartikkel) {if (++ current_size> high_water_mark) high_water_mark = current_size; super.push (artikkel); } offentlig Object pop () {--current_size; returner super.pop (); } public int maximum_size_so_far () {return high_water_mark; }} 

Denne nye klassen fungerer bra, i det minste en stund. Dessverre utnytter koden det faktum at push_many () gjør sitt ved å ringe trykk(). I begynnelsen virker ikke denne detaljene som et dårlig valg. Det forenkler koden, og du får den avledede klasseversjonen av trykk(), selv når Monitorable_stack er tilgjengelig via en Stable referanse, slik at høy_vann_merke oppdaterer riktig.

En fin dag kan noen kjøre en profil og legge merke til Stable er ikke så raskt som det kunne være og er mye brukt. Du kan skrive om Stable slik at den ikke bruker en ArrayList og følgelig forbedre Stableytelse. Her er den nye lean-and-mean versjonen:

klasse Stack {private int stack_pointer = -1; privat objekt [] stack = nytt objekt [1000]; public void push (Objektartikkel) {assert stack_pointer = 0; returstabel [stack_pointer--]; } public void push_many (Object [] articles) {assert (stack_pointer + articles.length) <stack.length; System.arraycopy (artikler, 0, stack, stack_pointer + 1, articles.length); stack_pointer + = articles.length; }} 

Legg merke til det push_many () ringer ikke lenger trykk() flere ganger — det gjør en blokkoverføring. Den nye versjonen av Stable fungerer fint; faktisk er det bedre enn forrige versjon. Dessverre, den Monitorable_stack avledet klasse gjør ikke det fungerer lenger, siden det ikke vil spore stackbruk riktig hvis push_many () kalles (den avledede klasseversjonen av trykk() kalles ikke lenger av arvet push_many () metode, så push_many () oppdaterer ikke lenger høyt_vann_merke). Stable er en skjør basisklasse. Som det viser seg er det praktisk talt umulig å eliminere denne typen problemer bare ved å være forsiktig.

Merk at du ikke har dette problemet hvis du bruker grensesnittarv, siden det ikke er noen arvelig funksjonalitet som går dårlig for deg. Hvis Stable er et grensesnitt, implementert av både a Simple_stack og en Monitorable_stack, da er koden mye mer robust.

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