Рубрики
Без рубрики

Шаблоны творческого проектирования в ядре Java

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

Автор оригинала: Graham Cox.

1. введение

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

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

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

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

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

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

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

2.1. Примеры в СП

Возможно, наиболее известными примерами этого шаблона в 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;
    }
}

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

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

3.1. Примеры в СП

Существует множество примеров этого шаблона проектирования, используемого во всей JVM. Наиболее часто встречаются пакеты XML — например, DocumentBuilderFactory , TransformerFactory, и XPathFactory . Все они имеют специальный метод newInstance() factory, позволяющий нашему коду получить экземпляр абстрактной фабрики .

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

Как только наш код вызовет метод newInstance () , у него будет экземпляр фабрики из соответствующей библиотеки XML. Затем эта фабрика создает фактические классы, которые мы хотим использовать из той же библиотеки.

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

4. Строитель

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

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 , doors и color , а затем , когда мы строим Car , все аргументы конструктора преобразуются в сохраненные значения.

4.1. Примеры в СПМ

В 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. Ленивая инициализация

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

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

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

class LazyPi {
    private Supplier calculator;
    private Double value;

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

Вычисление пи-это дорогостоящая операция, и нам, возможно, не понадобится ее выполнять. Вышесказанное будет сделано в первый раз, когда мы вызовем GetValue () , а не раньше.

5.1. Примеры в СПМ

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

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

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

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

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

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

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

6.1. Примеры в СП

Основным примером этого шаблона в 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 marker, а затем используя Object.clone() . Это приведет к созданию неглубокого клона объекта, созданию нового экземпляра и непосредственному копированию полей.

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

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. Примеры в СП

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

Другим примером является java.util.Дата класс. Примечательно, что это переопределяет объект . 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 есть несколько примеров этого с классами, которые представляют основные части самой JVMRuntime, Desktop, и SecurityManager . Все они имеют методы доступа, которые возвращают единственный экземпляр соответствующего класса.

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

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

9. Резюме

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