Više o geterima i postavljačima

To je 25-godišnje načelo objektno orijentiranog (OO) dizajna da provedbu objekta ne biste trebali izlagati bilo kojoj drugoj klasi u programu. Program je nepotrebno teško održavati kada izlažete implementaciju, prvenstveno zato što promjena objekta koji izlaže njegovu implementaciju zahtijeva promjene svih klasa koje koriste objekt.

Nažalost, idiom getera / postavljača za koji mnogi programeri misle da je objektno orijentiran krši ovaj temeljni princip OO u stvari. Razmotrimo primjer Moneyklase koja na sebi ima getValue()metodu koja vraća "vrijednost" u dolarima. U cijelom programu imat ćete kod poput sljedećeg:

dvostruki redTotal; Iznos novca = ...; // ... orderTotal + = iznos.getValue (); // orderTotal mora biti u dolarima

Problem s ovim pristupom je u tome što prethodni kod daje veliku pretpostavku o tome kako je Moneyklasa implementirana (da je "vrijednost" pohranjena u a double). Kôd koji pretpostavlja pretpostavke implementacije lomi se kad se implementacija promijeni. Ako, na primjer, trebate internacionalizirati svoj program kako biste podržavali valute koje nisu dolari, tada getValue()ništa značajno ne vraća. Možete dodati znak a getCurrency(), ali to bi getValue()znatno zakompliciralo sav kod koji okružuje poziv, pogotovo ako ustrajete u korištenju strategije dobivača / postavljača kako biste dobili informacije potrebne za posao. Tipična (neispravna) implementacija može izgledati ovako:

Iznos novca = ...; // ... vrijednost = iznos.getValue (); currency = iznos.getCurrency (); conversion = CurrencyTable.getConversionFactor (valuta, USDOLLARS); ukupna + = vrijednost * pretvorba; // ...

Ova je promjena prekomplicirana da bi se njome moglo upravljati automatskim refaktoringom. Štoviše, morali biste napraviti takve promjene svugdje u kodu.

Rješenje ovog problema na razini poslovne logike je obavljanje posla u objektu koji ima informacije potrebne za obavljanje posla. Umjesto da izvadite "vrijednost" da biste na njoj izveli neku vanjsku operaciju, trebali biste da Moneyklasa izvrši sve operacije povezane s novcem, uključujući pretvorbu valuta. Ispravno strukturirani objekt obrađivao bi zbroj ovako:

Novac ukupno = ...; Iznos novca = ...; total.increaseBy (iznos);

add()Način će shvatiti valutu operanda, učiniti sve potrebne pretvorbu valuta (što je, ispravno, operacija na novac ), i ažuriranje ukupno. Ako ste za početak koristili ovu strategiju objekt-koji-ima-informacija-obavlja-posao, pojam valute mogao bi se dodati u Moneyklasu bez ikakvih promjena potrebnih u kodu koji koristi Moneyobjekte. Odnosno, posao na refaktoriranju samo dolara za međunarodnu provedbu bio bi koncentriran na jednom mjestu: Moneyklasi.

Problem

Većina programera nema poteškoća u shvaćanju ovog koncepta na poslovno-logičkoj razini (iako je potreban određeni napor da se dosljedno razmišlja na taj način). Problemi se počinju pojavljivati ​​kad korisničko sučelje (UI) uđe u sliku. Problem nije u tome što za izgradnju korisničkog sučelja ne možete primijeniti tehnike poput one koju sam upravo opisao, već u tome što su mnogi programeri zatvoreni u mentalitet getera / postavljača kada je riječ o korisničkim sučeljima. Krivim ovaj problem za fundamentalno proceduralne alate za izradu koda poput Visual Basic-a i njegovih klonova (uključujući Java UI graditelje) koji vas prisiljavaju na ovaj proceduralni, getter / setter način razmišljanja.

(Digresija: Neki će od vas natuknuti na prethodnu izjavu i vrištati da se VB temelji na posvećenoj arhitekturi Model-View-Controller (MVC), pa tako i sakrosanktno. Imajte na umu da je MVC razvijen prije gotovo 30 godina. U ranim 1970-ih, najveće superračunalo bilo je ravnopravno s današnjim radnim površinama. Većina strojeva (poput DEC PDP-11) bila su 16-bitna računala, sa 64 KB memorije i radnim taktovima izmjerenim u desecima megaherca. Vaše korisničko sučelje vjerojatno je bilo snop bušenih karata. Ako ste imali dovoljno sreće da imate video terminal, možda ste koristili sustav ulazno / izlaznih (I / O) konzola temeljenih na ASCII. U proteklih smo 30 godina puno naučili. Čak Java Swing morao je zamijeniti MVC sličnom arhitekturom "odvojivog modela", prvenstveno zato što čisti MVC nedovoljno izolira slojeve korisničkog sučelja i domene.)

Dakle, definirajmo problem ukratko:

Ako objekt ne može izložiti informacije o implementaciji (metodama get / set ili na bilo koji drugi način), onda razumljivo da objekt mora nekako stvoriti vlastito korisničko sučelje. Odnosno, ako je način na koji su predstavljeni atributi objekta skriven od ostatka programa, tada te atribute ne možete izdvojiti da biste izgradili korisničko sučelje.

Usput, imajte na umu da ne skrivate činjenicu da atribut postoji. (Ovdje definiram atribut kao bitnu karakteristiku predmeta.) Znate da Employeemora imati atribut plaće ili nadnice, inače ne bi bio Employee. (Bilo bi to Person, a Volunteer, a Vagrantili nešto drugo što nema plaću.) Ono što ne znate - ili želite znati - je kako je ta plaća predstavljena unutar objekta. To bi mogla biti double, a String, krljuštima longili binarno kodirani decimalni brojevi. To može biti atribut "sintetički" ili "izvedeni", koji se izračunava u vrijeme izvođenja (na primjer iz platnog razreda ili naziva posla ili preuzimanjem vrijednosti iz baze podataka). Iako metoda get zaista može sakriti neke od ovih detalja implementacije,kao što smo vidjeli kodMoney na primjer, ne može sakriti dovoljno.

Pa kako objekt proizvodi vlastiti korisnički interfejs i ostaje održiv? Samo najjednostavniji objekti mogu podržati nešto poput displayYourself()metode. Realistički objekti moraju:

  • Prikazuju se u različitim formatima (XML, SQL, vrijednosti odvojene zarezima itd.).
  • Prikazuju različite prikaze samih sebe (jedan prikaz može prikazati sve atribute; drugi može prikazati samo podskup atributa; a treći atribute može prikazati na drugačiji način).
  • Prikazuju se u različitim okruženjima (na primjer, na strani klijenta ( JComponent) i posluživanju klijenta (HTML)) i obrađuju i ulaz i izlaz u oba okruženja.

Neki su čitatelji mog prethodnog članka o getteru / setteru skočili do zaključka da sam zagovarao dodavanje metoda objektu kako bi se pokrile sve te mogućnosti, ali da je "rješenje" očito besmisleno. Ne samo da je rezultirajući teški objekt previše kompliciran, morat ćete ga neprestano modificirati kako bi odgovarao novim zahtjevima korisničkog sučelja. Praktično, objekt jednostavno ne može sagraditi sva moguća korisnička sučelja za sebe, ako ni zbog čega drugog, osim toga mnogi od tih korisničkih sučelja nisu niti bili zamišljeni prilikom stvaranja klase.

Izgradite rješenje

Rješenje ovog problema je odvajanje UI koda od osnovnog poslovnog objekta stavljanjem u zasebnu klasu objekata. Odnosno, trebali biste u potpunosti odvojiti neke funkcije koje bi mogle biti u objektu.

Ova bifurkacija metoda predmeta pojavljuje se u nekoliko obrazaca dizajna. Najvjerojatnije ste upoznati sa strategijom koja se koristi za razne java.awt.Containerklase za izradu izgleda. Ti bi mogao riješiti problem izgleda sa izvod rješenje: FlowLayoutPanel, GridLayoutPanel, BorderLayoutPanel, itd, ali da mandati previše nastavu i puno dvostruki broj u tim razredima. Jedno rješenje teške kategorije (dodavanje metoda za Containersviđa layOutAsGrid(), layOutAsFlow()itd.) Također je nepraktično jer ne možete izmijeniti izvorni kod Containerjednostavno zato što vam je potreban nepodržani izgled. U obrascu za strategiju, te stvoriti Strategysučelje ( LayoutManager) provodi više Concrete Strategyklase ( FlowLayout, GridLayoutitd). Zatim kažete Contextpredmet (aContainer) kako nešto učiniti dodavanjem Strategypredmeta. (Prolazite kroz Containera LayoutManagerkoji definira strategiju izgleda.)

Uzorak graditelja sličan je strategiji. Glavna razlika je u tome što Builderklasa provodi strategiju za konstrukciju nečega (poput JComponentili XML toka koji predstavlja stanje objekta). Builderobjekti obično proizvode svoje proizvode i pomoću višestupanjskog postupka. Odnosno, pozivi različitim metodama Builderpotrebni su za dovršetak postupka gradnje i Builderobično ne znaju redoslijedom pozivanja ili brojem poziva jedne od njegovih metoda. Najvažnija karakteristika graditelja je da poslovni objekt (koji se naziva Context) ne zna točno što Builderobjekt gradi. Uzorak izolira poslovni objekt od njegovog predstavljanja.

Najbolji način da vidite kako funkcionira jednostavni graditelj je da ga pogledate. Prvo pogledajmo Contextposlovni objekt koji treba izložiti korisničko sučelje. Popis 1 prikazuje pojednostavljenu Employeeklasu. EmployeeIma name, idi salaryatribute. (Stubovi za ove razrede nalaze se na dnu popisa, ali ovi su ostaci samo rezervirana mjesta za pravu stvar. Možete - nadam se - lako zamisliti kako bi ove klase funkcionirale.)

Ovo posebno Contextkoristi ono što ja mislim kao dvosmjerni graditelj. Klasični Gang of Four Builder ide u jednom smjeru (izlaz), ali dodao sam i Builderda Employeeobjekt može koristiti za inicijalizaciju. BuilderPotrebna su dva sučelja. Employee.ExporterSučelje (Oglas 1, redak 8) obrađuje izlazni smjer. Definira sučelje s Builderobjektom koje konstruira prikaz trenutnog objekta. U Employeedelegati stvarni sučelje građevinske do Builderu export()metodi (on line 31). BuilderNije prošao stvarne polja, ali umjesto toga koristi Stringje proći zastupljenost tih polja.

Popis 1. Zaposlenik: Kontekst graditelja

1 uvoz java.util.Locale; 2 3 javni razred Zaposlenik 4 {privatno ime; 5 privatni ID zaposlenika; 6 privatnih novčanih plaća; 7 8 javno sučelje Izvoznik 9 {void addName (naziv niza); 10 void addID (ID niza); 11 nevažećih addSalary (niska plaća); 12} 13 14 javno sučelje Uvoznik 15 {String provideName (); 16 String provideID (); 17 String provideSalary (); 18 praznina otvorena (); 19 void zatvori (); 20} 21 22 javni zaposlenik (graditelj uvoznika) 23 {builder.open (); 24 this.name = novo ime (builder.provideName ()); 25 this.id = novi EmployeeId (builder.provideID ()); 26 this.salary = novi novac (builder.provideSalary (), 27 novih Locale ("en", "US")); 28 graditelj.close (); 29} 30 31 javni izvoz praznina (graditelj izvoznika) 32 {builder.addName (name.toString ()); 33 builder.addID (id.toString ()); 34 graditelj.addPlata (plaća.toString ()); 35} 36 37// ... 38} 39 // ---------------------------------------- ------------------------------ 40 // Unit-test stuff 41 // 42 klasa Ime 43 {private String value; 44 javno ime (vrijednost niza) 45 {this.value = value; 46} 47 javni String toString () {povratna vrijednost; }; 48} 49 50 class EmployeeId 51 {private String value; 52 javni EmployeeId (vrijednost niza) 53 {this.value = value; 54} 55 javni String toString () {povratna vrijednost; } 56} 57 58 klasa Novac 59 {private String value; 60 javni novac (vrijednost niza, mjesto na lokaciji) 61 {this.value = value; 62} 63 javni String toString () {povratna vrijednost; } 64}

Pogledajmo primjer. Sljedeći kod gradi korisničko sučelje slike 1:

Wilma zaposlenika = ...; JComponentExporter uiBuilder = novi JComponentExporter (); // Stvaranje graditelja wilma.export (uiBuilder); // Izgradnja korisničkog sučelja JComponent userInterface = uiBuilder.getJComponent (); // ... someContainer.add (userInterface);

Popis 2 prikazuje izvor za JComponentExporter. Kao što vidite, sav kôd povezan s korisničkim sučeljem koncentriran je u Concrete Builder(, JComponentExporter), a Context( Employee) pokreće postupak gradnje, ne znajući točno što gradi.

Popis 2. Izvoz u korisničko sučelje na strani klijenta

1 uvoz javax.swing. *; 2 import java.awt. *; 3 uvoz java.awt.event. *; 4 5 klasa JComponentExporter implementira Employee.Exporter 6 {ime privatnog niza, id, plaća; 7 8 javna praznina addName (ime niza) {this.name = name; } 9 javni void addID (ID niza) {this.id = id; } 10 javnih nevažećih addSalary (string plaća) {this.salary = plaća; } 11 12 JComponent getJComponent () 13 {Ploča JComponent = novi JPanel (); 14 panel.setLayout (novi GridLayout (3,2)); 15 panel.add (novi JLabel ("Ime:")); 16 panel.add (novi JLabel (ime)); 17 panel.add (novi JLabel ("ID zaposlenika:")); 18 panel.add (novi JLabel (id)); 19 panel.add (novi JLabel ("Plaća:")); 20 panel.add (novi JLabel (plaća)); 21 ploča za povratak; 22} 23}