Творчі шаблони дизайну в Core Java

1. Вступ

Шаблони дизайну - це загальні шаблони, які ми використовуємо під час написання нашого програмного забезпечення . Вони представляють усталені найкращі практики, розроблені з часом. Потім вони можуть допомогти нам забезпечити, щоб наш код був добре розроблений і добре побудований.

Творчі шаблони - це шаблони дизайну, які зосереджуються на тому, як ми отримуємо екземпляри об’єктів . Як правило, це означає, як ми створюємо нові екземпляри класу, але в деяких випадках це означає отримання вже побудованого екземпляра, готового до використання.

У цій статті ми збираємося переглянути деякі загальні шаблони креативного дизайну. Ми побачимо, як вони виглядають і де їх можна знайти в JVM або інших основних бібліотеках.

2. Заводський метод

Шаблон Factory Factory - це спосіб для нас відокремити конструкцію примірника від класу, який ми будуємо. Це так, щоб ми могли абстрагувати точний тип, дозволяючи нашому клієнтському коду працювати замість інтерфейсів або абстрактних класів:

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 () factory, що дозволяє нашому коду отримувати екземпляр абстрактної фабрики .

Внутрішньо цей метод використовує ряд різних механізмів - властивості системи, файли конфігурації в 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.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; } }

Обчислення пі - це дорога операція, і нам може не знадобитися виконати. Вищезазначене буде робити це вперше, коли ми називаємо 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, тому ми можемо бачити їх використання таким чином, що багато додатків вже отримують вигоду.