Креативни дизайнерски модели в Core Java

1. Въведение

Шаблоните за дизайн са често срещани модели, които използваме, когато пишем нашия софтуер . Те представляват утвърдени най-добри практики, разработени с течение на времето. Тогава те могат да ни помогнат да гарантираме, че нашият код е добре проектиран и добре изграден.

Творческите модели са дизайнерски модели, които се фокусират върху това как получаваме екземпляри от обекти . Обикновено това означава как конструираме нови екземпляри на клас, но в някои случаи това означава получаване на вече конструиран екземпляр, готов за нас.

В тази статия ще разгледаме някои често срещани дизайнерски модели. Ще видим как изглеждат и къде да ги намерим в JVM или други основни библиотеки.

2. Фабричен метод

Шаблонът на фабричния метод е начин за нас да отделим конструкцията на екземпляр от класа, който конструираме. Това е, за да можем да абстрахираме точния тип, позволявайки на нашия клиентски код вместо това да работи по отношение на интерфейси или абстрактни класове:

class SomeImplementation implements SomeInterface { // ... } 
public class SomeInterfaceFactory { public SomeInterface newInstance() { return new SomeImplementation(); } }

Тук нашият клиентски код никога не трябва да знае за SomeImplementation и вместо това работи по отношение на SomeInterface . Дори повече от това обаче, можем да променим типа, върнат от нашата фабрика и клиентският код не е необходимо да се променя . Това може дори да включва динамичен избор на типа по време на изпълнение.

2.1. Примери в JVM

Вероятно най-известните примери за този модел JVM са методите за изграждане на колекции в класа Collections , като singleton () , singletonList () и singletonMap (). Всички тези връщани екземпляри на подходящата колекция - Set , List или Map - но точният тип е без значение . Освен това методът Stream.of () и новите методи Set.of () , List.of () и Map.ofEntries () ни позволяват да направим същото с по-големи колекции.

Има и много други примери за това, включително Charset.forName () , който ще върне различен екземпляр на класа Charset в зависимост от исканото име и ResourceBundle.getBundle () , който ще зареди различен пакет ресурси в зависимост от на посоченото име.

Не всички от тях трябва да предоставят различни случаи. Някои са просто абстракции, за да скрият вътрешната работа. Например Calendar.getInstance () и NumberFormat.getInstance () винаги връщат един и същ екземпляр, но точните подробности са ирелевантни за клиентския код.

3. Абстрактна фабрика

Моделът на абстрактна фабрика е стъпка отвъд това, където използваната фабрика също има абстрактен основен тип. След това можем да напишем нашия код по отношение на тези абстрактни типове и да изберете конкретния фабричен екземпляр по някакъв начин по време на изпълнение.

Първо, имаме интерфейс и някои конкретни изпълнения за функционалността, която всъщност искаме да използваме:

interface FileSystem { // ... } 
class LocalFileSystem implements FileSystem { // ... } 
class NetworkFileSystem implements FileSystem { // ... } 

След това имаме интерфейс и някои конкретни изпълнения за фабриката, за да получим горното:

interface FileSystemFactory { FileSystem newInstance(); } 
class LocalFileSystemFactory implements FileSystemFactory { // ... } 
class NetworkFileSystemFactory implements FileSystemFactory { // ... } 

След това имаме друг фабричен метод за получаване на абстрактната фабрика, чрез който можем да получим действителния екземпляр:

class Example { static FileSystemFactory getFactory(String fs) { FileSystemFactory factory; if ("local".equals(fs)) { factory = new LocalFileSystemFactory(); else if ("network".equals(fs)) { factory = new NetworkFileSystemFactory(); } return factory; } }

Тук имаме интерфейс FileSystemFactory, който има две конкретни реализации. Избираме точното изпълнение по време на изпълнение, но кодът, който го използва, не трябва да се интересува кой екземпляр всъщност се използва . След това всеки от тях връща различен конкретен екземпляр на интерфейса на FileSystem , но отново нашият код не трябва да се интересува точно кой екземпляр от това имаме.

Често получаваме самата фабрика, използвайки друг фабричен метод, както е описано по-горе. В нашия пример тук методът getFactory () сам по себе си е фабричен метод, който връща абстрактна FileSystemFactory, която след това се използва за конструиране на FileSystem .

3.1. Примери в JVM

Има много примери за този модел на дизайн, използван в JVM. Най-често се виждат около XML пакетите - например DocumentBuilderFactory , TransformerFactory и XPathFactory . Всички те имат специален фабричен метод newInstance () , който позволява на нашия код да получи екземпляр на абстрактната фабрика .

Вътрешно този метод използва редица различни механизми - системни свойства, конфигурационни файлове в JVM и интерфейса на доставчика на услуги - за да се опита и да реши точно кой конкретен екземпляр да използва. Тогава това ни позволява да инсталираме алтернативни XML библиотеки в нашето приложение, ако желаем, но това е прозрачно за всеки код, който действително ги използва.

След като нашият код извика метода newInstance () , той ще има екземпляр на фабриката от съответната XML библиотека. След това тази фабрика конструира действителните класове, които искаме да използваме от същата тази библиотека.

Например, ако използваме внедряването на Xerces по подразбиране на JVM, ще получим екземпляр на com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl , но ако вместо това искаме да използваме различна реализация, тогава извикване newInstance () би я върнал прозрачно вместо това.

4. Строител

Моделът Builder е полезен, когато искаме да конструираме сложен обект по по-гъвкав начин. Той работи, като има отделен клас, който използваме за изграждането на нашия сложен обект и позволява на клиента да създаде това с по-опростен интерфейс:

class CarBuilder { private String make = "Ford"; private String model = "Fiesta"; private int doors = 4; private String color = "White"; public Car build() { return new Car(make, model, doors, color); } }

Това ни позволява да предоставяме индивидуално стойности за марка , модел , врати и цвят , а след това, когато изграждаме автомобила , всички аргументи на конструктора се решават на съхранените стойности.

4.1. Примери в JVM

Има някои много ключови примери за този модел в JVM. На StringBuilder и StringBuffer класовете са строители, които ни позволяват да се изгради един дълъг низ чрез осигуряване на много малки части . По-новият клас Stream.Builder ни позволява да направим абсолютно същото, за да изградим Stream :

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

5. Мързелива инициализация

Използваме модела на мързелива инициализация, за да отложим изчисляването на някаква стойност, докато не е необходимо. Понякога това може да включва отделни парчета данни, а друг път това може да означава цели обекти.

Това е полезно в редица сценарии. Например, ако цялостното конструиране на обект изисква достъп до база данни или мрежа и може никога да не се наложи да го използваме, тогава извършването на тези повиквания може да доведе до недостатъчно изпълнение на приложението ни . Като алтернатива, ако изчисляваме голям брой стойности, които може никога да не ни трябват, това може да доведе до ненужно използване на паметта.

Обикновено това работи, като един обект е мързеливата обвивка около данните, от които се нуждаем, и данните се изчисляват при достъп чрез метод за получаване:

class LazyPi { private Supplier calculator; private Double value; public synchronized Double getValue() { if (value == null) { value = calculator.get(); } return value; } }

Изчисляването на pi е скъпа операция и такава, която може да не ни се наложи да извършим. Горното ще направи това за първи път, когато извикаме getValue (), а не преди.

5.1. Примери в JVM

Examples of this in the JVM are relatively rare. However, the Streams API introduced in Java 8 is a great example. All of the operations performed on a stream are lazy, so we can perform expensive calculations here and know they are only called if needed.

However, the actual generation of the stream itself can be lazy as well. Stream.generate() takes a function to call whenever the next value is needed and is only ever called when needed. We can use this to load expensive values – for example, by making HTTP API calls – and we only pay the cost whenever a new element is actually needed:

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

Here, we have a Supplier that will make HTTP calls to load articles, filter them based on the associated tags, and then return the first matching title. If the very first article loaded matches this filter, then only a single network call needs to be made, regardless of how many articles are actually present.

6. Object Pool

We'll use the Object Pool pattern when constructing a new instance of an object that may be expensive to create, but re-using an existing instance is an acceptable alternative. Instead of constructing a new instance every time, we can instead construct a set of these up-front and then use them as needed.

The actual object pool exists to manage these shared objects. It also tracks them so that each one is only used in one place at the same time. In some cases, the entire set of objects gets constructed only at the start. In other cases, the pool may create new instances on demand if it's necessary

6.1. Examples in the JVM

The main example of this pattern in the JVM is the use of thread pools. An ExecutorService will manage a set of threads and will allow us to use them when a task needs to execute on one. Using this means that we don't need to create new threads, with all of the cost involved, whenever we need to spawn an asynchronous task:

ExecutorService pool = Executors.newFixedThreadPool(10); pool.execute(new SomeTask()); // Runs on a thread from the pool pool.execute(new AnotherTask()); // Runs on a thread from the pool

These two tasks get allocated a thread on which to run from the thread pool. It might be the same thread or a totally different one, and it doesn't matter to our code which threads are used.

7. Prototype

We use the Prototype pattern when we need to create new instances of an object that are identical to the original. The original instance acts as our prototype and gets used to construct new instances that are then completely independent of the original. We can then use these however is necessary.

Java has a level of support for this by implementing the Cloneable marker interface and then using Object.clone(). This will produce a shallow clone of the object, creating a new instance, and copying the fields directly.

This is cheaper but has the downside that any fields inside our object that have structured themselves will be the same instance. This, then, means changes to those fields also happen across all instances. However, we can always override this ourselves if necessary:

public class Prototype implements Cloneable { private Map contents = new HashMap(); public void setValue(String key, String value) { // ... } public String getValue(String key) { // ... } @Override public Prototype clone() { Prototype result = new Prototype(); this.contents.entrySet().forEach(entry -> result.setValue(entry.getKey(), entry.getValue())); return result; } }

7.1. Examples in the JVM

The JVM has a few examples of this. We can see these by following the classes that implement the Cloneable interface. For example, PKIXCertPathBuilderResult, PKIXBuilderParameters, PKIXParameters, PKIXCertPathBuilderResult, and PKIXCertPathValidatorResult are all Cloneable.

Another example is the java.util.Date class. Notably, this overrides the Object.clone() method to copy across an additional transient field as well.

8. Singleton

The Singleton pattern is often used when we have a class that should only ever have one instance, and this instance should be accessible from throughout the application. Typically, we manage this with a static instance that we access via a static method:

public class Singleton { private static Singleton instance = null; public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }

There are several variations to this depending on the exact needs — for example, whether the instance is created at startup or on first use, whether accessing it needs to be threadsafe, and whether or not there needs to be a different instance per thread.

8.1. Examples in the JVM

The JVM has some examples of this with classes that represent core parts of the JVM itselfRuntime, Desktop, and SecurityManager. These all have accessor methods that return the single instance of the respective class.

Additionally, much of the Java Reflection API works with singleton instances. The same actual class always returns the same instance of Class, regardless of whether it's accessed using Class.forName(), String.class, or through other reflection methods.

По подобен начин бихме могли да разгледаме екземпляра Thread, представляващ текущата нишка, за единичен. Често има много случаи на това, но по дефиниция има по един екземпляр на нишка. Извикването на Thread.currentThread () от всяко място, изпълняващо се в една и съща нишка, винаги ще връща един и същ екземпляр.

9. Обобщение

В тази статия разгледахме различни дизайнерски модели, използвани за създаване и получаване на копия на обекти. Също така разгледахме примери за тези модели, използвани и в основния JVM, така че можем да ги видим в употреба по начин, от който много приложения вече се възползват.