Java: Порождающие шаблоны проектирования

4402

November 25, 2017

Шаблон проектирования или паттерн (англ. design pattern) в разработке программного обеспечения — повторяемая архитектурная конструкция, представляющая собой решение проблемы проектирования в рамках некоторого часто возникающего контекста. (Wikipedia)

1. Вступление

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

Широкое применение термин Шаблон проектирования получил после выпуска книги Шаблоны проектирования: элементы повторяемого объектно-ориентированного программного обеспечения, выпущенной в 1994 году авторами by Erich Gamma, John Vlissides, Ralph Johnson, and Richard Helm (также известными как Gang of Four or GoF).

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

2. Порождающие шаблоны проектирования (Creational Design Patterns)

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

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

В этой статье мы поговорим про такие порождающие шаблоны проектирования, как:

  1. Синглтон (одиночка) - Singleton
  2. Фабричный метод - Facroty method
  3. Абстрактная фабрика - Abstract factory
  4. Строитель - Builder

3. Синглтон (одиночка) - Singleton design pattern

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

Достигается это путем скрытия конструктора класса и предоставления единого метода, который возрващает единственный экземпляр объекта класса.

3.1. Примеры шаблона проектирования Синглтон (одиночка)

Оригинальный вариант, предложенный GoF, в реализации на языке Java выглядит так:


public class MySingleton {

    private static MySingleton _instance;
    
    private MySingleton() {
    }
    
    public static MySingleton getInstance() {
        if (MySingleton._instance == null) {
            MySingleton._instance = new MySingleton();
        }
        return MySingleton._instance;
    }
    
}

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

Данная реализация очень хорошо подоходит для маленьких однопоточных программ, поскольку в случае вызова метода getInstance() одновременно из разных потоков возможна ситуация, когда код создания объекта класса

if (MySingleton._instance == null) {
    MySingleton._instance = new MySingleton();
}

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

Для избежания подобных ситуаций требуется обеспечить потоко-безопасное (thread-safely) создание единичного экземпляра данного класса.

Этого можно добиться, например, поместив код создания экземпляра класса в блок synchronized:


    public static MySingleton getInstance() {
        synchronized(MySingleton.class) {
            if (MySingleton._instance == null) {
                MySingleton._instance = new MySingleton();
            }
        }
        return MySingleton._instance;
    }

Или объявить весь метод getInstance() как synchronized:


    public static synchronized MySingleton getInstance() {
        if (MySingleton._instance == null) {
            MySingleton._instance = new MySingleton();
        }
        return MySingleton._instance;
    }

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

Но избежать этого можно. Если мызнаем, что наш класс-синглтон точно будет использоваться, то нам не важно, в какой момент времени создастся объект класса. Поэтому можно создать объект нашего класса заранее - в статическом инициализаторе:


public class MySingleton {

    private static MySingleton _instance;
    
    static {
        MySingleton._instance = new MySingleton();
    }
    
    private MySingleton() {
    }
    
    public static MySingleton getInstance() {
        return MySingleton._instance;
    }
    
}

В некоторых источниках может встретиться вариант потоко-безопасного класса-синглтона с хранением экземпляра класса в приватном статическом классе:


public class MySingleton  {    
    private MySingleton() {}
     
    private static class SingletonHolder {    
        public static final MySingleton instance = new MySingleton();
    }
 
    public static MySingleton getInstance() {    
        return SingletonHolder.instance;    
    }
}

3.2. Когда нужно использовать шаблон проектирования Синглтон

  1. Для ресурсов, которые являются дорогостоящими для создания (например, объектов подключения к базе данных)
  2. Хорошей практикой является сохранение всех систем логгирования как синглтонов, что повышает производительность
  3. Классы, обеспечивающие доступ к настройкам конфигурации для приложения
  4. Классы, содержащие ресурсы, которые доступны в режиме совместного доступа

4. Шаблон проектирования Фабричный метод - Factory Method Design Pattern,

в английской литературе также известен под термином Виртуальный конструктор - Virtual Constructor

Шаблон проектирования Фабричный метод - это наиболее часто используемый на практике шаблон проектирования.

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

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

Для этого мы полагаемся на класс, который предоставляет нам объекты, скрывая фактические детали реализации. Доступ к созданным объектам осуществляется с использованием общего интерфейса.

4.1 Пример использования Фабричного метода

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

Структура программы создания полигонов

Для начала создадим интерфейс Polygon:


public interface Polygon {
    String getType();
}

Теперь нам необходимо имплементировать наши классы геометрических фигур от единого интерфейса Polygon:


public class Triangle implements Polygon {
    //...
    public String getType() {
        return "TRIANGLE";
    }
    //...
}

public class Square implements Polygon {
    //...
    public String getType() {
        return "SQUARE";
    }
    //...
}

public class Pentagon implements Polygon {
    //...
    public String getType() {
        return "PENTAGON";
    }
    //...
}

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


public class PolygonFactory {
    
    public Polygon createPolygon(int numberOfSides) {
        if (numberOfSides == 3) {
            return new Triangle();
        } else if (numberOfSides == 4) {
            return new Square();
        } else if (numberOfSides == 5) {
            return new Pentagon();
        } else {
            return null;
        }
    }
    
}

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


public static void main(String[] args) {
    PolygonFactory pf = new PoligonFactory();
    
    System.out.println(pf.createPolygon(3).getType()); //TRIANGLE
    System.out.println(pf.createPolygon(4).getType()); //SQUARE
    System.out.println(pf.createPolygon(5).getType()); //PENTAGON
}

4.2. Когда следует использовать шаблон проектирования Фабричный метод

  1. Когда ожидается, что реализация интерфейса или абстрактного класса будет часто меняться
  2. Когда текущая реализация не может комфортно приспособиться к новым изменениям
  3. Когда процесс инициализации относительно прост, и конструктору требуется только несколько параметров

5. Шаблон проектирования Абстрактная фабрика - Abstract factory design pattern

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

Напротив, шаблон проектирования Абстрактная фабрика предназначен для реализации механизма создания объектов связанных или зависимых семейств. Этот шаблон также называют Фабрикой фабрик.

В определении GoF указано, что Абстрактная фабрика "предоставляет интерфейс для создания семей связанных или зависимых объектов без указания их конкретных классов".

5.1. Пример использования шаблона проектирования Абстрактная фабрика

В примере мы создадим два фабричных метода AnimalFactory и ColorFactory, а затем организуем к ним доступ, используя абстрактную фабрику.

Для начала создадим семейство классов Animal, мы будем его использовать при реализации абстрактной фабрики.


public interface Animal {
    String getAnimal();
    String makeSound();
}

Реализацией этого интерфейса будте класс Duck:


public class Duck implements Animal {
 
    @Override
    public String getAnimal() {
        return "Duck";
    }
 
    @Override
    public String makeSound() {
        return "Squeks";
    }
}

Аналогичным образом мы можем создать и другие классы, реализующие интерфейс Animal: Dog, Cat, Rat.

Абстрактная фабрика имеет дело с семействами зависимых объектов. Поэтому мы создадим другое семейство: Color - интерфейс с реализациями в классах Brown, White, Grey.

Теперь, когда у нас есть несколько семейств классов, приступим к реализации Абстрактной фабрики.


public interface AbstractFactory {
    Animal getAnimal(String animalType) ;
    Color getColor(String colorType);
}

Теперь необходимо реализовать этот интерфес в класс, используя шаблон проектирования Фабричный метод, описанный ранее.


public class AnimalFactory implements AbstractFactory {
 
    @Override
    public Animal getAnimal(String animalType) {
        if ("Dog".equalsIgnoreCase(animalType)) {
            return new Dog();
        } else if ("Duck".equalsIgnoreCase(animalType)) {
            return new Duck();
        }
 
        return null;
    }
 
    @Override
    public Color getColor(String color) {
        throw new UnsupportedOperationException();
    }
 
}

Аналогичным образом следует реализовать фабрику цветов ColorFactory.

Когда все готово, создадим класс FactoryProvider, который предоставит нам одну из реализаций AnimalFactory и ColorFactory в зависимости от значения параметра метода:


public class FactoryProvider {
    public static AbstractFactory getFactory(String choice) {
         
        if("Animal".equalsIgnoreCase(choice)) {
            return new AnimalFactory();
        }
        else if("Color".equalsIgnoreCase(choice)) {
            return new ColorFactory();
        }
         
        return null;
    }
}

Именно поэтому этот шаблон проектирования также зовется Фабрика фабрик.

5.2. Когда применяется шаблон проектирования Абстрактная фабрика

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

6. Порождающий шаблон проектирования Строитель - Builder design pattern

Шаблон проектирования Строитель - это еще один пораждающий шаблон, предназначенный для решения задач построения относительно сложных объектов.

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

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

6.1. Пример реализации шаблона проектирования Строитель

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

Джошуа Блох (Joshua Bloch) в своей книге «Эффективная Java» представил улучшенную версию шаблона Строителя, которая является чистой, легко читаемой (поскольку она использует поточную инициализацию) и проста в использовании с точки зрения клиента. В этом примере мы обсудим эту версию.

В примере у нас будет только один класс BankAccount, который содержит построитель в виде статического метода класса.


public class BankAccount {
     
    private String name;
    private String accountNumber;
    private String email;
    private boolean newsletter;
 
    // constructors/getters
    private BankAccount(BankAccountBuilder builder) {
        this.name           = builder.name;
        this.accountNumber  = builder.accountNumber;
        this.email          = builder.email;
        this.newsletter     = builder.newsletter;
    }
    
    public static class BankAccountBuilder {
        // builder code
    }
}

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

Все свойства класса BankAccoun инициализируются в конструкторе класса из параметра с типом Строитель.

Объявим Строитель BankAccountBuilder как статический вложенный класс:


public static class BankAccountBuilder {
     
    private String name;
    private String accountNumber;
    private String email;
    private boolean newsletter;
     
    public BankAccountBuilder(String name, String accountNumber) {
        this.name = name;
        this.accountNumber = accountNumber;
    }
 
    public BankAccountBuilder withEmail(String email) {
        this.email = email;
        return this;
    }
 
    public BankAccountBuilder wantNewsletter(boolean newsletter) {
        this.newsletter = newsletter;
        return this;
    }
     
    public BankAccount build() {
        return new BankAccount(this);
    }
}

Обратите внимание, мы объявили тот же самый набор свойств Строителя, что и основного класса BankAccount.

В качестве аргументов конструктора внутреннего класса требуются обязательные поля, а остальные необязательные поля могут быть заданы с использованием setter-методов.

Эта реализация также поддерживает поточную инициализацию, поскольку setter-методы возвращают объект класса Строитель.

Наконец, метод построения вызывает частный конструктор внешнего класса и передает себя в качестве аргумента. Возвращенный BankAccount будет создан с параметрами, установленными BankAccountBuilder.

Давайте посмотрим на использование Строителя в деле:


BankAccount newAccount = new BankAccount
  .BankAccountBuilder("Jon", "22738022275")
  .withEmail("jon@example.com")
  .wantNewsletter(true)
  .build();

6.2. Когда следует использовать шаблон проектирования Строитель

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

В заключение

В этой статье мы узнали о пораждающих шаблонах проектирования и их реализации в Java. Мы также обсудили их четыре разных типа, например, Синглтон (Singleton), Фабричный метод (Factory Method), Абстрактная фабрика (Abstract Factory) и Строитель (Builder Pattern), их преимущества, примеры и когда их использовать.