Tvorivé návrhové vzory v jadre Java

1. Úvod

Dizajnové vzory sú bežné vzory, ktoré používame pri písaní nášho softvéru. Predstavujú zavedené osvedčené postupy vyvinuté v priebehu času. Tieto nám potom môžu pomôcť zaistiť, aby bol náš kód správne navrhnutý a správne zostavený.

Tvorivé vzory sú návrhové vzory, ktoré sa zameriavajú na to, ako získavame inštancie objektov. Spravidla to znamená, ako konštruujeme nové inštancie triedy, ale v niektorých prípadoch to znamená získanie už vytvorenej inštancie pripravenej na použitie.

V tomto článku sa znova pozrieme na niektoré bežné vzory tvorivého dizajnu. Uvidíme, ako vyzerajú a kde ich nájdeme v rámci JVM alebo iných základných knižníc.

2. Továrenská metóda

Vzor Factory Method je spôsob, ako môžeme oddeliť konštrukciu inštancie od triedy, ktorú konštruujeme. Je to tak, že môžeme abstrahovať od presného typu, čo umožňuje nášmu klientskemu kódu pracovať namiesto toho v zmysle rozhraní alebo abstraktných tried:

trieda SomeImplementation implementuje SomeInterface {// ...} 
verejná trieda SomeInterfaceFactory {public SomeInterface newInstance () {return new SomeImplementation (); }}

Tu náš kód klienta nemusí nikdy vedieť SomeImplementation, a namiesto toho to funguje z hľadiska Niektoré rozhranie. Ešte viac než toto môžeme zmeniť typ vrátený z našej továrne a kód klienta sa nemusí meniť. To môže dokonca zahŕňať aj dynamický výber typu za behu.

2.1. Príklady v JVM

Pravdepodobne najznámejšími príkladmi tohto modelu sú metódy budovania zbierky na Zbierky trieda, ako singleton (), singletonList ()a singletonMap (). Toto všetko vrátia inštancie príslušnej zbierky - Nastaviť, Zoznamalebo Mapa - ale presný typ je irelevantný. Okrem toho Stream.of () metóda a nová Set.of (), Zoznam()a Map.ofEntries () metódy nám umožňujú urobiť to isté s väčšími zbierkami.

Existuje aj veľa ďalších príkladov, vrátane Charset.forName (), ktorý vráti inú inštanciu súboru Charset trieda v závislosti od požadovaného mena a ResourceBundle.getBundle (), ktorý načíta iný balík zdrojov v závislosti od zadaného názvu.

Nie všetky z nich musia tiež poskytovať rôzne inštancie. Niektoré sú iba abstrakcie, ktoré zakrývajú vnútorné fungovanie. Napríklad, Calendar.getInstance () a NumberFormat.getInstance () vracajte vždy tú istú inštanciu, ale presné podrobnosti nie sú pre kód klienta relevantné.

3. Abstraktná továreň

Vzor Abstract Factory je o krok ďalej, keď použitá továreň má aj abstraktný základný typ. Potom môžeme náš kód napísať z hľadiska týchto abstraktných typov a za behu modulu nejako vybrať inštanciu konkrétnej továrne.

Najprv máme rozhranie a niektoré konkrétne implementácie pre funkcionalitu, ktorú skutočne chceme použiť:

rozhranie FileSystem {// ...} 
trieda LocalFileSystem implementuje FileSystem {// ...} 
trieda NetworkFileSystem implementuje FileSystem {// ...} 

Ďalej máme rozhranie a niekoľko konkrétnych implementácií pre továreň na získanie vyššie uvedeného:

rozhranie FileSystemFactory {FileSystem newInstance (); } 
trieda LocalFileSystemFactory implementuje FileSystemFactory {// ...} 
trieda NetworkFileSystemFactory implementuje FileSystemFactory {// ...} 

Potom máme inú továrenskú metódu na získanie abstraktnej továrne, pomocou ktorej môžeme získať skutočnú inštanciu:

trieda Príklad {static FileSystemFactory getFactory (String fs) {FileSystemFactory factory; if ("local" .equals (fs)) {factory = new LocalFileSystemFactory (); else if ("network" .equals (fs)) {factory = new NetworkFileSystemFactory (); } vrátiť továreň; }}

Tu máme FileSystemFactory rozhranie, ktoré má dve konkrétne implementácie. Vyberieme presnú implementáciu za behu, ale kód, ktorý ju využíva, nemusí zaujímať, ktorá inštancia sa skutočne používa. Tieto potom vrátia inú konkrétnu inštanciu súboru Systém súborov naše rozhranie zase nemusí byť úplne jedno, ktorú inštanciu tohto máme.

Samotnú továreň často získavame pomocou inej továrenskej metódy, ako je popísané vyššie. V našom príklade tu je getFactory () metóda je sama o sebe továrenskou metódou, ktorá vracia abstrakt FileSystemFactory ktorý sa potom používa na zostrojenie a Systém súborov.

3.1. Príklady v JVM

Existuje veľa príkladov tohto návrhového vzoru použitých v rámci JVM. Najčastejšie sa vyskytujú okolo balíkov XML - napríklad DocumentBuilderFactory, TransformerFactory, a XPathFactory. Všetky majú špeciálnu newInstance () továrenská metóda, ktorá umožňuje nášmu kódu získať inštanciu abstraktnej továrne.

Táto metóda interne využíva množstvo rôznych mechanizmov - vlastnosti systému, konfiguračné súbory v prostredí JVM a rozhranie poskytovateľa služieb - aby sa pokúsila presne rozhodnúť, ktorú konkrétnu inštanciu použiť. To nám potom umožňuje inštalovať alternatívne knižnice XML do našej aplikácie, ak si to prajeme, ale to je transparentné pre akýkoľvek kód, ktorý ich skutočne používa.

Len čo náš kód zavolal newInstance () metóda, potom bude mať inštanciu továrne z príslušnej knižnice XML. Táto továreň potom z tej istej knižnice vytvorí skutočné triedy, ktoré chceme použiť.

Napríklad, ak používame predvolenú implementáciu Xerces JVM, dostaneme inštanciu com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl, ale ak by sme chceli namiesto toho použiť inú implementáciu, zavolajte newInstance () by to transparentne vrátilo.

4. Staviteľ

Vzor Builder je užitočný, keď chceme flexibilnejším spôsobom skonštruovať komplikovaný objekt. Funguje to tak, že máme samostatnú triedu, ktorú používame na zostavenie nášho komplikovaného objektu, a umožňujeme klientovi vytvoriť ho pomocou jednoduchšieho rozhrania:

trieda CarBuilder {private String make = "Ford"; private String model = "Fiesta"; súkromné ​​int dvere = 4; súkromná farba reťazca = "biela"; public Car build () {vrátiť nové auto (značka, model, dvere, farba); }}

To nám umožňuje individuálne poskytnúť hodnoty pre urobiť, Model, dverea farba, a potom, keď postavíme Auto, všetky argumenty konštruktora sa vyriešia na uložené hodnoty.

4.1. Príklady v JVM

Existuje niekoľko veľmi dôležitých príkladov tohto modelu v rámci JVM. The StringBuilder a StringBuffer triedy sú stavitelia, ktorí nám umožňujú postaviť dlho String poskytnutím mnohých malých častí. Čím novšia Stream. Staviteľ trieda nám umožňuje urobiť presne to isté, aby sme vytvorili a Prúd:

Stream.Builder builder = Stream.builder (); builder.add (1); builder.add (2); if (podmienka) {builder.add (3); builder.add (4); } builder.add (5); Stream stream = builder.build ();

5. Lenivá inicializácia

Pomocou vzoru Lazy Initialization odložíme výpočet určitej hodnoty, kým to nebude potrebné. Niekedy to môže zahŕňať jednotlivé časti údajov a inokedy to môže znamenať celé objekty.

Je to užitočné v mnohých scenároch. Napríklad, ak úplná konštrukcia objektu vyžaduje prístup do databázy alebo do siete a možno ho nikdy nebudeme musieť použiť, potom uskutočnenie týchto hovorov môže spôsobiť nedostatočné vykonávanie našej aplikácie. Prípadne, ak počítame veľké množstvo hodnôt, ktoré možno nikdy nebudeme potrebovať, môže to spôsobiť zbytočné využitie pamäte.

Spravidla to funguje tak, že jeden objekt bude lenivý obal okolo údajov, ktoré potrebujeme, a dáta sa budú počítať, keď sa k nim bude pristupovať pomocou metódy getra:

trieda LazyPi {súkromná kalkulačka dodávateľa; súkromná dvojitá hodnota; public synchronized Double getValue () {if (value == null) {value = calculator.get (); } návratová hodnota; }}

Výpočet pí je nákladná operácia, ktorú možno nebudeme musieť vykonať. Vyššie uvedené urobíme tak po prvýkrát, keď zavoláme getValue () a nie skôr.

5.1. Príklady v JVM

Príklady toho v JVM sú pomerne zriedkavé. Skvelým príkladom je však rozhranie Streams API zavedené v prostredí Java 8. Všetky operácie vykonávané v prúde sú lenivé, takže tu môžeme vykonávať drahé výpočty a vedieť, že sa volajú iba v prípade potreby.

Avšak samotná generácia toku môže byť tiež lenivá. Stream.generate () prevezme funkciu, ktorá zavolá vždy, keď je potrebná ďalšia hodnota, a zavolá sa iba v prípade potreby. Môžeme to použiť na načítanie drahých hodnôt - napríklad uskutočňovaním hovorov HTTP API - a náklady platíme iba vtedy, keď je skutočne potrebný nový prvok:

Stream.generate (new BaeldungArticlesLoader ()) .filter (article -> article.getTags (). Contains ("java-streams")) .map (article -> article.getTitle ()) .findFirst ();

Tu máme Dodávateľ ktoré uskutočnia hovory HTTP s cieľom načítať články, filtrovať ich na základe priradených značiek a potom vrátiť prvý zodpovedajúci nadpis. Ak sa tento filter zhoduje s úplne prvým načítaným článkom, je potrebné uskutočniť iba jedno sieťové volanie bez ohľadu na to, koľko článkov je v skutočnosti prítomných.

6. Objektový fond

Vzor skupiny objektov použijeme pri konštrukcii novej inštancie objektu, ktorého vytvorenie môže byť nákladné, ale opätovné použitie existujúcej inštancie je prijateľnou alternatívou. Namiesto toho, aby sme zakaždým konštruovali novú inštanciu, môžeme namiesto toho zostaviť ich množinu vopred a potom ich podľa potreby použiť.

Aktuálna oblasť objektov existuje na správu týchto zdieľaných objektov. Tiež ich sleduje, aby sa každá z nich používala iba na jednom mieste súčasne. V niektorých prípadoch sa celá skupina objektov vytvorí iba na začiatku. V iných prípadoch môže fond v prípade potreby vytvoriť nové inštancie na požiadanie

6.1. Príklady v JVM

Hlavným príkladom tohto vzoru v JVM je použitie oblastí vlákien. An ExecutorService bude spravovať množinu vlákien a umožní nám ich použiť, keď je potrebné úlohu vykonať na jednom. To znamená, že nepotrebujeme vytvárať nové vlákna so všetkými súvisiacimi nákladmi, kedykoľvek potrebujeme vytvoriť asynchrónnu úlohu:

Pool ExecutorService = Executors.newFixedThreadPool (10); pool.execute (nový SomeTask ()); // Beží na vlákne z fondu pool.execute (new AnotherTask ()); // Beží na vlákne z fondu

Tieto dve úlohy dostanú pridelené vlákno, ktoré sa má spustiť z fondu vlákien. Môže to byť rovnaké vlákno alebo úplne iné vlákno a na našom kóde nezáleží na tom, ktoré vlákna sa používajú.

7. Prototyp

Prototypový vzor používame, keď potrebujeme vytvoriť nové inštancie objektu, ktoré sú totožné s originálom. Pôvodná inštancia funguje ako náš prototyp a zvykne si vytvárať nové inštancie, ktoré sú potom úplne nezávislé od pôvodnej. Potom ich môžeme použiť, je to však nevyhnutné.

Java má pre to implementovanú podporu úrovne podpory Cloneable značkovacie rozhranie a potom pomocou Object.clone (). Takto sa vytvorí plytký klon objektu, ktorý vytvorí novú inštanciu a skopíruje polia priamo.

Je to lacnejšie, ale má to nevýhodu, že všetky polia v našom objekte, ktoré sa štruktúrovali, budú rovnakou inštanciou. To potom znamená, že zmeny v týchto poliach sa vyskytujú aj vo všetkých prípadoch. V prípade potreby to však môžeme kedykoľvek prekonať sami:

verejná trieda Prototyp implementuje Cloneable {private Map contents = new HashMap (); public void setValue (reťazcový kľúč, hodnota reťazca) {// ...} verejný reťazec getValue (reťazcový kľúč) {// ...} @Override public Prototype clone () {Prototype result = new Prototype (); this.contents.entrySet (). forEach (entry -> result.setValue (entry.getKey (), entry.getValue ())); návratový výsledok; }}

7.1. Príklady v JVM

JVM má niekoľko príkladov. Môžeme ich vidieť sledovaním tried, ktoré implementujú Cloneable rozhranie. Napríklad, PKIXCertPathBuilderResult, PKIXBuilderParameters, PKIXParametre, PKIXCertPathBuilderResulta PKIXCertPathValidatorResult sú všetci Cloneable.

Ďalším príkladom je java.util.Date trieda. Je pozoruhodné, toto má prednosť pred Objekt.klon () metóda kopírovania aj cez ďalšie prechodné pole.

8. Singleton

Singletonov vzor sa často používa, keď máme triedu, ktorá by mala mať vždy iba jednu inštanciu a táto inštancia by mala byť prístupná z celej aplikácie. Spravidla to spravujeme so statickou inštanciou, ku ktorej pristupujeme pomocou statickej metódy:

verejná trieda Singleton {súkromná statická inštancia Singleton = null; public static Singleton getInstance () {if (instance == null) {instance = new Singleton (); } návratová inštancia; }}

Existuje niekoľko variácií, ktoré závisia od presných potrieb - napríklad či je inštancia vytvorená pri štarte alebo pri prvom použití, či musí byť prístup k nej bezpečný pre vlákna a či pre každé vlákno musí byť iná inštancia.

8.1. Príklady v JVM

JVM má niekoľko príkladov toho, v čom sú triedy, ktoré reprezentujú základné časti samotného JVMRuntime, desktop, a SecurityManager. Všetky majú prístupové metódy, ktoré vracajú jednu inštanciu príslušnej triedy.

Okrem toho veľká časť rozhrania Java Reflection API funguje s inštanciami singleton. Rovnaká skutočná trieda vždy vráti rovnakú inštanciu Trieda, bez ohľadu na to, či je k nemu prístup pomocou Class.forName (), Reťazec.trieda, alebo prostredníctvom iných reflexných metód.

Podobným spôsobom by sme mohli zvážiť Závit inštancia predstavujúca aktuálne vlákno ako singleton. Často to bude veľa prípadov, ale podľa definície existuje na každé vlákno jedna inštancia. Telefonovanie Thread.currentThread () odkiaľkoľvek vykonávajúce v rovnakom vlákne vždy vráti rovnakú inštanciu.

9. Zhrnutie

V tomto článku sme sa pozreli na rôzne vzory návrhu, ktoré sa používajú na vytváranie a získavanie inštancií objektov. Pozreli sme sa tiež na príklady týchto vzorov, ktoré sa používajú aj v jadre JVM, takže ich použitie môžeme vidieť spôsobom, z ktorého už mnohé aplikácie profitujú.


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