Kreatív tervezési minták a Java Java-ban

1. Bemutatkozás

A tervezési minták gyakori minták, amelyeket szoftverünk megírásakor használunk. Ők képviselik az idő során kialakult bevált legjobb gyakorlatokat. Ezek aztán segíthetnek abban, hogy kódunk jól megtervezett és felépített legyen.

A kreatív minták olyan tervezési minták, amelyek arra összpontosítanak, hogyan szerezzük meg az objektumok példányait. Jellemzően ez azt jelenti, hogy hogyan építünk új osztálypéldányokat, de bizonyos esetekben egy már elkészített példány megszerzését jelenti, amely használatra kész.

Ebben a cikkben áttekintünk néhány általános kreatív tervezési mintát. Meglátjuk, hogy néznek ki és hol találják meg őket a JVM-ben vagy más törzskönyvtárakban.

2. Gyári módszer

A gyári módszer mintázat arra szolgál, hogy elkülönítsük a példány felépítését az általunk felépített osztálytól. Ez lehetővé teszi, hogy elvonhassuk a pontos típust, lehetővé téve az ügyfélkódnak, hogy interfészek vagy absztrakt osztályok helyett működjön:

class SomeImplementation implementálja a SomeInterface {// ...} 
public class SomeInterfaceFactory {public SomeInterface newInstance () {return new SomeImplementation (); }}

Itt ügyfelünk kódjának soha nem kell tudnia SomeImplementation, és ehelyett a kifejezés szempontjából működik SomeInterface. Még ennél is több, megváltoztathatjuk a gyárunkból visszaküldött típust, és az ügyfél kódjának nem kell megváltoznia. Ez magában foglalhatja akár a típus dinamikus kiválasztását futás közben.

2.1. Példák a JVM-re

A JVM ennek a mintának a legismertebb példái valószínűleg a gyűjteményépítési módszerek Gyűjtemények osztály, mint szingli(), singletonList (), és singletonMap (). Ezek a megfelelő gyűjtés összes példánya - Készlet, Lista, vagy Térkép - de a pontos típus lényegtelen. Ezenkívül a Stream.of () módszer és az új Készlet(), Listája(), és Map.ofEntries () módszerek lehetővé teszik, hogy ugyanezt tegyük nagyobb gyűjteményekkel is.

Rengeteg más példa van erre is, többek között Charset.forName (), amely a Charset osztály a kért név függvényében, és ResourceBundle.getBundle (), amely a megadott névtől függően más erőforráscsomagot tölt be.

Ezeknek sem kell különböző példányokat szolgáltatniuk. Néhány csak absztrakció a belső működés elrejtésére. Például, Calendar.getInstance () és NumberFormat.getInstance () mindig ugyanazt a példányt adja vissza, de a pontos részletek nem relevánsak az ügyfélkód szempontjából.

3. Absztrakt gyár

Az Absztrakt gyár minta egy lépést jelent ezen túl, ahol az alkalmazott gyárnak absztrakt alaptípusa is van. Ezután megírhatjuk a kódunkat ezeknek az absztrakt típusoknak a figyelembevételével, és futás közben valamilyen módon kiválaszthatjuk a konkrét gyári példányt.

Először is van egy felületünk és néhány konkrét megvalósításunk a valóban használni kívánt funkcionalitáshoz:

interfész FileSystem {// ...} 
class LocalFileSystem implementálja a FileSystem {// ...} 
osztály NetworkFileSystem implementálja a FileSystem {// ...} 

Ezután rendelkezünk egy interfésszel és néhány konkrét megvalósítással a gyár számára a fentiek megszerzéséhez:

interfész FileSystemFactory {FileSystem newInstance (); } 
class LocalFileSystemFactory megvalósítja a FileSystemFactory {// ...} 
class NetworkFileSystemFactory megvalósítja a FileSystemFactory {// ...} 

Ezután van egy másik gyári módszerünk az absztrakt gyár megszerzésére, amelyen keresztül megszerezhetjük a tényleges példányt:

osztály Példa {statikus FileSystemFactory getFactory (String fs) {FileSystemFactory gyár; if ("helyi" .egyenlő (fs)) {gyár = új LocalFileSystemFactory (); else if ("hálózat" .egyenlő (fs)) {gyár = új NetworkFileSystemFactory (); } visszatérő gyár; }}

Itt van egy FileSystemFactory felület, amely két konkrét megvalósítással rendelkezik. Futás közben választjuk ki a pontos megvalósítást, de az azt használó kódnak nem kell törődnie azzal, hogy melyik példányt használják valójában. Ezek aztán mindegyiknek egy másik konkrét példányát adják vissza Fájlrendszer felületet, de a kódunknak megint nem kell törődnie azzal, hogy ennek pontosan melyik példánya van.

Gyakran magát a gyárat más gyári módszerrel szerezzük be, a fentiek szerint. Az itt látható példánkban a getFactory () A módszer maga egy gyári módszer, amely absztraktot ad vissza FileSystemFactory hogy aztán felhasználják a Fájlrendszer.

3.1. Példák a JVM-re

Rengeteg példa található erre a tervezési mintára az egész JVM-ben. A leggyakrabban az XML csomagok körül láthatók - például DocumentBuilderFactory, TransformerFactory, és XPathFactory. Ezek mindegyike különleges newInstance () gyári módszer, amely lehetővé teszi, hogy kódunk megszerezze az absztrakt gyár példányát.

Belsőleg ez a módszer számos különböző mechanizmust használ - a rendszer tulajdonságait, a JVM konfigurációs fájljait és a szolgáltatói felületet -, hogy megpróbálja eldönteni, hogy pontosan mely konkrét példányt használja. Ez lehetővé teszi számunkra, hogy alternatív XML könyvtárakat telepítsünk az alkalmazásunkba, ha szeretnénk, de ez átlátható minden olyan kód számára, amely azokat ténylegesen használja.

Miután a kódunk felhívta a newInstance () metódus, akkor a megfelelő XML könyvtárból lesz egy gyári példánya. Ez a gyár majd elkészíti a tényleges osztályokat, amelyeket használni akarunk ugyanabból a könyvtárból.

Például, ha a JVM alapértelmezett Xerces megvalósítását használjuk, akkor kapunk egy példányt com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl, de ha ehelyett egy másik megvalósítást akartunk használni, akkor hívjon newInstance () átlátszóan visszaadná ezt.

4. Építő

A Builder minta akkor hasznos, ha egy bonyolult objektumot rugalmasabban akarunk létrehozni. Úgy működik, hogy van egy külön osztályunk, amelyet a bonyolult objektum felépítéséhez használunk, és lehetővé teszi az ügyfél számára, hogy ezt egy egyszerűbb felülettel hozza létre:

osztályú CarBuilder {private String gyártmány = "Ford"; privát húr modell = "Fiesta"; magán int ajtók = 4; privát karakterlánc színe = "Fehér"; public Car build () {return new Car (márka, modell, ajtók, szín); }}

Ez lehetővé teszi számunkra, hogy egyenként megadjuk az értékeket készítsen, modell, ajtók, és szín, majd amikor felépítjük a Autó, a konstruktor összes argumentuma megoldódik a tárolt értékekre.

4.1. Példák a JVM-re

Van néhány nagyon fontos példa erre a mintára a JVM-en belül. A StringBuilder és StringBuffer osztályok építők, amelyek lehetővé teszik számunkra, hogy hosszút építsünk Húr sok apró alkatrész rendelkezésre bocsátásával. Az újabb Stream.Builder osztály lehetővé teszi számunkra, hogy pontosan ugyanezt tegyük a Folyam:

Stream.Builder builder = Stream.builder (); építő.add (1); építő.add (2); if (feltétel) {építő.add (3); építő.add (4); } builder.add (5); Stream stream = builder.build ();

5. Lusta inicializálás

A Lazy inicializálási mintát arra használjuk, hogy elhalasszuk bizonyos értékek számítását, amíg erre szükség van. Néha ez egyes adatokat tartalmazhat, máskor pedig egész objektumokat.

Ez számos forgatókönyv esetén hasznos. Például, ha egy objektum teljes felépítéséhez adatbázis vagy hálózati hozzáférés szükséges, és lehet, hogy soha nem kell használnunk, akkor ezeknek a hívásoknak az végrehajtása az alkalmazásunk alulteljesítését eredményezheti. Alternatív megoldásként, ha sok olyan értéket számolunk, amelyekre soha nem lehet szükségünk, akkor ez felesleges memóriahasználatot okozhat.

Általában ez úgy működik, hogy egy objektum legyen a lusta burkoló a szükséges adatok köré, és az adatokat kiszámítsák, amikor egy getter módszerrel érik el őket:

osztály LazyPi {privát beszállítói számológép; magán Dupla érték; nyilvános szinkronizált Double getValue () {if (value == null) {value = calculator.get (); } visszatérési érték; }}

A pi kiszámítása drága művelet, amelyet valószínűleg nem kell végrehajtanunk. A fentiek ezt teszik először, amikor felhívjuk getValue () és nem korábban.

5.1. Példák a JVM-re

Erre a JVM-ben viszonylag ritkák a példák. A Java 8-ban bevezetett Streams API azonban remek példa. A folyamon végzett összes művelet lusta, így itt drága számításokat hajthatunk végre, és tudjuk, hogy csak szükség esetén hívják őket.

Azonban, maga a patak tényleges generációja is lusta lehet. Stream.generate () vesz egy függvényt, amelyet akkor hív, amikor a következő értékre van szükség, és csak akkor hívja meg, ha szükséges. Ezt felhasználhatjuk drága értékek betöltésére - például HTTP API-hívások kezdeményezésével -, és csak akkor fizetjük a költségeket, ha valóban új elemre van szükség:

Stream.generate (új BaeldungArticlesLoader ()). Filter

Itt van egy Támogató amelyek HTTP-hívásokat indítanak a cikkek betöltésére, a társított címkék alapján történő szűrésre, majd az első egyező cím visszaadására. Ha a legelső betöltött cikk megegyezik ezzel a szűrővel, akkor csak egyetlen hálózati hívást kell kezdeményezni, függetlenül attól, hogy hány cikk van jelen.

6. Objektumkészlet

Az Object Pool mintát akkor fogjuk használni, ha olyan objektumból építünk új példányt, amelynek létrehozása költséges lehet, de egy meglévő példány újrafelhasználása elfogadható alternatíva. Ahelyett, hogy minden alkalommal új példányt állítanánk elő, ehelyett összeállíthatunk ezekből egy halmazt, majd szükség szerint felhasználhatjuk őket.

A tényleges objektumkészlet létezik ezeknek a megosztott objektumoknak a kezelésére. Az is nyomon követi őket, hogy mindegyiket csak egy helyen használják egyszerre. Bizonyos esetekben a teljes objektumkészlet csak az elején épül fel. Más esetekben a készlet szükség esetén új példányokat hozhat létre

6.1. Példák a JVM-re

Ennek a mintának a fő példája a JVM-ben a szálkészletek használata. An ExecutorService kezelni fog egy szálkészletet, és lehetővé teszi számunkra, hogy felhasználjuk őket, amikor egy feladatot egyre kell végrehajtani. Ez azt jelenti, hogy nem kell új szálakat létrehoznunk az összes költséggel, amikor aszinkron feladatot kell létrehoznunk:

ExecutorService pool = Executors.newFixedThreadPool (10); pool.execute (new SomeTask ()); // szálon fut a pool pool.execute (new AnotherTask ()); // Szálon fut a medencéből

Ez a két feladat hozzárendel egy szálat, amelyen a szálkészletből futtatható. Lehet, hogy ugyanaz a szál vagy teljesen más, és a kódunk számára nem mindegy, melyik szálat használják.

7. Prototípus

Akkor használjuk a Prototype mintát, amikor új objektumokat kell létrehoznunk, amelyek azonosak az eredetivel. Az eredeti példány prototípusunkként működik, és megszokja az új példányok összeállítását, amelyek aztán teljesen függetlenek az eredetitől. Ezután felhasználhatjuk ezeket, de szükséges.

A Java bizonyos szintű támogatást nyújt ehhez a Klónozható marker interfész, majd a Object.clone (). Ez az objektum sekély klónját hozza létre, létrehoz egy új példányt, és közvetlenül másolja a mezőket.

Ez olcsóbb, de van egy hátránya, hogy az objektumunkon belül minden olyan mező, amely önmagát strukturálta, ugyanaz a példány lesz. Ez tehát azt jelenti, hogy ezeken a mezőkön is változások történnek minden példányban. Szükség esetén azonban ezt mindig magunk is felülírhatjuk:

public class Prototípus megvalósítja Cloneable {privát Térkép tartalma = new HashMap (); public void setValue (String kulcs, String érték) {// ...} public String getValue (String kulcs) {// ...} @Orride public Prototype clone () {Prototype result = new Prototype (); this.contents.entrySet (). forEach (bejegyzés -> eredmény.setValue (bejegyzés.getKey (), bejegyzés.getValue ())); visszatérési eredmény; }}

7.1. Példák a JVM-re

A JVM-nek van néhány példája erre. Ezeket láthatjuk, ha követjük azokat az osztályokat, amelyek megvalósítják a Klónozható felület. Például, PKIXCertPathBuilderResult, PKIXBuilderParameters, PKIXParaméterek, PKIXCertPathBuilderResult, és PKIXCertPathValidatorResult mind Klónozható.

Egy másik példa a java.util.Dátum osztály. Nevezetesen, ez felülírja a Tárgy.klón () módszer egy további átmeneti mező átmásolására is.

8. Singleton

A Singleton mintát gyakran használják, ha van olyan osztályunk, amelynek csak egy példánya lehet, és ennek a példánynak az egész alkalmazásból elérhetőnek kell lennie. Általában ezt egy statikus példánnyal kezeljük, amelyhez statikus módszerrel férünk hozzá:

nyilvános osztály Singleton {privát statikus Singleton példány = null; nyilvános statikus Singleton getInstance () {if (instance == null) {instance = new Singleton (); } visszatérési példány; }}

Ennek számos változata van a pontos igényektől függően - például, hogy a példány induláskor vagy első használatkor jön-e létre, hogy a hozzáférésnek szálbiztonságosnak kell-e lennie, és szálanként más példánynak kell-e lennie.

8.1. Példák a JVM-re

A JVM-nek van néhány példája erre olyan osztályokkal, amelyek maguk a JVM alapvető részeit képviselikFutásidejű, asztali, és SecurityManager. Ezek mindegyike rendelkezik hozzáférési módszerekkel, amelyek az adott osztály egyetlen példányát adják vissza.

Ezenkívül a Java Reflection API nagy része egyedülálló példányokkal működik. Ugyanaz a tényleges osztály mindig ugyanazt a példányt adja vissza Osztály, függetlenül attól, hogy a rendszer elérte-e Class.forName (), Karakterlánc.osztály, vagy más reflexiós módszerekkel.

Hasonló módon fontolóra vehetjük a cérna az aktuális szálat szingulettnek ábrázoló példány. Ennek gyakran sok példánya lesz, de definíció szerint szálanként egyetlen példány létezik. Hívás Thread.currentThread () bárhonnan, ugyanabban a szálban végrehajtva mindig ugyanazt a példányt adja vissza.

9. Összegzés

Ebben a cikkben az objektumok példányainak létrehozásához és megszerzéséhez használt különféle tervezési mintákat tekintettük meg. Megvizsgáltuk ezeknek a mintáknak a JVM-en belül használt példáit is, így láthatjuk, hogy használatban vannak olyanok, amelyek már számos alkalmazás számára előnyösek.