«1. Введение

Шаблоны проектирования — это общие шаблоны, которые мы используем при написании нашего программного обеспечения. Они представляют собой устоявшиеся лучшие практики, разработанные с течением времени. Затем они могут помочь нам убедиться, что наш код хорошо спроектирован и хорошо построен.

Creational Patterns — это шаблоны проектирования, которые сосредоточены на том, как мы получаем экземпляры объектов. Обычно это означает, как мы создаем новые экземпляры класса, но в некоторых случаях это означает получение уже созданного экземпляра, готового для использования.

В этой статье мы собираемся вернуться к некоторым распространенным творческим шаблонам проектирования. Мы увидим, как они выглядят и где их найти в 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(), которая загрузит другой экземпляр класса Charset. пакет ресурсов в зависимости от предоставленного имени.

Не все из них также должны предоставлять разные экземпляры. Некоторые просто абстракции, чтобы скрыть внутреннюю работу. Например, 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-библиотеки. Затем эта фабрика создает фактические классы, которые мы хотим использовать, из той же библиотеки.

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);
    }
}

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

4. Builder

Шаблон Builder полезен, когда мы хотим создать сложный объект более гибким способом. Он работает, имея отдельный класс, который мы используем для создания нашего сложного объекта, и позволяя клиенту создавать его с более простым интерфейсом:

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();

Это позволяет нам индивидуально предоставлять значения для марки, модели, дверей и цвета, а затем, когда мы создаем Car, все аргументы конструктора преобразуются в сохраненные значения.

4.1. Примеры в JVM

Есть несколько очень важных примеров использования этого шаблона в JVM. Классы StringBuilder и StringBuffer — это конструкторы, которые позволяют нам создавать длинную строку, предоставляя множество мелких частей. Более новый класс Stream.Builder позволяет нам делать то же самое для создания потока:

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

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

Примеры этого в JVM относительно редки. Однако Streams API, представленный в Java 8, является отличным примером. Все операции, выполняемые над потоком, ленивы, поэтому здесь мы можем выполнять дорогостоящие вычисления и знать, что они вызываются только в случае необходимости.

Однако фактическая генерация самого потока также может быть ленивой. Stream.generate() принимает функцию для вызова всякий раз, когда требуется следующее значение, и вызывается только тогда, когда это необходимо. Мы можем использовать это для загрузки дорогостоящих значений — например, путем выполнения вызовов HTTP API — и мы оплачиваем стоимость только тогда, когда новый элемент действительно необходим:

Здесь у нас есть Поставщик, который будет делать HTTP-вызовы для загрузки статей, фильтровать их на основе связанных тегов, а затем возвращать первый соответствующий заголовок. Если самая первая загруженная статья соответствует этому фильтру, то необходимо сделать только один сетевой вызов, независимо от того, сколько статей присутствует на самом деле.

6. Пул объектов

Мы будем использовать шаблон пула объектов при создании нового экземпляра объекта, создание которого может быть дорогостоящим, но приемлемой альтернативой является повторное использование существующего экземпляра. Вместо того, чтобы каждый раз создавать новый экземпляр, мы можем заранее создать набор из них, а затем использовать их по мере необходимости.

«Фактический пул объектов существует для управления этими общими объектами. Он также отслеживает их, чтобы каждый из них использовался только в одном месте в одно и то же время. В некоторых случаях весь набор объектов строится только на старте. В других случаях пул может создавать новые экземпляры по требованию, если это необходимо

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

6.1. Примеры в JVM

Основным примером этого шаблона в JVM является использование пулов потоков. ExecutorService будет управлять набором потоков и позволит нам использовать их, когда задача должна выполняться в одном из них. Использование этого означает, что нам не нужно создавать новые потоки со всеми связанными с этим затратами всякий раз, когда нам нужно создать асинхронную задачу:

Этим двум задачам выделяется поток, в котором они запускаются из потока. бассейн. Это может быть тот же поток или совершенно другой, и для нашего кода не имеет значения, какие потоки используются.

7. Прототип

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

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;
    }
}

В Java есть поддержка для этого путем реализации интерфейса маркера Cloneable и последующего использования Object.clone(). Это приведет к созданию поверхностного клона объекта, созданию нового экземпляра и непосредственному копированию полей.

Это дешевле, но имеет недостаток, заключающийся в том, что любые поля внутри нашего объекта, которые сами себя структурировали, будут одним и тем же экземпляром. Это означает, что изменения в этих полях также происходят во всех экземплярах. Однако при необходимости мы всегда можем переопределить это сами:

7.1. Примеры в JVM

В JVM есть несколько таких примеров. Мы можем увидеть это, следуя классам, которые реализуют интерфейс Cloneable. Например, PKIXCertPathBuilderResult, PKIXBuilderParameters, PKIXParameters, PKIXCertPathBuilderResult и PKIXCertPathValidatorResult являются клонируемыми.

Другой пример — класс java.util.Date. Примечательно, что это переопределяет метод Object.clone() для копирования в дополнительное переходное поле.

public class Singleton {
    private static Singleton instance = null;

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

8. Singleton

Шаблон Singleton часто используется, когда у нас есть класс, у которого должен быть только один экземпляр, и этот экземпляр должен быть доступен из всего приложения. Как правило, мы управляем этим с помощью статического экземпляра, к которому мы обращаемся через статический метод:

Существует несколько вариантов этого в зависимости от конкретных потребностей — например, создается ли экземпляр при запуске или use, должен ли доступ к нему быть потокобезопасным и должен ли быть другой экземпляр для каждого потока.

8.1. Примеры в JVM

В JVM есть несколько примеров этого с классами, которые представляют основные части самой JVM — Runtime, Desktop и SecurityManager. Все они имеют методы доступа, которые возвращают единственный экземпляр соответствующего класса.

Кроме того, большая часть Java Reflection API работает с одноэлементными экземплярами. Один и тот же фактический класс всегда возвращает один и тот же экземпляр Class, независимо от того, осуществляется ли доступ к нему с помощью Class.forName(), String.class или других методов отражения.

Аналогичным образом мы можем рассматривать экземпляр Thread, представляющий текущий поток, как синглтон. Часто бывает много таких экземпляров, но по определению на каждый поток приходится один экземпляр. Вызов Thread.currentThread() из любого места, выполняющегося в одном и том же потоке, всегда будет возвращать один и тот же экземпляр.