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ák
nebo 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 dlouho
nebo 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
, BorderLayoutPanel
atd., 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ěstnanec
má ná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ětiva
s 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}