Programiranje

Zakaj se razteza je zlo

The podaljša ključna beseda je zlo; morda ne na ravni Charlesa Mansona, vendar dovolj slabo, da se je je treba izogibati, kadar je le mogoče. Banda štirih Vzorci oblikovanja knjiga dolgo razpravlja o nadomestitvi dedovanja izvajanja (podaljša) z dedovanjem vmesnika (izvaja).

Dobri oblikovalci večino svoje kode pišejo z vmesniki in ne s konkretnimi osnovnimi razredi. Ta članek opisuje zakaj oblikovalci imajo tako čudne navade in uvedejo tudi nekaj osnov programiranja na osnovi vmesnikov.

Vmesniki v primerjavi z razredi

Enkrat sem se udeležil sestanka uporabniške skupine Java, kjer je bil James Gosling (Javin izumitelj) predstavljeni govornik. Med nepozabno sejo vprašanj in vprašanj ga je nekdo vprašal: "Če bi lahko znova naredil Java, kaj bi spremenil?" "Pustil bi pouk," je odgovoril. Po tem, ko je smeh zamrl, je pojasnil, da resnični problem niso razredi sami po sebi, temveč izvedbeno dedovanje ( podaljša razmerje). Dedovanje vmesnika ( izvaja razmerje) je bolj zaželeno. Izogibajte se dedovanju izvedbe, kadar je to mogoče.

Izguba prilagodljivosti

Zakaj bi se morali izogibati dedovanju izvedbe? Prva težava je, da vas eksplicitna uporaba konkretnih imen razredov zaklene v določene izvedbe, kar po nepotrebnem otežuje nadaljnje spremembe.

V jedru sodobnih razvojnih metodologij Agile je koncept vzporednega oblikovanja in razvoja. Programiranje začnete, preden v celoti določite program. Ta tehnika se spopada s tradicionalno modrostjo - da mora biti načrt popoln pred začetkom programiranja, vendar so številni uspešni projekti dokazali, da lahko na ta način hitreje (in stroškovno učinkoviteje) razvijete visokokakovostno kodo kot s tradicionalnim cevovodnim pristopom. V jedru vzporednega razvoja pa je pojem prilagodljivosti. Kodo morate napisati tako, da lahko novo odkrite zahteve čim bolj neboleče vključite v obstoječo kodo.

Namesto da bi implementirali funkcije morda potrebujete, implementirate samo funkcije, ki jih imate vsekakor potrebe, vendar na način, ki ustreza spremembam. Če nimate te prilagodljivosti, vzporedni razvoj preprosto ni mogoč.

Programiranje vmesnikov je jedro prilagodljive strukture. Da vidimo, zakaj, poglejmo, kaj se zgodi, ko jih ne uporabljate. Upoštevajte naslednjo kodo:

f () {LinkedList list = nov LinkedList (); //... g (seznam); } g (Seznam povezanih seznamov) {list.add (...); g2 (seznam)} 

Zdaj pa predpostavimo, da se je pojavila nova zahteva po hitrem iskanju, zato LinkedList ne deluje. Zamenjati ga morate z HashSet. V obstoječi kodi ta sprememba ni lokalizirana, saj jo morate spremeniti ne samo f () ampak tudi g () (kar traja LinkedList argument) in kar koli drugega g () posreduje seznam.

Prepisovanje kode, kot je ta:

f () {Seznam zbirk = nov LinkedList (); //... g (seznam); } g (seznam zbirk) {list.add (...); g2 (seznam)} 

omogoča spreminjanje povezanega seznama v zgoščeno tabelo preprosto z zamenjavo nov LinkedList () z nov HashSet (). To je to. Druge spremembe niso potrebne.

Kot drug primer primerjajte to kodo:

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

za to:

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

The g2 () metoda lahko zdaj prečka Zbirka izvedene finančne instrumente ter sezname ključev in vrednosti, ki jih lahko dobite iz a Zemljevid. Pravzaprav lahko napišete iteratorje, ki ustvarjajo podatke, namesto da bi prečkali zbirko. V program lahko napišete iteratorje, ki podatke pošljejo s testnega odra ali datoteke. Tu je izjemna prilagodljivost.

Sklopka

Pomembnejši problem pri dedovanju izvedbe je sklopka—Neželeno zanašanje enega dela programa na drugega. Globalne spremenljivke predstavljajo klasičen primer, zakaj močno povezovanje povzroča težave. Če spremenite vrsto globalne spremenljivke, na primer vse funkcije, ki uporabljajo spremenljivko (tj. So skupaj na spremenljivko), zato je treba vso to kodo preučiti, spremeniti in ponovno preizkusiti. Poleg tega so vse funkcije, ki uporabljajo spremenljivko, povezane s spremenljivko. To pomeni, da lahko ena funkcija napačno vpliva na vedenje druge funkcije, če se vrednost spremenljivke spremeni v neprijetnem času. Ta težava je še posebej gnusna v večnitnih programih.

Kot oblikovalec si morate prizadevati za zmanjšanje povezav med seboj. V celoti ne morete odpraviti spenjanja, ker je klic metode iz predmeta enega razreda v objekt drugega oblika ohlapnega spenjanja. Ne morete imeti programa brez nekaj sklopke. Kljub temu lahko povezovanje znatno zmanjšate tako, da suženjsko sledite zapovedim OO (objektno usmerjene) (najpomembneje je, da je izvedba predmeta popolnoma skrita pred predmeti, ki ga uporabljajo). Na primer, spremenljivke primerka predmeta (polja članov, ki niso konstante), bi morale biti vedno zasebno. Obdobje. Brez izjem. Kdaj. Resno mislim. (Občasno lahko uporabite zaščiten metode učinkovito, vendar zaščiten spremenljivke primerka so grozota.) Nikoli ne bi smeli uporabljati funkcij get / set iz istega razloga - to so le preveč zapleteni načini, da lahko polje postane javno (čeprav so funkcije dostopa, ki vrnejo predmete v celoti, namesto vrednosti osnovnega tipa, razumno v situacijah, ko je razred vrnjenega predmeta ključna abstrakcija pri oblikovanju).

Tu nisem pedantna. V svojem delu sem našel neposredno povezavo med strogostjo mojega pristopa OO, hitrim razvojem kode in enostavnim vzdrževanjem kode. Kadarkoli kršim osrednje načelo OO, kot je skrivanje izvedbe, na koncu to kodo prepišem (običajno zato, ker je kode nemogoče odpraviti) Nimam časa za prepisovanje programov, zato se držim pravil. Moja skrb je povsem praktična - čistosti zaradi čistosti ne zanima.

Krhka težava osnovnega razreda

Zdaj pa uporabimo koncept spajanja pri dedovanju. V sistemu za izvajanje-dedovanje, ki uporablja podaljša, izpeljani razredi so zelo tesno povezani z osnovnimi razredi in ta tesna povezava je nezaželena. Oblikovalci so za opis tega vedenja uporabili ime "krhka težava osnovnega razreda". Osnovni razredi se štejejo za krhke, ker lahko osnovni razred spremenite na na videz varen način, vendar lahko to novo vedenje, če ga podedujejo izpeljani razredi, povzroči okvaro. Z ločeno preučevanjem metod osnovnega razreda ne morete ugotoviti, ali je sprememba osnovnega razreda varna; pogledati morate (in preizkusiti) tudi vse izpeljane razrede. Poleg tega morate preveriti vso kodo, ki uporablja oba osnovnega razreda in tudi predmeti izpeljanega razreda, saj bi lahko novo vedenje tudi to kodo zlomilo. Preprosta sprememba osnovnega razreda ključa lahko onemogoči delovanje celotnega programa.

Oglejmo si skupaj krhke težave pri povezovanju osnovnega in osnovnega razreda. Naslednji razred razširja Javo ArrayList razred, da se obnaša kot sklad:

razred Stack razširi ArrayList {private int stack_pointer = 0; javni void push (Object article) {add (stack_pointer ++, article); } javni Object pop () {return remove (--stack_pointer); } public void push_many (Object [] članki) {for (int i = 0; i <articles.length; ++ i) push (articles [i]); }} 

Celo tako preprost razred, kot je ta, ima težave. Razmislite, kaj se zgodi, ko uporabnik izkoristi dedovanje in uporabi ArrayListje jasno () metoda, da se vse izloči iz sklada:

Stog a_stack = nov sklad (); a_stack.push ("1"); a_stack.push ("2"); a_stack.clear (); 

Koda je uspešno prevedena, ker pa osnovni razred ne ve ničesar o kazalcu sklada, Stack objekt je zdaj v nedefiniranem stanju. Naslednji klic push () postavi novo postavko v indeks 2 ( kazalec_staka(trenutna vrednost), tako da ima sklad dejansko tri elemente - spodnja dva so smeti. (Java Stack razred ima ravno to težavo; ne uporabljajte.)

Ena od rešitev neželenega problema z dedovanjem metode je za Stack preglasiti vse ArrayList metode, ki lahko spremenijo stanje matrike, zato preglasitve bodisi pravilno manipulirajo s kazalcem sklada ali vržejo izjemo. (The removeRange () metoda je dober kandidat za vrnitev izjeme.)

Ta pristop ima dve slabosti. Prvič, če preglasite vse, mora biti osnovni razred res vmesnik in ne razred. Če ne uporabite nobenega od podedovanih metod, nima smisla izvajati dedovanje. Drugič, in kar je še pomembneje, ne želite, da kup podpira vse ArrayList metode. To nadležno removeRange () metoda na primer ni uporabna. Edini smiseln način za izvajanje neuporabne metode je, da vrže izjemo, ker je nikoli ne bi smeli poklicati. Ta pristop dejansko premakne tisto, kar bi bila napaka pri prevajanju, v runtime. Slabo. Če metoda preprosto ni deklarirana, prevajalnik sproži napako, ki je ni mogoče najti. Če metoda obstaja, vendar vrže izjemo, ne boste izvedeli za klic, dokler se program dejansko ne zažene.

Boljša rešitev za težavo osnovnega razreda je vključitev podatkovne strukture namesto dedovanja. Tu je nova in izboljšana različica Stack:

razred Stack {private int stack_pointer = 0; private ArrayList the_data = new ArrayList (); javni void push (članek predmeta) {the_data.add (stack_pointer ++, article); } public Object pop () {return the_data.remove (--stack_pointer); } public void push_many (Object [] članki) {for (int i = 0; i <o.length; ++ i) push (articles [i]); }} 

Zaenkrat je dobro, vendar razmislite o krhkem vprašanju osnovnega razreda. Recimo, da želite v različici ustvariti različico Stack ki sledi največji velikosti sklada v določenem časovnem obdobju. Ena od možnih izvedb je lahko videti tako:

razred Monitorable_stack razširja Stack {private int high_water_mark = 0; private int current_size; javni void push (članek predmeta) {if (++ current_size> high_water_mark) high_water_mark = current_size; super.push (članek); } javni objekt pop () {--current_size; vrni super.pop (); } public int maximum_size_so_far () {return high_water_mark; }} 

Ta novi razred vsaj nekaj časa dobro deluje. Koda žal izkorišča dejstvo, da push_many () opravlja svoje delo s klicem push (). Sprva se ta podrobnost ne zdi slaba izbira. Poenostavlja kodo in dobite izvedeno različico razreda push (), tudi ko Monitorable_stack je dostopen prek a Stack sklic, torej oznaka_visoke_vode pravilno posodablja.

Nekega lepega dne lahko nekdo zažene profiler in opazi Stack ni tako hiter, kot bi lahko bil, in se pogosto uporablja. Lahko prepišete Stack zato ne uporablja ArrayList in posledično izboljšati Stackuspešnost. Tu je nova vitka in povprečna različica:

razred Stack {private int stack_pointer = -1; zasebni objekt [] sklad = nov objekt [1000]; javni void push (predmetni članek) {assert stack_pointer = 0; vrnitev sklada [stack_pointer--]; } public void push_many (Object [] članki) {assert (stack_pointer + articles.length) <stack.length; System.arraycopy (članki, 0, stack, stack_pointer + 1, articles.length); stack_pointer + = articles.length; }} 

Opazite to push_many () ne kliče več push () večkrat - opravi blokovni prenos. Nova različica Stack dobro deluje; pravzaprav je bolje kot prejšnja različica. Na žalost Monitorable_stack izpeljani razred ne ne deluje več, saj ne bo pravilno sledil uporabi sklada, če push_many () se imenuje (različica izpeljanega razreda push () podedovanega ne pokliče več push_many () metoda, torej push_many () ne posodablja več visok_vodni_znak). Stack je krhek osnovni razred. Izkazalo se je, da je tovrstnih težav praktično nemogoče odpraviti zgolj s previdnostjo.

Upoštevajte, da te težave nimate, če uporabljate dedovanje vmesnika, ker ni podedovanih funkcij, ki bi vam škodile. Če Stack je vmesnik, ki ga izvajata a Simple_stack in a Monitorable_stack, potem je koda veliko bolj robustna.

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