Ohjelmointi

Miksi jatkuu, on paha

ulottuu avainsana on paha; ehkä ei Charles Mansonin tasolla, mutta tarpeeksi huono, että se olisi vältettävä aina kun mahdollista. Neljän jengi Suunnittelumalleja kirjassa keskustellaan pitkälti toteutusperinnön korvaamisesta (ulottuu) käyttöliittymän perinnöllä (työvälineet).

Hyvät suunnittelijat kirjoittavat suurimman osan koodistaan ​​käyttöliittyminä, ei konkreettisina perusluokkina. Tässä artikkelissa kuvataan miksi Suunnittelijoilla on niin outoja tapoja, ja he esittävät myös muutaman käyttöliittymäpohjaisen ohjelmoinnin perustiedot.

Liitännät vs. luokat

Osallistuin kerran Java-käyttäjäryhmätapaamiseen, jossa James Gosling (Java-keksijä) oli esillä oleva puhuja. Ikimuistoisen Q & A-istunnon aikana joku kysyi häneltä: "Jos voisit tehdä Java uudestaan, mitä muuttaisit?" "Jättäisin kurssit pois", hän vastasi. Naurun loppuessa hän selitti, että todellinen ongelma ei ollut luokkia sinänsä, vaan pikemminkin toteutusperintö ( ulottuu suhde). Liitännän perintö ( työvälineet suhde) on parempi. Sinun tulisi välttää toteutuksen perimistä aina kun mahdollista.

Menettää joustavuutta

Miksi sinun tulisi välttää toteutusperintöä? Ensimmäinen ongelma on, että konkreettisten luokkien nimien nimenomainen käyttö lukitsee sinut tiettyihin toteutuksiin, mikä tekee alamuutoksista tarpeettomasti vaikeita.

Nykyaikaisten ketterien kehitysmenetelmien ytimessä on rinnakkaisen suunnittelun ja kehityksen käsite. Aloitat ohjelmoinnin, ennen kuin määrität ohjelman kokonaan. Tämä tekniikka on perinteisen viisauden edessä - suunnittelun pitäisi olla valmis ennen ohjelmoinnin aloittamista -, mutta monet onnistuneet projektit ovat osoittaneet, että korkealaatuista koodia voidaan kehittää nopeammin (ja kustannustehokkaammin) tällä tavoin kuin perinteisellä putkilinjalla. Rinnakkaiskehityksen ytimessä on kuitenkin joustavuuden käsite. Sinun on kirjoitettava koodisi siten, että voit sisällyttää äskettäin löydetyt vaatimukset olemassa olevaan koodiin mahdollisimman kivuttomasti.

Sen sijaan, että toteuttaisi ominaisuuksia voi tarvitset, otat käyttöön vain ne ominaisuudet, jotka olet ehdottomasti tarvetta, mutta tavalla, joka mukautuu muutokseen. Jos sinulla ei ole tätä joustavuutta, rinnakkainen kehitys ei yksinkertaisesti ole mahdollista.

Ohjelmointi rajapinnoille on joustavan rakenteen ydin. Jos haluat nähdä miksi, katsotaan mitä tapahtuu, kun et käytä niitä. Harkitse seuraavaa koodia:

f () {LinkedList-luettelo = uusi LinkedList (); //... g (luettelo); } g (LinkedList-luettelo) {list.add (...); g2 (luettelo)} 

Oletetaan nyt, että nopea haku on uusi vaatimus, joten LinkedList ei toimi. Sinun on korvattava se a: lla HashSet. Nykyisessä koodissa muutosta ei ole lokalisoitu, koska sinun on muutettava paitsi f () mutta myös g () (joka vie a LinkedList väite), ja mitä tahansa g () välittää luettelon.

Kirjoita koodi uudestaan ​​näin:

f () {Kokoelista = uusi LinkedList (); //... g (luettelo); } g (Kokoelmaluettelo) {list.add (...); g2 (luettelo)} 

mahdollistaa linkitetyn luettelon muuttamisen hash-taulukoksi yksinkertaisesti korvaamalla uusi LinkedList () kanssa uusi HashSet (). Se siitä. Muita muutoksia ei tarvita.

Toisena esimerkkinä vertaa tätä koodia:

f () {Kokoelma c = uusi HashSet (); //... g (c); } g (Kokoelma c) {for (Iterator i = c.iterator (); i.hasNext ();) tee jotain (i.sext ()); } 

tähän:

f2 () {Kokoelma c = uusi HashSet (); //... g2 (s.siteraattori ()); } g2 (Iterator i) {while (i.hasNext ();) tee jotain (i.sext ()) kanssa; } 

g2 () menetelmä voi nyt kulkea Kokoelma johdannaiset sekä avain- ja arvoluettelot, jotka saat a Kartta. Itse asiassa voit kirjoittaa iteraattoreita, jotka tuottavat dataa kokoelman kulkemisen sijaan. Voit kirjoittaa iteraattoreita, jotka syöttävät tietoja testitelineestä tai tiedostosta ohjelmaan. Täällä on valtava joustavuus.

Kytkentä

Ratkaisevampi ongelma toteutuksen perinnössä on kytkentä—Ohjelman yhden osan ei-toivottu luotettavuus toiseen osaan. Globaalit muuttujat tarjoavat klassisen esimerkin siitä, miksi vahva kytkentä aiheuttaa ongelmia. Jos muutat esimerkiksi globaalin muuttujan tyyppiä, kaikki muuttujaa käyttävät toiminnot (eli ovat kytketty muuttujaan) voi vaikuttaa, joten kaikki tämä koodi on tutkittava, muokattava ja testattava uudelleen. Lisäksi kaikki muuttujaa käyttävät toiminnot on kytketty toisiinsa muuttujan kautta. Toisin sanoen yksi toiminto saattaa vaikuttaa väärin toisen toiminnon käyttäytymiseen, jos muuttujan arvoa muutetaan hankalana aikana. Tämä ongelma on erityisen kamala monisäikeisissä ohjelmissa.

Suunnittelijana sinun tulisi pyrkiä minimoimaan kytkentäsuhteet. Kytkennää ei voida poistaa kokonaan, koska menetelmän kutsu yhden luokan objektista toisen objektiin on eräänlainen löysä kytkentä. Sinulla ei voi olla ohjelmaa ilman kytkentää. Siitä huolimatta voit minimoida kytkennän huomattavasti noudattamalla orjuudella OO (olio-suuntautuneita) määräyksiä (tärkeintä on, että objektin toteutus on täysin piilotettu sitä käyttäviltä esineiltä). Esimerkiksi objektin ilmentymämuuttujien (jäsenkentät, jotka eivät ole vakioita) pitäisi olla aina yksityinen. Aika. Ei poikkeuksia. Koskaan. Tarkoitan sitä. (Voit joskus käyttää suojattu menetelmiä tehokkaasti, mutta suojattu ilmentymämuuttujat ovat kauhistus.) Älä koskaan käytä get / set-funktioita samasta syystä - ne ovat vain liian monimutkaisia ​​tapoja tehdä kentästä julkinen (vaikka pääsyfunktiot, jotka palauttavat täysimittaisia ​​objekteja perustyyppisen arvon sijaan, ovat kohtuullinen tilanteissa, joissa palautetun kohteen luokka on suunnittelun avain abstraktio).

En ole pedanttinen täällä. Olen havainnut omassa työssäni suoran korrelaation OO-lähestymistavan tiukkuuden, nopean koodikehityksen ja koodin helpon ylläpidon välillä. Aina kun rikkoo keskeistä OO-periaatetta, kuten toteutuksen piilottaminen, kirjoitan sen koodin uudestaan ​​(yleensä siksi, että koodia on mahdoton virheenkorjaus). Minulla ei ole aikaa kirjoittaa ohjelmia uudelleen, joten noudatan sääntöjä. Huoleni on täysin käytännöllinen - minua ei kiinnosta puhtaus puhtauden vuoksi.

Hauras perusluokan ongelma

Sovelletaan nyt yhdistämisen käsitettä perintöön. Toteutus-perintöjärjestelmässä, joka käyttää ulottuu, johdetut luokat on kytketty hyvin tiukasti perusluokkiin, ja tämä läheinen yhteys ei ole toivottavaa. Suunnittelijat ovat käyttäneet monikertaa "hauras perusluokan ongelma" kuvaamaan tätä käyttäytymistä. Perusluokkia pidetään hauraina, koska voit muokata perusluokkaa näennäisesti turvallisella tavalla, mutta tämä uusi käyttäytyminen, kun johdetut luokat perivät, saattaa aiheuttaa johdettujen luokkien toimintahäiriön. Et voi kertoa, onko perusluokan muutos turvallinen, yksinkertaisesti tutkimalla perusluokan menetelmiä erikseen; sinun on tarkasteltava (ja testattava) myös kaikkia johdettuja luokkia. Lisäksi sinun on tarkistettava kaikki koodit käyttää molemmat perusluokan ja myös johdetun luokan objektit, koska uusi käytös saattaa myös rikkoa tämän koodin. Yksinkertainen muutos avainperusluokkaan voi tehdä koko ohjelman käyttökelvottomaksi.

Tarkastellaan hauraita perusluokan ja perusluokan kytkentäongelmia yhdessä. Seuraava luokka laajentaa Java-tiedostoja ArrayList luokassa, jotta se käyttäytyisi pinona:

luokan pino laajentaa 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 [] artikkelit) {for (int i = 0; i <artikkelit.pituus; ++ i) push (artikkelit [i]); }} 

Jopa niin yksinkertaisella luokassa kuin tässä on ongelmia. Mieti, mitä tapahtuu, kun käyttäjä hyödyntää perintöä ja käyttää ArrayListon asia selvä() menetelmä kaiken pudottamiseksi pinosta:

Pino a_stack = uusi pino (); a_stack.push ("1"); a_stack.push ("2"); a_stack.clear (); 

Koodi kääntyy onnistuneesti, mutta koska perusluokka ei tiedä mitään pinon osoittimesta, Pino esine on nyt määrittelemättömässä tilassa. Seuraava puhelu numeroon työntää() asettaa uuden kohteen hakemistoon 2 ( pino_osoitinnykyinen arvo), joten pinossa on tosiasiallisesti kolme elementtiä - kaksi alinta ovat roskaa. (Java Pino luokassa on juuri tämä ongelma; älä käytä sitä.)

Yksi ratkaisu ei-toivottuun menetelmä-perintöongelmaan on Pino ohittaa kaikki ArrayList menetelmiä, jotka voivat muuttaa taulukon tilaa, joten ohitukset joko manipuloivat pinon osoitinta oikein tai heittävät poikkeuksen. ( removeRange () menetelmä on hyvä ehdokas heittää poikkeus.)

Tällä lähestymistavalla on kaksi haittaa. Ensinnäkin, jos ohitat kaiken, perusluokan pitäisi todella olla käyttöliittymä, ei luokka. Ei ole mitään järkeä toteutuksen perinnössä, jos et käytä mitään perittyjä menetelmiä. Toiseksi ja mikä tärkeintä, et halua, että pino tukee kaikkia ArrayList menetelmiä. Se ärsyttävä removeRange () menetelmä ei ole hyödyllinen. Ainoa järkevä tapa toteuttaa hyödytön menetelmä on saada se tekemään poikkeus, koska sitä ei pitäisi koskaan kutsua. Tämä lähestymistapa siirtää käännösaikavirheen tehokkaasti ajonaikaiseksi. Ei hyvä. Jos menetelmää ei yksinkertaisesti ilmoiteta, kääntäjä laukaisee menetelmän, jota ei löydy -virheen. Jos menetelmä on olemassa, mutta heittää poikkeuksen, et saa tietoa puhelusta ennen kuin ohjelma todella toimii.

Parempi ratkaisu perusluokan ongelmaan on tietorakenteen kapseloiminen perimisen sijaan. Tässä on uusi ja parannettu versio Pino:

luokan pino {private int stack_pointer = 0; yksityinen ArrayList the_data = uusi 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 [] artikkelit) {for (int i = 0; i <o.length; ++ i) push (artikkelit [i]); }} 

Toistaiseksi niin hyvä, mutta ota huomioon hauras perusluokan asia. Oletetaan, että haluat luoda muunnelman Pino joka seuraa suurinta pinon kokoa tietyllä ajanjaksolla. Yksi mahdollinen toteutus saattaa näyttää tältä:

class Monitorable_stack laajentaa Stack {private int high_water_mark = 0; yksityinen int nykyinen_koko; public void push (Object Article) {if (++ current_size> high_water_mark) high_water_mark = current_size; super.push (artikkeli); } public Object pop () {--current_size; palaa super.pop (); } public int maximum_size_so_far () {return high_water_mark; }} 

Tämä uusi luokka toimii hyvin, ainakin jonkin aikaa. Valitettavasti koodi hyödyntää sitä push_many () tekee työnsä soittamalla työntää(). Aluksi tämä yksityiskohta ei näytä olevan huono valinta. Se yksinkertaistaa koodia, ja saat johdetun luokan version työntää(), vaikka Monitorable_stack pääsee a Pino viite, joten high_water_mark päivitykset oikein.

Eräänä hyvänä päivänä joku saattaa ajaa profiloijan ja huomata sen Pino ei ole niin nopea kuin voisi olla ja sitä käytetään paljon. Voit kirjoittaa Pino joten se ei käytä ArrayList ja siten parantaa Pinosuorituskyky. Tässä on uusi vähärasvainen versio:

luokan pino {private int stack_pointer = -1; yksityinen objekti [] pino = uusi objekti [1000]; public void push (Objektiartikkeli) {assert stack_pointer = 0; paluupino [pino_pointer--]; } public void push_many (Object [] artikkelit) {väitä (pino_osoitin + artikkelit.pituus) <pino.pituus; System.arraycopy (artikkelit, 0, pino, pinoosoitin + 1, artikkelit.pituus); stack_pointer + = artikkelit.pituus; }} 

Huomaa, että push_many () ei enää soita työntää() useita kertoja - se suorittaa lohkosiirron. Uusi versio Pino toimii hyvin; itse asiassa se on paremmin kuin edellinen versio. Valitettavasti Monitorable_stack johdettu luokka ei toimi enää, koska se ei seuraa pinon käyttöä oikein, jos push_many () kutsutaan (johdetun luokan versio työntää() peritty ei enää kutsu push_many () menetelmä, niin push_many () ei enää päivitä high_water_mark). Pino on hauras perusluokka. Kuten käy ilmi, on lähes mahdotonta poistaa tämän tyyppisiä ongelmia yksinkertaisesti olemalla varovainen.

Huomaa, että sinulla ei ole tätä ongelmaa, jos käytät käyttöliittymän perintöä, koska sinulla ei ole perittyjä toimintoja, jotka menisivät sinulle huonoksi. Jos Pino on käyttöliittymä, jonka molemmat toteuttavat a Simple_stack ja a Monitorable_stack, niin koodi on paljon vankempi.