1. Введение
Шаблоны проектирования — это общие шаблоны, которые мы используем при написании нашего программного обеспечения . Они представляют собой устоявшиеся лучшие практики, разработанные с течением времени. Затем они могут помочь нам убедиться, что наш код хорошо спроектирован и хорошо построен.
Creational Patterns — это шаблоны проектирования, которые сосредоточены на том, как мы получаем экземпляры объектов . Обычно это означает, как мы создаем новые экземпляры класса, но в некоторых случаях это означает получение уже созданного экземпляра, готового для использования.
В этой статье мы собираемся вернуться к некоторым распространенным шаблонам креативного дизайна. Мы увидим, как они выглядят и где их найти в JVM или других основных библиотеках.
2. Заводской метод
Шаблон Factory Method — это способ отделить конструкцию экземпляра от класса, который мы создаем. Это сделано для того, чтобы мы могли абстрагироваться от точного типа, позволяя нашему клиентскому коду вместо этого работать в терминах интерфейсов или абстрактных классов:
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);
}
}
Это позволяет нам индивидуально предоставлять значения для make
, model
, door
и color
, а затем, когда мы создаем Car
, все аргументы конструктора преобразуются в сохраненные значения.
4.1. Примеры в JVM
В JVM есть несколько очень важных примеров этого шаблона. Классы StringBuilder
и StringBuffer
являются компоновщиками, которые позволяют нам создавать длинную строку
, предоставляя множество мелких частей . Более новый класс Stream.Builder
позволяет нам делать то же самое для создания Stream
:
Stream.Builder<Integer> builder = Stream.builder<Integer>();
builder.add(1);
builder.add(2);
if (condition) {
builder.add(3);
builder.add(4);
}
builder.add(5);
Stream<Integer> stream = builder.build();
5. Ленивая инициализация
Мы используем шаблон ленивой инициализации, чтобы отложить вычисление некоторого значения до тех пор, пока оно не понадобится. Иногда это могут быть отдельные фрагменты данных, а иногда это могут быть целые объекты.
Это полезно в ряде сценариев. Например, если для полного построения объекта требуется доступ к базе данных или сети, и нам может никогда не понадобиться его использовать, то выполнение этих вызовов может привести к снижению производительности нашего приложения . В качестве альтернативы, если мы вычисляем большое количество значений, которые нам могут никогда не понадобиться, это может привести к ненужному использованию памяти.
Как правило, это работает, когда один объект является ленивой оболочкой для данных, которые нам нужны, и когда данные вычисляются при доступе через метод получения:
class LazyPi {
private Supplier<Double> calculator;
private Double value;
public synchronized Double getValue() {
if (value == null) {
value = calculator.get();
}
return value;
}
}
Вычисление числа пи — дорогостоящая операция, которую нам, возможно, не нужно выполнять. Вышеприведенное будет делать это при первом вызове getValue()
, а не раньше.
5.1. Примеры в JVM
Примеры этого в JVM относительно редки. Однако Streams API , представленный в Java 8, является отличным примером. Все операции, выполняемые над потоком, ленивы , поэтому здесь мы можем выполнять дорогостоящие вычисления и знать, что они вызываются только в случае необходимости.
Однако фактическая генерация самого потока также может быть ленивой . Stream.generate()
принимает функцию для вызова всякий раз, когда требуется следующее значение, и вызывается только тогда, когда это необходимо. Мы можем использовать это для загрузки дорогостоящих значений — например, путем выполнения вызовов HTTP API — и мы оплачиваем стоимость только тогда, когда новый элемент действительно необходим:
Stream.generate(new ForEachArticlesLoader())
.filter(article -> article.getTags().contains("java-streams"))
.map(article -> article.getTitle())
.findFirst();
Здесь у нас есть поставщик
, который будет выполнять HTTP-вызовы для загрузки статей, фильтровать их на основе связанных тегов, а затем возвращать первый соответствующий заголовок. Если самая первая загруженная статья соответствует этому фильтру, то необходимо сделать только один сетевой вызов, независимо от того, сколько статей присутствует на самом деле.
6. Пул объектов
Мы будем использовать шаблон пула объектов при создании нового экземпляра объекта, создание которого может быть дорогостоящим, но приемлемой альтернативой является повторное использование существующего экземпляра. Вместо того, чтобы каждый раз создавать новый экземпляр, мы можем заранее создать набор из них, а затем использовать их по мере необходимости.
Фактический пул объектов существует для управления этими общими объектами . Он также отслеживает их, чтобы каждый из них использовался только в одном месте в одно и то же время. В некоторых случаях весь набор объектов строится только на старте. В других случаях пул может создавать новые экземпляры по запросу, если это необходимо.
6.1. Примеры в JVM
Основным примером этого паттерна в JVM является использование пулов потоков . ExecutorService будет управлять набором
потоков и позволит нам использовать их, когда задача должна выполняться в одном из них. Использование этого означает, что нам не нужно создавать новые потоки со всеми связанными с этим затратами всякий раз, когда нам нужно создать асинхронную задачу:
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
Этим двум задачам выделяется поток для запуска из пула потоков. Это может быть один и тот же поток или совершенно другой, и для нашего кода не имеет значения, какие потоки используются.
7. Прототип
Мы используем шаблон прототипа, когда нам нужно создать новые экземпляры объекта, идентичные оригиналу. Исходный экземпляр действует как наш прототип и используется для создания новых экземпляров, которые затем полностью независимы от оригинала. Затем мы можем использовать их, однако это необходимо.
Java имеет уровень поддержки для этого, реализуя интерфейс маркера Cloneable
, а затем используя Object.clone()
. Это приведет к созданию поверхностного клона объекта, созданию нового экземпляра и непосредственному копированию полей.
Это дешевле, но имеет недостаток, заключающийся в том, что любые поля внутри нашего объекта, которые сами себя структурировали, будут одним и тем же экземпляром. Это означает, что изменения в этих полях также происходят во всех экземплярах. Однако мы всегда можем переопределить это сами, если это необходимо:
public class Prototype implements Cloneable {
private Map<String, String> 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. Примеры в JVM
В JVM есть несколько таких примеров. Мы можем увидеть это, следуя классам, которые реализуют интерфейс Cloneable
. Например, PKIXCertPathBuilderResult
, PKIXBuilderParameters
, PKIXParameters
, PKIXCertPathBuilderResult
и PKIXCertPathValidatorResult
являются клонируемыми .
Другой пример — класс java.util.Date .
Примечательно, что это переопределяет Object.
clone()
для копирования в дополнительное переходное поле .
8. Синглтон
Шаблон Singleton часто используется, когда у нас есть класс, у которого должен быть только один экземпляр, и этот экземпляр должен быть доступен из всего приложения. Обычно мы управляем этим с помощью статического экземпляра, к которому мы обращаемся через статический метод:
public class Singleton {
private static Singleton instance = null;
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
Существует несколько вариантов этого в зависимости от конкретных потребностей — например, создается ли экземпляр при запуске или при первом использовании, должен ли доступ к нему быть потокобезопасным и должен ли быть другой экземпляр для каждого потока.
8.1. Примеры в JVM
В JVM есть несколько примеров этого с классами, которые представляют основные части самой JVM — Runtime, Desktop
и SecurityManager
. Все они имеют методы доступа, которые возвращают единственный экземпляр соответствующего класса.
Кроме того, большая часть Java Reflection API работает с одноэлементными экземплярами . Один и тот же фактический класс всегда возвращает один и тот же экземпляр Class,
независимо от того, осуществляется ли доступ к нему с помощью Class.forName()
, String.class
или других методов отражения.
Аналогичным образом мы можем рассматривать экземпляр Thread
, представляющий текущий поток, как синглтон. Часто бывает много таких экземпляров, но по определению на каждый поток приходится один экземпляр. Вызов Thread.currentThread()
из любого места, выполняющегося в одном и том же потоке, всегда будет возвращать один и тот же экземпляр.
9. Резюме
В этой статье мы рассмотрели различные шаблоны проектирования, используемые для создания и получения экземпляров объектов. Мы также рассмотрели примеры использования этих шаблонов в основной JVM, поэтому мы можем увидеть их использование таким образом, который уже приносит пользу многим приложениям.