Порождающие паттерны проектирования: для каких задач нужны, виды и примеры реализации

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

Другие статьи серии: «Что такое паттерны проектирования»«Структурные паттерны», «Поведенческие паттерны».

Еще раз про паттерны

Паттерны проектирования — это решения распространенных проблем при разработке приложений. Также они известны как шаблоны проектирования, паттерны объектно-ориентированного программирования и design patterns. В отличие от готовых функций или библиотек, паттерн представляет собой не конкретный код, а общую концепцию решения проблемы, которую еще нужно подстроить под задачи.

Всего существует 23 классических паттерна, которые были описаны в книге «Банды четырех». В зависимости от того, какие задачи они решают, делятся на порождающие, структурные и поведенческие.

Порождающие паттерны

Согласно Википедии, порождающие шаблоны (creational patterns) — шаблоны проектирования, которые позволяют сделать систему независимой от способа создания, композиции и представления объектов.

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

  • Singleton, или Одиночка
  • Builder, или Строитель
  • Factory Method, или Фабричный метод
  • Prototype, или Прототип
  • Abstract Factory, или Абстрактная фабрика

3 самых популярных порождающих паттерна

По мнению разработчиков MediaSoft Singleton, Builder и Factory Method — это самые используемые порождающие паттерны в разработке. Давайте разберемся, с какими задачами они помогают справляться, и посмотрим на примеры их реализации.

Singleton (Одиночка)

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

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

Еще проще: хороший пример этого паттерна из жизни — это классный журнал в школе. У каждого класса он только один, и если учитель спросит журнал, то всегда получит один и тот же экземпляр.

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

Как создать:

  1. Создаем класс, в котором прописываем логику.
  2. Создаем статическое поле и инициализируем его.
  3. Дальше создаем приватный инициализатор. Это гарантирует, что никакой другой клиент или класс не сможет создавать экземпляр этого класса.
  4. Теперь, чтобы использовать методы нашего класса, мы получаем доступ через статическое поле.

 

Пример реализации:

Java

public class Singleton {
  private static Singleton instance;
  private Singleton () {};

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

JavaScript

// создаем класс Singleton, который проверяет имеется ли у него свойство instance
// если нет, тогда создается новый экземпляр, но если свойство есть
// тогда мы возвращаем ранее созданный экземпляр класса
class Singleton {
  constructor(data) {
    if (Singleton.instance) {
      return Singleton.instance;
    }

    Singleton.instance = this;
    this.data = data;
  }

  consoleData() {
    console.log(this.data);
  }
};

// в работе Singleton мы можем убедиться создав 2 экземпляра класса
const firstSingleton = new Singleton('firstSingleton');
const secondSingleton = new Singleton('secondSingleton');

// в обоих случаях в консоли выведется одинаковое сообщение
firstSingleton.consoleData(); //firstSingleton
secondSingleton.consoleData(); //firstSingleton

 

Комментарий: Есть мнение, что одиночка является анти-паттерном — его противники утверждают, что одиночка привносит глобальное состояние в приложение и трудно тестируется. Несмотря на то, что в этом есть своя правда, в действительности же, этот паттерн не вызывает проблем, если помнить о следующем: суть одиночки заключается в том, чтобы в определенный момент выполнения программы обеспечить наличие одного и только одного экземпляра определенного класса. Создание одиночки для «удобства» — верный признак плохого кода.

Builder (Строитель)

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

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

Еще проще: пример этого паттерна в жизни — покупка компьютера в магазине. При выборе мы указываем, какими характеристиками должна обладать техника (например, память 16 ГБ, процессор Intel core i7 и так далее). Таким образом, мы можем создавать различные виды одного объекта.

Когда нужен: пригодится при составлении SQL-запроса, а также в юнит-тестах.

Как создать:

  1. Создаем абстрактный класс Строитель, у которого объявляем методы инициализации параметров продукта.
  2. Создаем классы, наследующие Строитель, и переопределяющие методы инициализации параметров продукта.
  3. Создаем класс-распорядитель, который выполняет скоординированные действия по созданию Продукта при помощи Строителя

 

Пример реализации:

Java

class Pizza {
    private String dough = "";
    private String sauce = "";

    public void setDough(String dough) { this.dough = dough; }
    public void setSauce(String sauce) { this.sauce = sauce; }
}

abstract class PizzaBuilder { // Abstract Builder
    protected Pizza pizza;
    public void createNewPizzaProduct() { pizza = new Pizza(); }

    public abstract void buildDough();
    public abstract void buildSauce();
}

class SpicyPizzaBuilder extends PizzaBuilder {  // Concrete Builder
    public void buildDough() { pizza.setDough("pan baked"); }
    public void buildSauce() { pizza.setSauce("hot"); }
}

class Waiter {  // Director
    private final PizzaBuilder pizzaBuilder;
    public Waiter(PizzaBuilder pb) { pizzaBuilder = pb; }

    public void constructPizza() {
        pizzaBuilder.createNewPizzaProduct();
        pizzaBuilder.buildDough();
        pizzaBuilder.buildSauce();
    }
}

public class BuilderExample {  // A customer ordering a pizza
    public static void main(String[] args) {
        Waiter waiter = new Waiter(new SpicyPizzaBuilder());
        waiter.constructPizza();
    }
}

 

JavaScript

class Apartment {
  constructor(options) {
    for (const option in options) {
      this[option] = options[option];
    }
  }
  
  getOptions() {
    return Количество комнат: ${this.roomsNumber}, площадь: ${this.square}, этаж: ${this.floor};
  }
}

class ApartmentBuilder {
  setRoomsNumber(roomsNumber) {
    this.roomsNumber = roomsNumber;

    return this;
  }

  setFloor(floor) {
    this.floor = floor;

    return this;
  }

  setSquare(square) {
    this.square = square;

    return this;
  }

  build() {
    return new Apartment(this);
  }
}

const bigApartment = new ApartmentBuilder()
  .setFloor(10)
  .setRoomsNumber(5)
  .setSquare(120)
  .build();

console.log(bigApartment.getOptions());

 

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

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

Factory Method (Фабричный метод)

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

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

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

Когда нужен: паттерн подходит для ситуаций, когда нам необходимо выполнить действие (например, отправить запрос), но заранее неизвестны все данные, так как они зависят от входных параметров (например, от протокола передачи данных — rest, soap или socket).

Как создать:

  1. Создаем класс TicketFactory.
  2. Добавляем методы, которые принимают нужные параметры, и создают сущности, которые инициализируются переданными параметрами.
  3. Внутри этих методов вызываются конструкторы и сеттеры для инициализации параметров.
  4. На выходе нам возвращается инициализированная сущность.

 

Пример реализации:

Java

interface Product { }
class ConcreteProductA implements Product { }
class ConcreteProductB implements Product { }

abstract class Creator {
    public abstract Product factoryMethod();
}
class ConcreteCreatorA extends Creator {
    public Product factoryMethod() { return new ConcreteProductA(); }
}
class ConcreteCreatorB extends Creator {
    public Product factoryMethod() { return new ConcreteProductB(); }
}

public class FactoryMethodExample {
    public static void main(String[] args) {
        List<Creator> creators = List.of(new ConcreteCreatorA(), new ConcreteCreatorB());
        creators.stream().map(Creator::factoryMethod).map(Object::getClass).forEach(System.out::println);
    }
}

 

JavaScript

class Apartment {
  constructor(roomsNumber, square, floor) {
    this.roomsNumber = roomsNumber;
    this.floor = floor;
    this.square = square;
  }

  getOptions() {
    return Количество комнат: ${this.roomsNumber}, площадь: ${this.square}, этаж: ${this.floor};
  }
};

class ApartmentFactory {
  createApartament(type, floor) {
    switch(type) {
      case('oneRoom'):
        return new Apartment(1, 35, floor);
      case('twoRooms'):
        return new Apartment(2, 55, floor);
      case('threeRooms'):
        return new Apartment(3, 75, floor);
      default:
        throw new Error('Такой квартиры не найдено');
    }
  }
}

const oneRoomAparnament = new ApartmentFactory().createApartament('oneRoom', 10);

console.log(oneRoomAparnament.getOptions());

ЗАКЛЮЧЕНИЕ

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

В зависимости от того, какие задачи решают паттерны проектирования, они делятся на три вида: порождающие, структурные и поведенческие.

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

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

Builder, или Строитель, позволяет создавать разные объекты с заданным состоянием, используя один и тот же код. Будет полезен при составлении SQL-запроса или в юнит-тестах.

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

В следующих статьях мы подробнее расскажем про структурные и поведенческие паттерны и разберем самые популярные из них.

Материалы для дополнительного изучения

«Паттерны объектно-ориентированного программирования» Э. Гамма, Р. Хелм, Р. Джонсон, Дж. Влиссидес — та самая книга от авторов «Банды четырех».

Refactoring.guru — Электронная книга о паттернах и принципах проектирования