В этой части материала про паттерны разбираемся, что такое порождающие паттерны проектирования и какие задачи они решают, а также разберем три самых часто используемых.
Другие статьи серии: «Что такое паттерны проектирования», «Структурные паттерны», «Поведенческие паттерны».
Еще раз про паттерны
Паттерны проектирования — это решения распространенных проблем при разработке приложений. Также они известны как шаблоны проектирования, паттерны объектно-ориентированного программирования и design patterns. В отличие от готовых функций или библиотек, паттерн представляет собой не конкретный код, а общую концепцию решения проблемы, которую еще нужно подстроить под задачи.
Всего существует 23 классических паттерна, которые были описаны в книге «Банды четырех». В зависимости от того, какие задачи они решают, делятся на порождающие, структурные и поведенческие.
Порождающие паттерны
Согласно Википедии, порождающие шаблоны (creational patterns) — шаблоны проектирования, которые позволяют сделать систему независимой от способа создания, композиции и представления объектов.
Проще говоря, порождающие паттерны предназначены для создания экземпляра объекта или группы связанных объектов. К ним относятся:
- Singleton, или Одиночка
- Builder, или Строитель
- Factory Method, или Фабричный метод
- Prototype, или Прототип
- Abstract Factory, или Абстрактная фабрика
3 самых популярных порождающих паттерна
По мнению разработчиков MediaSoft Singleton, Builder и Factory Method — это самые используемые порождающие паттерны в разработке. Давайте разберемся, с какими задачами они помогают справляться, и посмотрим на примеры их реализации.
Singleton (Одиночка)
Согласно Википедии, Singleton — порождающий шаблон проектирования, гарантирующий, что в однопоточном приложении будет единственный экземпляр некоторого класса, и предоставляющий глобальную точку доступа к этому экземпляру.
Проще говоря: Singleton гарантирует, что созданный объект будет единственным в приложении и позволяет получать к нему доступ из любой части приложения.
Еще проще: хороший пример этого паттерна из жизни — это классный журнал в школе. У каждого класса он только один, и если учитель спросит журнал, то всегда получит один и тот же экземпляр.
Когда нужен: пригодится, если в приложении есть управляющий объект, в котором хранится весь контекст приложения. Например, в сервисах хранения данных.
Как создать:
- Создаем класс, в котором прописываем логику.
- Создаем статическое поле и инициализируем его.
- Дальше создаем приватный инициализатор. Это гарантирует, что никакой другой клиент или класс не сможет создавать экземпляр этого класса.
- Теперь, чтобы использовать методы нашего класса, мы получаем доступ через статическое поле.
Пример реализации:
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-запроса, а также в юнит-тестах.
Как создать:
- Создаем абстрактный класс Строитель, у которого объявляем методы инициализации параметров продукта.
- Создаем классы, наследующие Строитель, и переопределяющие методы инициализации параметров продукта.
- Создаем класс-распорядитель, который выполняет скоординированные действия по созданию Продукта при помощи Строителя
Пример реализации:
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).
Как создать:
- Создаем класс TicketFactory.
- Добавляем методы, которые принимают нужные параметры, и создают сущности, которые инициализируются переданными параметрами.
- Внутри этих методов вызываются конструкторы и сеттеры для инициализации параметров.
- На выходе нам возвращается инициализированная сущность.
Пример реализации:
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 — Электронная книга о паттернах и принципах проектирования
статьи по теме
-
ЧитатьПродолжаем набор на бесплатную подготовку к ЕГЭ и ОГЭ 2025: математика, физика и информатика18.03.2025
-
ЧитатьПолезные материалы для тестировщиков: каналы, ресурсы и книги17.02.2025
-
ЧитатьОткрыт набор на подготовку к ЕГЭ и ОГЭ по математике, физике и информатике 2025 года22.08.2024
-
ЧитатьЛайфхаки при использовании Java29.02.2024
-
ЧитатьС чего начать путь React-разработчику14.12.2023
-
ЧитатьЛайфхаки при изучении React24.10.2023