Uzorci kreativnog dizajna u jezgri Java

1. Uvod

Uzorci dizajna su uobičajeni obrasci koje koristimo prilikom pisanja našeg softvera. Oni predstavljaju ustaljenu najbolju praksu razvijenu tijekom vremena. Oni nam tada mogu pomoći da osiguramo da je naš kod dobro dizajniran i dobro izgrađen.

Kreativni uzorci su dizajnerski uzorci koji se usredotočuju na to kako dobivamo primjerke predmeta. To obično znači kako konstruiramo nove instance klase, ali u nekim slučajevima to znači dobivanje već izgrađene instance spremne za upotrebu.

U ovom ćemo članku ponovno posjetiti neke uobičajene uzorke kreativnog dizajna. Vidjet ćemo kako izgledaju i gdje ih pronaći u JVM-u ili drugim temeljnim knjižnicama.

2. Tvornička metoda

Uzorak tvorničke metode način je za koji možemo odvojiti konstrukciju instance od klase koju gradimo. To je tako da možemo apstrahirati točan tip, dopuštajući našem klijentskom kodu da umjesto toga radi u smislu sučelja ili apstraktnih klasa:

klasa SomeImplementation implementira SomeInterface {// ...} 
javna klasa SomeInterfaceFactory {public SomeInterface newInstance () {return new SomeImplementation (); }}

Ovdje kod našeg klijenta nikada ne mora znati NeštoImplementacija, i umjesto toga, radi u smislu SomeInterface. Čak i više od ovoga, možemo promijeniti tip vraćen iz naše tvornice i kod klijenta ne mora se mijenjati. To čak može uključivati ​​dinamički odabir vrste tijekom izvođenja.

2.1. Primjeri u JVM

Vjerojatno najpoznatiji primjeri ovog uzorka JVM-a su metode izgradnje zbirke na Zbirke razred, poput jednokrevetna (), singletonList (), i singletonMap (). Sve ove instance vraćanja odgovarajuće kolekcije - Postavi, Popis, ili Karta - ali točan tip je nebitan. Uz to, Stream.of () metoda i nova Set.of (), Popis(), i Map.ofEntries () metode omogućuju nam da učinimo isto s većim zbirkama.

Postoji i dosta drugih primjera za to, uključujući Charset.forName (), koji će vratiti drugu instancu Charset razred, ovisno o traženom imenu, i ResourceBundle.getBundle (), koji će učitati drugačiji paket resursa, ovisno o navedenom imenu.

Ni svi ovi ne trebaju pružati različite instance. Neke su samo apstrakcije koje skrivaju unutarnje djelovanje. Na primjer, Calendar.getInstance () i NumberFormat.getInstance () uvijek vraćaju istu instancu, ali točni detalji nisu bitni za klijentski kod.

3. Tvornica sažetaka

Uzorak Tvornice sažetaka korak je dalje od ovoga, gdje tvornica koja se koristi također ima apstraktni osnovni tip. Tada svoj kôd možemo napisati u smislu ovih apstraktnih tipova i odabrati betonsku instancu nekako na vrijeme izvođenja.

Prvo, imamo sučelje i neke konkretne implementacije za funkcionalnost koju zapravo želimo koristiti:

sučelje FileSystem {// ...} 
klasa LocalFileSystem implementira FileSystem {// ...} 
klasa NetworkFileSystem implementira FileSystem {// ...} 

Dalje, imamo sučelje i neke konkretne implementacije kako bi tvornica dobila gore navedeno:

sučelje FileSystemFactory {FileSystem newInstance (); } 
klasa LocalFileSystemFactory implementira FileSystemFactory {// ...} 
klasa NetworkFileSystemFactory implementira FileSystemFactory {// ...} 

Zatim imamo drugu tvorničku metodu za dobivanje apstraktne tvornice putem koje možemo dobiti stvarnu instancu:

klasa Primjer {static FileSystemFactory getFactory (String fs) {FileSystemFactory tvornica; if ("local" .equals (fs)) {factory = novi LocalFileSystemFactory (); inače if ("network" .equals (fs)) {factory = new NetworkFileSystemFactory (); } tvornica povratka; }}

Evo, imamo FileSystemFactory sučelje koje ima dvije konkretne implementacije. Odabiremo točnu implementaciju tijekom izvođenja, ali kôd koji je koristi ne treba brinuti koja se instanca stvarno koristi. Tada svaka vraća različitu konkretnu instancu Sustav datoteka sučelje, ali opet, naš kôd ne treba točno voditi računa o tome koji primjer imamo.

Često dobivamo samu tvornicu drugom tvorničkom metodom, kao što je gore opisano. U našem primjeru ovdje, getFactory () metoda je sama po sebi tvornička metoda koja vraća sažetak FileSystemFactory koja se zatim koristi za konstrukciju a Sustav datoteka.

3.1. Primjeri u JVM

Puno je primjera ovog uzorka dizajna koji se koristi u cijelom JVM-u. Najčešće se viđaju oko XML paketa - na primjer, DocumentBuilderFactory, TransformerFactory, i XPathFactory. Svi oni imaju poseban newInstance () tvornička metoda koja omogućuje našem kodu da dobije instancu apstraktne tvornice.

Ova metoda interno koristi niz različitih mehanizama - svojstva sustava, konfiguracijske datoteke u JVM-u i sučelje davatelja usluga - da pokuša točno odlučiti koju konkretnu instancu koristiti. To nam onda omogućuje instaliranje alternativnih XML knjižnica u našu aplikaciju ako želimo, ali to je transparentno za bilo koji kod koji ih stvarno koristi.

Nakon što naš kod pozove newInstance () metoda, tada će imati primjerak tvornice iz odgovarajuće XML knjižnice. Ova tvornica zatim iz te iste knjižnice konstruira stvarne razrede koje želimo koristiti.

Na primjer, ako koristimo zadanu implementaciju Xerces JVM-a, dobit ćemo primjerak com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl, ali ako bismo umjesto toga željeli koristiti drugu implementaciju, onda pozivamo newInstance () bi to transparentno vratio.

4. Graditelj

Uzorak Graditelja koristan je kada želimo složeniji objekt konstruirati na fleksibilniji način. Djeluje tako što ima zasebnu klasu koju koristimo za izgradnju našeg kompliciranog objekta i omogućava klijentu da to stvori jednostavnijim sučeljem:

klasa CarBuilder {private String make = "Ford"; model privatnog niza = "Fiesta"; privatna ulazna vrata = 4; private String color = "White"; javni Car build () {vratiti novi automobil (marka, model, vrata, boja); }}

To nam omogućuje da pojedinačno pružimo vrijednosti za napraviti, model, vrata, i boja, a zatim kada gradimo Automobil, svi se argumenti konstruktora rješavaju na pohranjene vrijednosti.

4.1. Primjeri u JVM

Postoji nekoliko vrlo ključnih primjera ovog uzorka unutar JVM-a. The StringBuilder i StringBuffer klase su graditelji koji nam omogućuju da konstruiramo dugo Niz pružanjem mnogih malih dijelova. Novija Stream.Builder klasa omogućuje nam da učinimo potpuno isto kako bismo konstruirali a Stream:

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

5. Lijena inicijalizacija

Uzorak lijene inicijalizacije koristimo za odgađanje izračuna neke vrijednosti dok ne zatreba. To ponekad može uključivati ​​pojedine dijelove podataka, a ponekad to može značiti čitave objekte.

To je korisno u brojnim scenarijima. Na primjer, ako potpuno konstruiranje objekta zahtijeva pristup bazi podataka ili mreži i možda ga nikada nećemo trebati koristiti, tada izvršavanje tih poziva može uzrokovati da naša aplikacija ima slabu izvedbu. Ako računamo velik broj vrijednosti koje nam možda nikad neće trebati, to može dovesti do nepotrebne upotrebe memorije.

To tipično funkcionira tako što jedan objekt bude lijeni omotač oko podataka koji su nam potrebni i ako se podaci izračunaju kada im se pristupi putem getter metode:

klasa LazyPi {kalkulator privatnog dobavljača; privatna dvostruka vrijednost; javno sinkronizirano Double getValue () {if (value == null) {value = calculator.get (); } povratna vrijednost; }}

Računanje pi skupa je operacija koju možda nećemo trebati izvesti. Gore navedeno učinit će to prvi put kad nazovemo getValue () a ne prije.

5.1. Primjeri u JVM

Primjeri toga u JVM-u su relativno rijetki. Međutim, izvrstan je primjer Streams API predstavljen u Javi 8. Sve operacije koje se izvode na potoku su lijene, tako da ovdje možemo izvoditi skupe izračune i znati da se pozivaju samo ako je potrebno.

Međutim, stvarna generacija samog potoka također može biti lijena. Stream.generate () uzima funkciju za pozivanje kad god je potrebna sljedeća vrijednost i poziva se samo kad je potrebno. To možemo koristiti za učitavanje skupih vrijednosti - na primjer, upućivanjem HTTP API poziva - i trošak plaćamo samo kad god je novi element zapravo potreban:

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

Evo, imamo Dobavljač koji će HTTP pozive učitati članke, filtrirati na temelju povezanih oznaka i vratiti prvi odgovarajući naslov. Ako se prvi učitani članak podudara s ovim filtrom, tada je potrebno uputiti samo jedan mrežni poziv, bez obzira na to koliko je članaka zapravo prisutno.

6. Skup objekata

Uzorak spremišta objekata koristit ćemo prilikom izrade nove instance objekta koji je možda skupo stvoriti, ali ponovna upotreba postojeće instance prihvatljiva je alternativa. Umjesto da svaki put konstruiramo novu instancu, možemo umjesto toga konstruirati skup ovih unaprijed i zatim ih koristiti prema potrebi.

Stvarno spremište objekata postoji za upravljanje tim dijeljenim objektima. Također ih prati tako da se svaki istodobno koristi samo na jednom mjestu. U nekim se slučajevima cjelokupni skup objekata gradi tek na početku. U drugim slučajevima, bazen može stvoriti nove instance na zahtjev ako je to potrebno

6.1. Primjeri u JVM

Glavni primjer ovog uzorka u JVM-u je uporaba spremišta niti. An ExecutorService upravljat će skupom niti i omogućit će nam da ih koristimo kada zadatak treba izvršiti na jednoj. Korištenje ovoga znači da ne trebamo stvarati nove niti, sa svim uključenim troškovima, kad god trebamo izroditi asinkroni zadatak:

ExecutorService pool = Executors.newFixedThreadPool (10); pool.execute (novi SomeTask ()); // Pokreće se na niti iz bazena pool.execute (new AnotherTask ()); // Radi na niti iz bazena

Ova dva zadatka dobivaju nit na kojoj će se izvoditi iz spremišta niti. To može biti ista nit ili potpuno drugačija, a našem je kodu svejedno koje se niti koriste.

7. Prototip

Uzorak prototipa koristimo kada trebamo stvoriti nove primjerke objekta koji su identični izvorniku. Izvorni primjerak djeluje kao naš prototip i navikne se na konstrukciju novih primjeraka koji su tada potpuno neovisni o izvorniku. Tada ih možemo koristiti, ali je potrebno.

Java ima razinu podrške za to primjenom Klonirajući sučelje markera, a zatim pomoću Object.clone (). To će stvoriti plitki klon objekta, stvoriti novu instancu i izravno kopirati polja.

Ovo je jeftinije, ali ima lošu stranu što će sva polja unutar našeg objekta koja su se strukturirala biti ista instanca. To onda znači da se promjene na tim poljima događaju i u svim instancama. Međutim, to uvijek možemo poništiti ako je potrebno:

prototip javne klase implementira Cloneable {sadržaj privatne mape = novi HashMap (); javna praznina setValue (ključ niza, vrijednost niza) {// ...} javni niz getValue (ključ niza) {// ...} @Override javni prototip clone () {Rezultat prototipa = novi prototip (); this.contents.entrySet (). forEach (entry -> result.setValue (entry.getKey (), entry.getValue ())); povratni rezultat; }}

7.1. Primjeri u JVM

JVM ima nekoliko primjera za to. To možemo vidjeti slijedeći razrede koji implementiraju Klonirajući sučelje. Na primjer, PKIXCertPathBuilderResult, PKIXBuilderParameters, PKIXParametri, PKIXCertPathBuilderResult, i PKIXCertPathValidatorResult su svi Klonirajući.

Drugi primjer je java.util.Datum razred. Značajno, ovo nadjačava Objekt.klon() metoda kopiranja i preko dodatnog prolaznog polja.

8. Singleton

Uzorak Singleton često se koristi kada imamo klasu koja bi trebala imati samo jednu instancu, a ova bi instanca trebala biti dostupna iz cijele aplikacije. Tipično upravljamo ovim pomoću statičke instance kojoj pristupamo putem statičke metode:

javna klasa Singleton {privatna statička pojedinačna instanca = null; javni statički Singleton getInstance () {if (instance == null) {instance = new Singleton (); } povratna instanca; }}

Postoji nekoliko varijacija ovoga, ovisno o točnim potrebama - na primjer, da li je instanca stvorena pri pokretanju ili pri prvoj upotrebi, treba li pristup njoj biti siguran i treba li postojati druga instanca po niti.

8.1. Primjeri u JVM

JVM ima nekoliko primjera toga s klasama koje predstavljaju temeljne dijelove samog JVM-aRuntime, radna površina, i SecurityManager. Svi oni imaju metode pristupa koje vraćaju pojedinačnu instancu odgovarajuće klase.

Uz to, velik dio API-ja Java Reflection radi s pojedinačnim instancama. Ista stvarna klasa uvijek vraća istu instancu Predavanje, bez obzira na to pristupa li mu se pomoću Class.forName (), String.razred, ili drugim metodama refleksije.

Na sličan bismo način mogli razmotriti i Nit instanca koja predstavlja trenutnu nit kao jednostruku. Toga će često biti mnogo, ali po definiciji postoji jedan primjerak po niti. Pozivanje Thread.currentThread () s bilo kojeg mjesta koje se izvršava u istoj niti uvijek će vratiti istu instancu.

9. Sažetak

U ovom smo članku pogledali različite dizajnerske uzorke koji se koriste za stvaranje i dobivanje primjeraka objekata. Također smo pogledali primjere ovih obrazaca koji se koriste i unutar jezgre JVM, tako da ih možemo vidjeti u upotrebi na način na koji mnoge aplikacije već imaju koristi.