Programování

Více o getrech a setterech

Je to 25 let starý princip objektově orientovaného (OO) designu, který byste neměli vystavovat implementaci objektu jiným třídám v programu. Program je zbytečně obtížné udržovat, když vystavujete implementaci, hlavně proto, že změna objektu, který vystavuje jeho implementaci, vyžaduje změny všech tříd, které objekt používají.

Naneštěstí idiom getter / setter, který si mnoho programátorů myslí jako objektově orientovaný, porušuje tento základní princip OO v piky. Zvažte příklad a Peníze třída, která má getValue () metoda, která vrací „hodnotu“ v dolarech. Po celém vašem programu budete mít následující kód:

double orderTotal; Částka peněz = ...; //... orderTotal + = amount.getValue (); // orderTotal must be in dollars

Problém tohoto přístupu spočívá v tom, že výše uvedený kód vytváří velký předpoklad o tom, jak Peníze třída je implementována (že „hodnota“ je uložena v a dvojnásobek). Kód, který při změně implementace přeruší předpoklady implementace. Pokud například potřebujete aplikaci internacionalizovat, aby podporovala jiné měny než dolary, pak getValue () nevrátí nic smysluplného. Můžete přidat a getCurrency (), ale tím by se vytvořil celý kód kolem getValue () volání mnohem komplikovanější, zvláště pokud vytrváte v používání strategie getter / setter k získání informací, které potřebujete k provedení práce. Typická (chybná) implementace může vypadat takto:

Částka peněz = ...; //... value = amount.getValue (); měna = amount.getCurrency (); conversion = CurrencyTable.getConversionFactor (měna, USDOLLARS); celková + = hodnota * konverze; //...

Tato změna je příliš komplikovaná na to, aby ji zvládl automatický refaktoring. Navíc byste tyto druhy změn museli provádět všude ve svém kódu.

Řešení tohoto problému na úrovni obchodní logiky je provést práci v objektu, který má informace potřebné k provedení práce. Místo extrahování „hodnoty“ k provedení nějaké externí operace s ní byste měli mít Peníze třída provádí všechny peněžní operace, včetně převodu měn. Správně strukturovaný objekt by zpracoval celkem takto:

Peníze celkem = ...; Částka peněz = ...; total.increaseBy (částka); 

The přidat() metoda by zjistila měnu operandu, provedla veškerou nezbytnou konverzi měny (což je správně operace na peníze) a aktualizujte celkem. Pokud jste nejprve použili tuto strategii objektu, která má informace, pracuje, pojem měna lze přidat do Peníze třída bez jakýchkoli změn požadovaných v kódu, který používá Peníze předměty. To znamená, že práce na refaktorování pouze dolaru na mezinárodní implementaci by byla soustředěna na jednom místě: Peníze třída.

Problém

Většina programátorů nemá potíže s uchopením tohoto konceptu na úrovni obchodní logiky (i když to může vyžadovat určité úsilí, aby to důsledně přemýšlelo). Problémy se však začnou objevovat, když do obrazu vstoupí uživatelské rozhraní (UI). Problém není v tom, že nemůžete použít techniky jako ten, který jsem právě popsal, k vytvoření uživatelského rozhraní, ale že mnoho programátorů je uzamčeno do mentality getter / setter, pokud jde o uživatelská rozhraní. Obviňuji tento problém ze zásadně procedurálních nástrojů pro konstrukci kódu, jako je Visual Basic a jeho klony (včetně stavitelů Java UI), které vás nutí do tohoto procedurálního, getter / setterového způsobu myšlení.

(Digression: Někteří z vás se budou bránit předchozímu tvrzení a budou křičet, že VB je založen na posvátné architektuře Model-View-Controller (MVC), stejně tak je to posvátné. Mějte na paměti, že MVC byl vyvinut téměř před 30 lety. V sedmdesátých letech byl největší superpočítač srovnatelný s dnešními desktopy. Většina strojů (například DEC PDP-11) byly 16bitové počítače s pamětí 64 kB a rychlostmi hodin měřenými v desítkách megahertzů. Vaše uživatelské rozhraní bylo pravděpodobně hromádka děrných štítků. Pokud jste měli to štěstí, že máte video terminál, možná jste používali systém vstupu / výstupu (I / O) konzoly založený na ASCII. Za posledních 30 let jsme se toho hodně naučili. Java Swing musel nahradit MVC podobnou architekturou „oddělitelného modelu“, a to především proto, že čistý MVC dostatečně neizoluje vrstvy uživatelského rozhraní a modelu domény.)

Pojďme tedy ve zkratce definovat problém:

Pokud objekt nemusí vystavit informace o implementaci (metodami get / set nebo jiným způsobem), pak je logické, že objekt musí nějakým způsobem vytvořit své vlastní uživatelské rozhraní. To znamená, že pokud je způsob, jakým jsou reprezentovány atributy objektu, skrytý před zbytkem programu, nemůžete tyto atributy extrahovat, abyste mohli vytvořit uživatelské rozhraní.

Mimochodem, neskrýváte skutečnost, že atribut existuje. (Definuji atribut, zde, jako základní charakteristika objektu.) Víte, že an Zaměstnanec musí mít atribut plat nebo mzda, jinak by to nebyl Zaměstnanec. (Bylo by to Osoba, a Dobrovolník, a Tuláknebo něco jiného, ​​co nemá plat.) Co nevíte - nebo chcete vědět - je to, jak je tento plat reprezentován uvnitř objektu. Může to být dvojnásobek, a Tětiva, v měřítku dlouhonebo binárně kódované desetinné místo. Může se jednat o atribut „syntetický“ nebo „odvozený“, který se počítá za běhu (například z platové třídy nebo názvu pracovní pozice nebo načtením hodnoty z databáze). Ačkoli metoda get může skrýt některé z těchto detailů implementace, jak jsme viděli u Peníze například se nemůže dostatečně skrýt.

Jak tedy objekt vytváří své vlastní uživatelské rozhraní a zůstává udržovatelný? Pouze ty nejjednodušší objekty mohou podporovat něco jako a zobrazit sebe () metoda. Realistické objekty musí:

  • Zobrazovat se v různých formátech (XML, SQL, hodnoty oddělené čárkami atd.).
  • Zobrazit jiné pohledy samy o sobě (jedno zobrazení může zobrazit všechny atributy; jiné může zobrazit pouze podmnožinu atributů; a třetí může prezentovat atributy jiným způsobem).
  • Zobrazovat se v různých prostředích (na straně klienta (JComponent) a sloužil klientovi (HTML), například) a zpracovávat vstup i výstup v obou prostředích.

Někteří čtenáři mého předchozího článku o getterech / setterech usoudili, že se zasazuji o to, abyste do objektu přidali metody, které pokryjí všechny tyto možnosti, ale toto „řešení“ je zjevně nesmyslné. Nejen, že je výsledný těžký objekt příliš komplikovaný, budete jej muset neustále upravovat, aby zvládl nové požadavky uživatelského rozhraní. Prakticky objekt prostě nemůže vytvořit všechna možná uživatelská rozhraní pro sebe, pokud z jiného důvodu než mnoho z těchto uživatelských rozhraní nebylo ani vytvořeno, když byla třída vytvořena.

Vytvořte řešení

Řešení tohoto problému je oddělit kód uživatelského rozhraní od hlavního obchodního objektu vložením do samostatné třídy objektů. To znamená, že byste měli oddělit některé funkce, které mohl být v objektu úplně do samostatného objektu.

Toto rozdvojení metod objektu se objevuje v několika návrhových vzorech. S největší pravděpodobností jste obeznámeni se strategií, která se používá u různých kontejner java.awt třídy dělat rozložení. Problém s rozložením můžete vyřešit derivačním řešením: FlowLayoutPanel, GridLayoutPanel, BorderLayoutPanelatd., ale to v těchto třídách vyžaduje příliš mnoho tříd a spoustu duplikovaného kódu. Jediné řešení třídy těžké váhy (přidání metod do Kontejner jako layOutAsGrid (), layOutAsFlow (), atd.) je také nepraktické, protože nemůžete upravit zdrojový kód pro Kontejner jednoduše proto, že potřebujete nepodporované rozvržení. Ve vzoru Strategie vytvoříte a Strategie rozhraní (LayoutManager) implementováno několika Konkrétní strategie třídy (FlowLayout, Rozvržení mřížky, atd.). Pak řeknete a Kontext objekt (a Kontejner) jak něco udělat předáním a Strategie objekt. (Předáte a Kontejner A LayoutManager který definuje strategii rozložení.)

Stavitelský vzor je podobný strategii. Hlavní rozdíl je v tom, že Stavitel třída implementuje strategii pro konstrukci něčeho (jako a JComponent nebo XML stream, který představuje stav objektu). Stavitel objekty obvykle vytvářejí své produkty také pomocí vícestupňového procesu. To znamená, že volá po různých metodách Stavitel jsou povinni dokončit stavební proces a Stavitel obvykle neví, v jakém pořadí budou volání provedena, ani kolikrát bude některá z jejích metod volána. Nejdůležitější vlastností stavitele je, že obchodní objekt (tzv Kontext) přesně neví, co Stavitel objekt je budova. Vzor izoluje obchodní objekt od jeho reprezentace.

Nejlepší způsob, jak zjistit, jak funguje jednoduchý stavitel, je podívat se na jeden. Nejprve se podívejme na Kontext, obchodní objekt, který potřebuje vystavit uživatelské rozhraní. Výpis 1 ukazuje zjednodušující Zaměstnanec třída. The Zaměstnanecnázev, id, a plat atributy. (Pahýly pro tyto třídy jsou ve spodní části seznamu, ale tyto pahýly jsou pouze zástupci skutečné věci. Můžete si - doufám - snadno představit, jak by tyto třídy fungovaly.)

Tento konkrétní Kontext používá to, co považuji za obousměrný stavitel. Klasický Gang of Four Builder jde jedním směrem (výstupem), ale přidal jsem také a Stavitel že Zaměstnanec objekt lze použít k inicializaci. Dva Stavitel jsou vyžadována rozhraní. The Zaměstnanec. Vývozce interface (Výpis 1, řádek 8) zpracovává směr výstupu. Definuje rozhraní k Stavitel objekt, který vytváří reprezentaci aktuálního objektu. The Zaměstnanec deleguje skutečnou konstrukci uživatelského rozhraní na Stavitel v vývozní() metoda (na řádku 31). The Stavitel není předáno skutečné pole, ale místo toho používá Tětivas předat reprezentaci těchto polí.

Výpis 1. Zaměstnanec: Kontext Tvůrce

 1 import java.util.Locale; 2 3 veřejná třída Zaměstnanec 4 {soukromé jméno jméno; 5 soukromé ID zaměstnance; 6 plat soukromých peněz; 7 8 veřejné rozhraní Exportér 9 {void addName (název řetězce); 10 void addID (String id); 11 void addSalary (Řetězcový plat); 12} 13 14 veřejné rozhraní Dovozce 15 {String provideName (); 16 String provideID (); 17 String provideSalary (); 18 void open (); 19 void close (); 20} 21 22 public Employee (Importer builder) 23 {builder.open (); 24 this.name = new Name (builder.provideName ()); 25 this.id = new EmployeeId (builder.provideID ()); 26 this.salary = new Money (builder.provideSalary (), 27 new Locale ("en", "US")); 28 builder.close (); 29} 30 31 public void export (Exporter builder) 32 {builder.addName (name.toString ()); 33 builder.addID (id.toString ()); 34 builder.addSalary (plat.toString ()); 35} 36 37 //... 38 } 39 //---------------------------------------------------------------------- 40 // Testovací jednotky 41 // 42 název třídy 43 {soukromá hodnota řetězce; 44 veřejné jméno (hodnota řetězce) 45 {this.value = hodnota; 46} 47 public String toString () {návratová hodnota; }; 48} 49 50 třída EmployeeId 51 {private Řetězcová hodnota; 52 public EmployeeId (String value) 53 {this.value = value; 54} 55 public String toString () {návratová hodnota; } 56} 57 58 třída Peníze 59 {soukromá hodnota řetězce; 60 veřejných peněz (hodnota řetězce, umístění národního prostředí) 61 {this.value = hodnota; 62} 63 public String toString () {návratová hodnota; } 64} 

Podívejme se na příklad. Následující kód vytváří uživatelské rozhraní obrázku 1:

Zaměstnanec wilma = ...; JComponentExporter uiBuilder = nový JComponentExporter (); // Vytvořte stavitele wilma.export (uiBuilder); // Vytvoření uživatelského rozhraní JComponent userInterface = uiBuilder.getJComponent (); //... someContainer.add (userInterface); 

Výpis 2 zobrazuje zdroj pro JComponentExporter. Jak vidíte, veškerý kód související s uživatelským rozhraním je soustředěn v Stavitel betonu ( JComponentExporter) a Kontext ( Zaměstnanec) řídí proces sestavení, aniž by přesně věděl, co staví.

Výpis 2. Export do uživatelského rozhraní na straně klienta

 1 import javax.swing. *; 2 import java.awt. *; 3 import java.awt.event. *; 4 5 třída JComponentExporter implementuje Employee.Exporter 6 {soukromé jméno řetězce, ID, plat; 7 8 public void addName (název řetězce) {this.name = name; } 9 public void addID (String id) {this.id = id; } 10 public void addSalary (Řetězcový plat) {this.salary = plat; } 11 12 JComponent getJComponent () 13 {panel JComponent = nový JPanel (); 14 panel.setLayout (nový GridLayout (3,2)); 15 panel.add (nový JLabel ("Název:")); 16 panel.add (nový JLabel (jméno)); 17 panel.add (nový JLabel ("ID zaměstnance:")); 18 panel.add (nový JLabel (id)); 19 panel.add (nový JLabel ("Plat:")); 20 panel.add (nový JLabel (plat)); 21 vratný panel; 22} 23}