Programavimas

Kodėl tęsiasi, yra blogis

tęsiasi raktinis žodis yra blogis; gal ne Charleso Mansono lygiu, bet pakankamai blogas, kad jo reikėtų vengti, kai tik įmanoma. Keturių gauja Dizaino modeliai knygoje ilgai diskutuojama apie įgyvendinimo paveldėjimo pakeitimą (tęsiasi) su sąsajos paveldėjimu (padargai).

Geri dizaineriai didžiąją dalį savo kodo rašo sąsajomis, o ne konkrečiomis pagrindinėmis klasėmis. Šiame straipsnyje aprašoma kodėl dizaineriai turi tokius keistus įpročius, taip pat pristato kelis sąsaja pagrįstus programavimo pagrindus.

Sąsajos ir klasės

Kartą dalyvavau „Java“ vartotojų grupės susitikime, kuriame kalbėjo Jamesas Goslingas („Java“ išradėjas). Per įsimintiną klausimų ir atsakymų sesiją kažkas jo paklausė: "Jei galėtumėte dar kartą atlikti" Java ", ką pakeistumėte?" - Aš palikčiau pamokas, - atsakė jis. Po to, kai juokas nuslūgo, jis paaiškino, kad tikroji problema buvo ne klasės per se, o veikiau paveldėjimas tęsiasi santykiai). Sąsajos paveldėjimas ( padargai santykiai). Turėtumėte vengti įgyvendinimo paveldėjimo, kai tik įmanoma.

Lošimo praradimas

Kodėl turėtumėte vengti įgyvendinimo paveldėjimo? Pirmoji problema yra ta, kad aiškus konkrečių klasių pavadinimų naudojimas įtraukia jus į konkrečius diegimus, todėl be reikalo sunku atlikti pakeitimus.

Šiuolaikinių „Agile“ kūrimo metodikų esmė yra paralelinio projektavimo ir kūrimo samprata. Jūs pradedate programuoti prieš visiškai nurodydami programą. Ši technika skamba atsižvelgiant į tradicinę išmintį - kad dizainas turi būti baigtas prieš pradedant programuoti -, tačiau daugelis sėkmingų projektų įrodė, kad tokiu būdu galite greitai (ir ekonomiškai efektyviau) sukurti aukštos kokybės kodą, nei naudodamiesi tradiciniu metodu. Tačiau lygiagrečios plėtros esmė yra lankstumo sąvoka. Turite parašyti savo kodą taip, kad galėtumėte neskausmingai įtraukti naujai atrastus reikalavimus į esamą kodą.

Užuot įgyvendinę savybes gali poreikį, jūs įdiegiate tik tas funkcijas, kurias jūs tikrai poreikį, tačiau taip, kad prisitaikytų prie pokyčių. Jei neturite tokio lankstumo, lygiagreti plėtra tiesiog neįmanoma.

Programavimas sąsajose yra lanksčios struktūros pagrindas. Norėdami sužinoti, kodėl, pažiūrėkime, kas nutinka, kai jų nenaudojate. Apsvarstykite šį kodą:

f () {LinkedList list = new LinkedList (); //... g (sąrašas); } g („LinkedList“ sąrašas) {sąrašas.add (...); g2 (sąrašas)} 

Dabar tarkime, kad atsirado naujas greito paieškos reikalavimas, todėl „LinkedList“ neveikia. Turite jį pakeisti a „HashSet“. Esamame kode šis pakeitimas nėra lokalizuotas, nes jūs turite modifikuoti ne tik f () bet ir g () (tam reikia a „LinkedList“ argumentas), ir bet kas g () perduoda sąrašą.

Perrašykite kodą taip:

f () {Kolekcijos sąrašas = naujas „LinkedList“ (); //... g (sąrašas); } g (kolekcijos sąrašas) {list.add (...); g2 (sąrašas)} 

leidžia susietą sąrašą pakeisti į maišos lentelę paprasčiausiai pakeičiant naujas „LinkedList“ () su naujas „HashSet“ (). Viskas. Jokių kitų pakeitimų nereikia.

Kaip kitą pavyzdį palyginkite šį kodą:

f () {Kolekcija c = naujas HashSet (); //... g (c); } g (c rinkinys) {for (Iterator i = c.iterator (); i.hasNext ();) do_shalhing_with (i.next ()); } 

šiam:

f2 () {rinkinys c = naujas „HashSet“ (); //... g2 (c.iteratorius ()); } g2 (Iterator i) {while (i.hasNext ();) daryti_kažką su (i. Next ()); } 

g2 () metodas dabar gali kirsti Kolekcija dariniai, taip pat raktų ir vertybių sąrašai, kuriuos galite gauti iš a Žemėlapis. Tiesą sakant, galite rašyti iteratorius, generuojančius duomenis, užuot važiavę per kolekciją. Galite rašyti iteratorius, kurie programai tiekia informaciją iš bandomųjų pastolių ar failo. Čia yra didžiulis lankstumas.

Sukabinimas

Svarbesnė įgyvendinimo paveldėjimo problema yra sukabinimas- nepageidaujamas vienos programos dalies pasikliavimas kita. Visuotiniai kintamieji yra klasikinis pavyzdys, kodėl stiprus susiejimas sukelia problemų. Pavyzdžiui, pakeitus visuotinio kintamojo tipą, visos funkcijos, naudojančios kintamąjį (t. Y., Yra susieta kintamajam) gali būti paveiktas, todėl visas šis kodas turi būti ištirtas, modifikuotas ir išbandytas iš naujo. Be to, visos kintamąjį naudojančios funkcijos yra sujungtos viena su kita per kintamąjį. Tai yra, viena funkcija gali neteisingai paveikti kitos funkcijos elgseną, jei kintamojo vertė bus pakeista nepatogiu metu. Ši problema yra ypač baisi daugialypėse programose.

Kaip dizaineris, turėtumėte stengtis sumažinti susiejimo santykius. Negalite visiškai pašalinti susiejimo, nes metodo iškvietimas iš vienos klasės objekto į kitos objektą yra laisvos jungties forma. Negalite turėti programos be tam tikros jungties. Nepaisant to, galite labai sumažinti susiejimą, vergiškai laikydamiesi OO (į objektą orientuotų) nurodymų (svarbiausia yra tai, kad objekto įgyvendinimas turėtų būti visiškai paslėptas nuo jį naudojančių objektų). Pavyzdžiui, objekto egzemplioriaus kintamieji (narių laukai, kurie nėra konstantos), visada turėtų būti privatus. Laikotarpis. Jokių išimčių. Kada nors. Aš rimtai. (Retkarčiais galite naudoti saugomi metodus efektyviai, bet saugomi egzempliorių kintamieji yra šlykštumas.) Niekada neturėtumėte naudoti „get / set“ funkcijų dėl tos pačios priežasties - jie tiesiog yra pernelyg sudėtingi būdai, kaip lauką paviešinti (nors prieigos funkcijos, kurios grąžina ne tik pagrindinio tipo, bet ir pilnaverčius objektus pagrįstas tais atvejais, kai grąžinamo objekto klasė yra pagrindinė projekto abstrakcija).

Čia nesu pedantiškas. Savo darbe radau tiesioginę koreliaciją tarp OO požiūrio griežtumo, greito kodo kūrimo ir lengvo kodo priežiūros. Kiekvieną kartą, kai pažeidžiu centrinį OO principą, pvz., Slepiasi įgyvendinimas, galų gale perrašau tą kodą (dažniausiai todėl, kad kodo neįmanoma derinti). Neturiu laiko perrašyti programų, todėl laikausi taisyklių. Mano rūpestis yra visiškai praktiškas - aš nesu suinteresuotas grynumu dėl grynumo.

Trapi bazinės klasės problema

Dabar pritaikykime paveldėjimo sąvoką susiejimas. Įgyvendinimo-paveldėjimo sistemoje, kuri naudoja tęsiasi, išvestinės klasės yra labai glaudžiai susijusios su pagrindinėmis klasėmis, ir šis artimas ryšys yra nepageidaujamas. Dizaineriai apibūdino šį elgesį monikeriu „trapi bazinės klasės problema“. Pagrindinės klasės laikomos pažeidžiamomis, nes galite modifikuoti bazinę klasę iš pažiūros saugiu būdu, tačiau dėl šio naujo elgesio, paveldėjus išvestinėms klasėms, išvestinės klasės gali sutrikti. Negalite pasakyti, ar bazinės klasės pakeitimas yra saugus, paprasčiausiai nagrinėdami bazinės klasės metodus atskirai; taip pat turite apžvelgti (ir išbandyti) visas išvestines klases. Be to, turite patikrinti visą tą kodą naudoja tiek bazinės klasės ir išvestinės klasės objektai, nes naujas elgesys šį kodą taip pat gali sugadinti. Paprastas pagrindinės klasės pakeitimas gali padaryti visą programą neveiksnia.

Panagrinėkime trapias bazinės klasės ir bazinės klasės sujungimo problemas kartu. Ši klasė praplečia „Java“ „ArrayList“ klasę, kad ji elgtųsi kaip kaminas:

„Class Stack“ prailgina „ArrayList“ {private int stack_pointer = 0; public void push (Object article) {add (stack_pointer ++, article); } public Object pop () {return remove (--stack_pointer); } public void push_many (Object [] straipsniai) {for (int i = 0; i <articles.length; ++ i) push (articles [i]); }} 

Net tokia paprasta klasė kaip ši turi problemų. Apsvarstykite, kas nutinka, kai vartotojas pasinaudoja paveldėjimu ir naudojasi „ArrayList“'s aišku () metodas iššokti viską iš kamino:

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

Kodas sėkmingai sukompiliuotas, bet kadangi pagrindinė klasė nieko nežino apie rietuvės žymeklį, Sukrauti objektas dabar yra neapibrėžtos būsenos. Kitas skambutis stumti () įtraukia naują elementą į 2 indeksą ( stack_pointerdabartinė vertė), todėl kamino faktiškai yra trys elementai - du apatiniai yra šiukšlės. („Java“ Sukrauti klasė turi būtent šią problemą; nenaudokite jo.)

Yra vienas nepageidaujamos metodo paveldėjimo problemos sprendimas Sukrauti kad nepaisytų visų „ArrayList“ metodai, galintys modifikuoti masyvo būseną, todėl nepaisymai teisingai manipuliuoja kamino žymekliu arba išmeta išimtį. ( removeRange () metodas yra geras kandidatas išimčiai atmesti.)

Šis požiūris turi du trūkumus. Pirma, jei viską nepaisysite, pagrindinė klasė tikrai turėtų būti sąsaja, o ne klasė. Nėra prasmės paveldėti, jei nenaudojate jokio paveldimo būdo. Antra, ir dar svarbiau, jūs nenorite, kad krūva palaikytų visus „ArrayList“ metodai. Tas nemalonus removeRange () Pavyzdžiui, metodas nėra naudingas. Vienintelis pagrįstas būdas naudoti nenaudingą metodą yra leisti jam taikyti išimtį, nes jo niekada nereikėtų kviesti. Šis metodas efektyviai perkelia tai, kas būtų kompiliavimo laiko klaida, į vykdymo laiką. Negerai. Jei metodas paprasčiausiai nėra deklaruojamas, kompiliatorius iškelia metodo nerastą klaidą. Jei metodas yra, bet yra išimtis, apie skambutį sužinosite tik tada, kai programa tikrai paleis.

Geresnis bazinės klasės problemos sprendimas yra duomenų struktūros sujungimas, o ne paveldėjimo naudojimas. Štai nauja ir patobulinta versija Sukrauti:

class Stack {privatus int stack_pointer = 0; privatus ArrayList the_data = naujas ArrayList (); public void push (Object article) {the_data.add (stack_pointer ++, straipsnis); } public Object pop () {return the_data.remove (--stack_pointer); } public void push_many (Object [] straipsniai) {for (int i = 0; i <o.length; ++ i) push (straipsniai [i]); }} 

Kol kas viskas gerai, bet apsvarstykite trapų pagrindinės klasės klausimą. Tarkime, kad norite sukurti variantą Sukrauti kuris stebi didžiausią kamino dydį per tam tikrą laikotarpį. Vienas galimas įgyvendinimas gali atrodyti taip:

klasė „Monitorable_stack“ tęsiasi „Stack“ {private int high_water_mark = 0; privatus int dabartinis_dydis; public void push (Object article) {if (++ current_size> high_water_mark) high_water_mark = current_size; super.push (straipsnis); } public Object pop () {--current_size; grįžti super.pop (); } public int maximum_size_so_far () {return high_water_mark; }} 

Ši nauja klasė bent jau kurį laiką veikia gerai. Deja, kodas išnaudoja tai push_many () atlieka savo darbą paskambinęs stumti (). Iš pradžių ši detalė neatrodo blogas pasirinkimas. Tai supaprastina kodą ir gausite išvestinę klasės versiją stumti (), net kai „Monitorable_stack“ prieinama per a Sukrauti nuoroda, taigi high_water_mark atnaujinimai teisingai.

Vieną gražią dieną kažkas gali paleisti profilį ir pastebėti Sukrauti nėra taip greitai, kaip galėtų būti, ir yra labai naudojamas. Galite perrašyti Sukrauti todėl nenaudoja „ArrayList“ ir todėl pagerinti Sukrautipasirodymą. Štai nauja „liesa ir vidutinė“ versija:

class Stack {private int stack_pointer = -1; privatus objektas [] kaminas = naujas objektas [1000]; public void push (Object article) {teigti kamino_pointer = 0; grąžinti kaminą [stack_pointer--]; } public void push_many (Object [] straipsniai) {teigti (stack_pointer + straipsniai.length) <stack.length; System.arraycopy (straipsniai, 0, kaminas, kamino_pointeris + 1, straipsniai.ilgis); stack_pointer + = straipsniai.length; }} 

Pastebėti, kad push_many () nebeskambina stumti () kelis kartus - tai atlieka blokinį perdavimą. Nauja versija Sukrauti veikia gerai; iš tikrųjų tai yra geriau nei ankstesnė versija. Deja, „Monitorable_stack“ išvestinė klasė neturi dirbti daugiau, nes tai nebus teisingai sekti kamino naudojimą, jei push_many () vadinamas (išvestinės klasės versija stumti () nebevadina paveldėtas push_many () metodas, taigi push_many () nebeatnaujina high_water_mark). Sukrauti yra trapi bazinė klasė. Kaip paaiškėja, praktiškai neįmanoma pašalinti tokio tipo problemų paprasčiausiai atsargiai.

Atkreipkite dėmesį, kad neturite šios problemos, jei naudojate sąsajos paveldėjimą, nes nėra paveldimo funkcionalumo, kuris galėtų jums pakenkti. Jei Sukrauti yra sąsaja, kurią įgyvendina abu a „Simple_stack“ ir a „Monitorable_stack“, tada kodas yra daug tvirtesnis.