Programmēšana

Kāpēc pagarina, tas ir ļauns

The pagarina atslēgvārds ir ļauns; varbūt ne Čārlza Mensona līmenī, bet pietiekami slikts, ka to vajadzētu izvairīties, kad vien iespējams. Četru banda Dizaina modeļi grāmatā ilgi tiek apspriesta ieviešanas mantojuma aizstāšana (pagarina) ar saskarnes mantojumu (īsteno).

Labi dizaineri lielāko daļu sava koda raksta saskarnēs, nevis konkrētās bāzes klasēs. Šajā rakstā ir aprakstīts kāpēc dizaineriem ir tādi nepāra paradumi, kā arī tiek ieviesti daži uz saskarni balstīti programmēšanas pamati.

Saskarnes pret klasēm

Reiz es apmeklēju Java lietotāju grupas sanāksmi, kurā Džeimss Goslings (Java izgudrotājs) bija galvenais runātājs. Neaizmirstamās jautājumu un atbilžu sesijas laikā kāds viņam jautāja: "Ja jūs varētu atkal veikt Java, ko jūs mainītu?" "Es pamestu stundas," viņš atbildēja. Pēc tam, kad smiekli norima, viņš paskaidroja, ka patiesā problēma nav klases per se, bet gan īstenošanas mantojums pagarina attiecības). Saskarnes mantojums ( īsteno vēlams. Kad vien iespējams, jums vajadzētu izvairīties no iedzimtas ieviešanas.

Zaudēt elastību

Kāpēc jums vajadzētu izvairīties no iedzimtas ieviešanas? Pirmā problēma ir tā, ka nepārprotami lietojot konkrētus klašu nosaukumus, jūs tiekat ieslēgti konkrētās realizācijās, lieki apgrūtinot izmaiņas līnijā.

Mūsdienu Agile attīstības metodoloģiju pamatā ir paralēlas projektēšanas un izstrādes koncepcija. Jūs sākat programmēt, pirms pilnībā norādāt programmu. Šis paņēmiens ir pretrunā ar tradicionālo gudrību - ka dizainam jābūt pilnīgam pirms programmēšanas sākuma -, taču daudzi veiksmīgi projekti ir pierādījuši, ka šādā veidā augstas kvalitātes kodu var izstrādāt ātrāk (un rentablāk) nekā ar tradicionālo cauruļvadu metodi. Paralēlās attīstības pamatā tomēr ir elastības jēdziens. Jums ir jāraksta kods tā, lai jūs varētu pēc iespējas nesāpīgāk iekļaut jaunatklātās prasības esošajā kodā.

Tā vietā, lai ieviestu jūsu funkcijas varenība nepieciešams, jūs ieviešat tikai tās funkcijas, kuras jūs izmantojat noteikti vajadzība, bet tādā veidā, kas pielāgojas pārmaiņām. Ja jums nav šīs elastības, paralēla attīstība vienkārši nav iespējama.

Programmēšana saskarnēs ir elastīgas struktūras pamatā. Lai saprastu, kāpēc, apskatīsim, kas notiek, ja tos neizmantojat. Apsveriet šādu kodu:

f () {LinkedList saraksts = jauns LinkedList (); //... g (saraksts); } g (LinkedList saraksts) {list.add (...); g2 (saraksts)} 

Tagad pieņemsim, ka ir parādījusies jauna prasība ātrai meklēšanai, tāpēc LinkedList nedarbojas. Jums tas jāaizstāj ar a HashSet. Esošajā kodā šīs izmaiņas nav lokalizētas, jo jums ir jāmaina ne tikai f () bet arī g () (kas prasa a LinkedList arguments), un jebko g () nodod sarakstu.

Pārrakstot kodu šādi:

f () {Kolekcijas saraksts = jauns LinkedList (); //... g (saraksts); } g (kolekcijas saraksts) {list.add (...); g2 (saraksts)} 

ļauj mainīt saistīto sarakstu uz jaukšanas tabulu, vienkārši aizstājot jauns LinkedList () ar jauns HashSet (). Tieši tā. Citas izmaiņas nav nepieciešamas.

Kā citu piemēru salīdziniet šo kodu:

f () {Kolekcija c = jauna HashSet (); //... g (c); } g (kolekcija c) {par (Iterator i = c.iterator (); i.hasNext ();) do_somet__ithith (i.next ()); } 

uz šo:

f2 () {Kolekcija c = new HashSet (); //... g2 (c.iterators ()); } g2 (Iterators i) {while (i.hasNext ();) do_somet__with (i.next ()); } 

The g2 () metode tagad var šķērsot Kolekcija atvasinājumi, kā arī atslēgu un vērtību saraksti, kurus varat iegūt no a Karte. Faktiski jūs varat rakstīt atkārtotājus, kas ģenerē datus, nevis šķērso kolekciju. Varat rakstīt atkārtotājus, kas informāciju no testa sastatnēm vai faila padod programmai. Šeit ir milzīga elastība.

Savienošana

Svarīgāka problēma ir iedzimta ieviešana sakabe- nevēlama vienas programmas daļas paļaušanās uz citu daļu. Globālie mainīgie ir klasisks piemērs tam, kāpēc spēcīga savienošana rada nepatikšanas. Piemēram, mainot globālā mainīgā veidu, visas funkcijas, kas izmanto mainīgo (t.i., ir savienots var mainīties), tāpēc viss šis kods ir jāpārbauda, ​​jāpārveido un jāpārbauda no jauna. Turklāt visas funkcijas, kurās tiek izmantots mainīgais, ir saistītas ar mainīgo. Tas ir, viena funkcija var nepareizi ietekmēt citas funkcijas uzvedību, ja mainīgā vērtība tiek mainīta neērtā laikā. Šī problēma ir īpaši briesmīga daudzlīniju programmās.

Kā dizaineram jums jācenšas samazināt saiknes attiecības. Jūs nevarat pilnībā novērst sasaisti, jo metodes izsaukums no vienas klases objekta uz citas objektu ir brīvas savienošanas veids. Jūs nevarat izveidot programmu bez dažu savienojumu. Neskatoties uz to, jūs varat ievērojami samazināt sasaisti, verdziski izpildot OO (objektorientētus) priekšrakstus (vissvarīgākais ir tas, ka objekta ieviešana ir pilnībā jāslēpj no objektiem, kuri to izmanto). Piemēram, objekta instances mainīgajiem (dalībnieku laukiem, kas nav konstanti) vienmēr jābūt Privāts. Periods. Bez izņēmumiem. Kādreiz. Es to domāju. (Jūs laiku pa laikam varat izmantot aizsargāts metodes efektīvi, bet aizsargāts instances mainīgie ir negantība.) Nekad nevajadzētu izmantot get / set funkcijas tā paša iemesla dēļ - tie ir tikai pārāk sarežģīti veidi, kā padarīt lauku publisku (lai gan piekļuves funkcijas, kas atgriež pilnīgi izpūstus objektus, nevis pamata tipa vērtību, ir saprātīgi situācijās, kad atgrieztā objekta klase ir galvenā abstrakcija dizainā).

Es šeit neesmu pedantisks. Esmu savā darbā atradis tiešu korelāciju starp manas OO pieejas stingrību, ātru koda izstrādi un vieglu koda uzturēšanu. Ikreiz, kad pārkāpju centrālo OO principu, piemēram, slēpjas ieviešana, es galu galā pārrakstu šo kodu (parasti tāpēc, ka kodu nav iespējams atkļūdot). Man nav laika pārrakstīt programmas, tāpēc es ievēroju noteikumus. Manas rūpes ir pilnīgi praktiskas - tīrības labad mani neinteresē tīrība.

Trauslā bāzes klases problēma

Tagad piemērosim savienojuma jēdzienu mantojumam. Īstenošanas-mantošanas sistēmā, kas izmanto pagarina, atvasinātās klases ir ļoti cieši saistītas ar bāzes klasēm, un šī ciešā saikne ir nevēlama. Lai aprakstītu šo uzvedību, dizaineri ir izmantojuši monikeru "trauslo pamatklases problēmu". Bāzes klases tiek uzskatītas par trauslām, jo ​​jūs varat modificēt bāzes klasi šķietami drošā veidā, taču šī jaunā rīcība, ja to pārmanto atvasinātās klases, var izraisīt atvasināto klašu nepareizu darbību. Jūs nevarat pateikt, vai bāzes klases maiņa ir droša, vienkārši pārbaudot bāzes klases metodes atsevišķi; jums jāaplūko (un jāpārbauda) arī visas atvasinātās klases. Turklāt jums ir jāpārbauda viss kods izmanto gan bāzes klases un arī atvasinātas klases objekti, jo šo kodu var arī salauzt jaunā uzvedība. Vienkāršas izmaiņas galvenajā bāzes klasē var padarīt visu programmu nederīgu.

Apskatīsim trauslās bāzes klases un bāzes klases savienošanas problēmas kopā. Šī klase paplašina Java ArrayList klasei, lai tā izturētos kā kaudze:

klases kaudze paplašina 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 [] raksti) {for (int i = 0; i <raksti.length; ++ i) push (raksti [i]); }} 

Pat tik vienkāršai klasei kā šī ir problēmas. Apsveriet, kas notiek, ja lietotājs izmanto mantojumu un izmanto ArrayList's skaidrs () metode visu izlaist no kaudzes:

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

Kods ir veiksmīgi sastādīts, bet, tā kā bāzes klase neko nezina par kaudzes rādītāju, Kaudze objekts tagad ir nedefinētā stāvoklī. Nākamais zvans uz virzīt () jauno vienumu ievieto indeksā 2 ( stack_pointerpašreizējā vērtība), tāpēc kaudzē faktiski ir trīs elementi - divi apakšējie ir atkritumi. (Java Kaudze klasei ir tieši šī problēma; nelietojiet to.)

Ir paredzēts viens nevēlamās metodes-mantojuma problēmas risinājums Kaudze lai ignorētu visus ArrayList metodes, kas var modificēt masīva stāvokli, tāpēc ignorēšana vai nu pareizi manipulē ar kaudzes rādītāju, vai arī rada izņēmumu. (The removeRange () metode ir labs kandidāts, lai izmestu izņēmumu.)

Šai pieejai ir divi trūkumi. Pirmkārt, ja jūs visu ignorējat, bāzes klasei patiešām vajadzētu būt saskarnei, nevis klasei. Īstenošanas mantošanai nav jēgas, ja neizmantojat kādu no mantotajām metodēm. Otrkārt, un vēl svarīgāk, jūs nevēlaties, lai kaudze atbalstītu visus ArrayList metodes. Tas nepatīkams removeRange () metode, piemēram, nav noderīga. Vienīgais saprātīgais veids, kā ieviest bezjēdzīgu metodi, ir likt tai mest izņēmumu, jo to nekad nevajadzētu izsaukt. Šī pieeja efektīvi pārceļ to, kas būtu kompilēšanas laika kļūda, izpildlaikā. Nav labi. Ja metode vienkārši nav deklarēta, kompilators izsauc kļūdu, kas nav atrasta. Ja metode ir, bet ir izņēmums, jūs nezināt par zvanu, kamēr programma faktiski nedarbojas.

Labāks bāzes klases jautājuma risinājums ir datu struktūras iekapsulēšana, nevis mantojuma izmantošana. Šeit ir jauna un uzlabota versijas versija Kaudze:

klases kaudze {private int stack_pointer = 0; privāts ArrayList the_data = jauns ArrayList (); public void push (Object article) {the_data.add (stack_pointer ++, article); } public Object pop () {return the_data.remove (--stack_pointer); } public void push_many (Object [] raksti) {for (int i = 0; i <o.length; ++ i) push (raksti [i]); }} 

Pagaidām tas ir labi, bet apsveriet trauslo bāzes klases jautājumu. Pieņemsim, ka vēlaties izveidot variantu Kaudze kas izseko maksimālo kaudzes izmēru noteiktā laika periodā. Viena iespējamā ieviešana varētu izskatīties šādi:

klase Monitorable_stack paplašina Stack {private int high_water_mark = 0; private int current_size; public void push (Object article) {if (++ current_size> high_water_mark) high_water_mark = current_size; super.push (raksts); } public Object pop () {--current_size; atgriezties super.pop (); } public int maximum_size_so_far () {return high_water_mark; }} 

Šī jaunā klase vismaz kādu laiku darbojas labi. Diemžēl kods izmanto faktu, ka push_many () veic savu darbu, piezvanot virzīt (). Sākumā šī detaļa nešķiet slikta izvēle. Tas vienkāršo kodu, un jūs iegūstat atvasināto klases versiju virzīt (), pat ja Monitorable_stack ir pieejams caur a Kaudze atsauce, tāpēc high_water_mark atjauninājumi pareizi.

Kādā jaukā dienā kāds var palaist profilētāju un pamanīt Kaudze nav tik ātri, kā varētu būt, un tiek ļoti izmantots. Jūs varat pārrakstīt Kaudze tāpēc tas neizmanto ArrayList un attiecīgi uzlabot Kaudzesniegumu. Lūk, jaunā liesā un vidējā versija:

klases kaudze {private int stack_pointer = -1; privāts objekts [] kaudze = jauns objekts [1000]; public void push (Objekta raksts) {apgalvot kaudzes_izzinējs = 0; atgriešanās kaudze [stack_pointer--]; } public void push_many (Object [] raksti) {apgalvot (stack_pointer + raksti.length) <stack.length; System.arraycopy (raksti, 0, kaudze, kaudzes rādītājs + 1, raksti. Garums); stack_pointer + = raksti.garums; }} 

Ievērojiet to push_many () vairs nezvana virzīt () vairākas reizes - tas veic bloku pārsūtīšanu. Jaunā versija Kaudze darbojas labi; patiesībā tā ir labāk nekā iepriekšējā versija. Diemžēl Monitorable_stack atvasināta klase nav strādāt vairs, jo tas nepareizi izsekos kaudzes lietošanu, ja push_many () tiek saukts (atvasinātās klases versija virzīt () mantotais vairs nesauc push_many () metodi, tātad push_many () vairs neatjaunina high_water_mark). Kaudze ir trausla bāzes klase. Kā izrādās, praktiski nav iespējams novērst šāda veida problēmas, vienkārši rīkojoties uzmanīgi.

Ņemiet vērā, ka jums nav šīs problēmas, ja izmantojat saskarnes mantojumu, jo nav iedzimtas funkcionalitātes, kas varētu jums nodarīt sliktu. Ja Kaudze ir saskarne, kuru ievieš abi Simple_stack un a Monitorable_stack, tad kods ir daudz izturīgāks.

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