В этой части материала про паттерны разбираемся, что такое структурные паттерны проектирования и какие задачи они решают, а также изучаем три самых часто используемых.
Другие статьи серии: «Что такое паттерны проектирования», «Порождающие паттерны», «Поведенческие паттерны».
Еще раз про паттерны
Паттерны проектирования — это решения распространенных проблем при разработке приложений. Также они известны как шаблоны проектирования, паттерны объектно-ориентированного программирования и design patterns. В отличие от готовых функций или библиотек, паттерн представляет собой не конкретный код, а общую концепцию решения проблемы, которую еще нужно подстроить под задачи.
Всего существует 23 классических паттерна, которые были описаны в книге «Банды четырех». В зависимости от того, какие задачи они решают, делятся на порождающие, структурные и поведенческие.
Структурные паттерны
Согласно Википедии, структурные шаблоны (structural patterns) — шаблоны проектирования, в которых рассматривается вопрос о том, как из классов и объектов образуются более крупные структуры.
Проще говоря, структурные паттерны связаны с композицией объектов или тем, как сущности могут использовать друг друга. К ним относятся:
- Facade, или Фасад
- Adapter, или Адаптер
- Decorator, или Декоратор
- Bridge, или Мост
- Composite, или Компоновщик
- Front controller, или Единая точка входа
- Flyweight, или Приспособленец, или Лекговес
- Proxy, или Заместитель
3 самых популярных структурных паттерна
По мнению разработчиков MediaSoft Facade, Adaptor и Decorator — это самые используемые структурные паттерны в разработке. Давайте разберемся, с какими задачами они помогают справляться, и посмотрим на примеры их реализации.
Facade (Фасад)
Согласно Википедии, Facade — структурный шаблон проектирования, позволяющий скрыть сложность системы путём сведения всех возможных внешних вызовов к одному объекту, делегирующему их соответствующим объектам системы.
Проще говоря: Facade предоставляет упрощенный интерфейс для сложной системы.
Еще проще: При оплате покупки через Apple Pay вы подносите телефон к устройству и оплачиваете покупку. Кажется, что все просто. Но на самом деле внутри этого процесса происходит гораздо больше вещей. Этот упрощенный интерфейс называется фасадом.
Когда нужен: Используется в библиотеках и позволяет описать их так, чтобы пользователю не нужно было вникать в их реализацию.
Структура паттерна:
- Фасад — интерфейс для легкого доступа. Предоставляет только тот функционал, который нужен клиенту, и скрывает всё остальное.
- Сложная система из классов и методов.
Как создать:
- Создаем интерфейс фасада. В нем перечисляем нужные нам методы системы, которые предоставляет фасад.
- Создаем класс фасада, который реализует этот интерфейс.
- В методах этого класса реализуем обращения к сложной системе, которая скрывается за фасадом.
Пример реализации:
Java
class CPU {
public void execute() { ... }
}
class Memory {
public void load(long position, byte[] data) { ... }
}
class HardDrive {
public byte[] read(long lba, int size) { ... }
}
class Computer { // Facade
private CPU cpu = new CPU();
private Memory memory = new Memory();
private HardDrive hardDrive = new HardDrive();
public void startComputer() {
memory.load(BOOT_ADDRESS, hardDrive.read(BOOT_SECTOR, SECTOR_SIZE));
cpu.execute();
}
}
class Application { // Client
public static void main(String[] args) {
Computer computer = new Computer();
computer.startComputer();
}
}
JavaScript
// Создаем классы, Doors и Body, которые отвечают за конфигурацию отдельных частей автомобиля
class Doors {
getNumberOfDoors(type) {
return type === 'truck' ? 2 : 4;
}
}
class Body {
createBody(type) {
return type === 'truck';
}
}
// Далее создаем класс Car, который и является нашим фасадом. В методе creacteCar мы создали экземпляры классов и вызвали нужные методы у них, чтобы получить готовую конфигурацию автомобиля. Таким образом мы оградили пользователя от нужды понимания всех классов и их взаимодействия. Взамен этого предложили удобный интерфейс
class Car {
createCar(type) {
const doors = new Doors().getNumberOfDoors(type);
const body = new Body().createBody(type);
return {doors, body};
}
}
console.log(new Car().createCar('truck')); // { doors: 2, body: true }
console.log(new Car().createCar('sport')); // { doors: 4, body: false }
Adapter (Адаптер)
Согласно Википедии, Adapter — структурный шаблон проектирования, предназначенный для организации использования функций объекта, недоступного для модификации, через специально созданный интерфейс.
Проще говоря, Adapter позволяет объектам с несовместимыми интерфейсами работать вместе.
Еще проще: европейские розетки отличаются от английских, поэтому, приезжая в Лондон, туристы обязательно берут переходник, или адаптер.
Когда нужен: часто используется, если мы работаем со сторонней библиотекой или компонентом, доступ к изменениями методов которого у нас отсутствует. Или когда есть несколько сторонних систем, доступ к которым должен осуществляться единообразно через одинаковый интерфейс.
Существует два варианта этого паттерна:
- предоставляет возможность работать при помощи интерфейса/протокола, который наши объекты должны реализовать;
- дает доступ при помощи отдельного класса-адаптера, если нам необходимо внедрить более сложную логику преобразований.
Выбор той или иной реализации этого шаблона зависит от предпочтений разработчика и сложности кода, который необходимо адаптировать.
Как создать:
- Создаем класс Адаптера.
- В этом классе реализуем метод, который принимает на входе объект в незнакомом формате, а возвращает объект нужного нам формата.
- В этом методе реализуем логику преобразования объекта одного формата в другой.
Пример реализации:
Java:
interface Chief {
Object makeBreakfast();
Object makeDinner();
}
public class Plumber { // Adaptee
public Object getScrewNut() { ... }
public Object getGasket() { ... }
}
public class ChiefAdapter extends Plumber implements Chief { // Adapter
public Object makeBreakfast() {
return getGasket();
}
public Object makeDinner() {
return getScrewNut();
}
}
public class Client { // Client
public static void eat(Object dish) { ... }
public static void main(String[] args) {
Chief ch = new ChiefAdapter();
eat(ch.makeBreakfast());
eat(ch.makeDinner());
}
}
JavaScript:
// У нас есть класс калькулятора, который реализует единственный метод "operation"
class Calculator {
operation(num1, num2, operation) {
switch (operation) {
case 'multiplication':
return num1 * num2;
case 'division':
return num1 / num2;
default:
return NaN;
}
}
}
// Допустим нам нужно создать улучшенную версию калькулятора
class NewCalculator {
add(num1, num2) {
return num1 + num2;
}
div(num1, num2) {
return num1 / num2;
}
mult(num1, num2) {
return num1 * num2;
}
}
// Но возникает проблема, что нет обратной совместимости со старым калькулятором, как раз в этом нам и поможет адаптер. Мы адаптируем новый калькулятор под функционал старого
class CalculatorAdapter {
constructor() {
this.calculator = new NewCalculator();
}
operation(num1, num2, operation) {
switch (operation) {
case "add":
return this.calculator.add(num1, num2);
case "multiplication":
return this.calculator.mult(num1, num2);
case "division":
return this.calculator.div(num1, num2);
default:
return NaN;
}
}
}
// Обратите внимание, мы используем адаптер вместо старого калькулятора
const calcAdapter = new CalculatorAdapter();
const sumAdapter = calcAdapter.operation(2, 2, "multiplication");
console.log(sumAdapter);
// А новый функционал используем от экземпляра класса нового калькулятора
const calculator = new Calculator();
const sum = calculator.mult(2, 2);
console.log(sum);
Decorator (Декоратор)
Согласно Википедии, Decorator — структурный шаблон проектирования, предназначенный для динамического подключения дополнительного поведения к объекту.
Проще говоря, паттерн позволяет добавлять объектам новые функции с помощью обертки без создания отдельного класса.
Еще проще: пример декоратора из жизни — это подключение мышки к ноутбуку, то есть вы добавляете новую функцию устройству, не меняя его.
Когда нужен: у этого паттерна широкая область применения. Им пользуются каждый раз, когда нужно добавить логику в уже созданные объекты и библиотеки, менять которые нельзя.
Структура:
- Компонент с заданным интерфейсом
- Декоратор, который оборачивает компонент и добавляет новое поведение.
Как создать:
- Создаем интерфейс. В нем перечисляем методы библиотеки, с которой будет работать декоратор.
- Создаем класс, реализующий этот интерфейс. В нем прописываем логику обращения к объекту, который скрывается за декоратором, и добавляем свою кастомную логику.
Пример реализации:
Java:
public interface InterfaceComponent { void doOperation(); }
class MainComponent implements InterfaceComponent {
public void doOperation() { System.out.print("World!"); }
}
abstract class Decorator implements InterfaceComponent {
protected InterfaceComponent component;
public Decorator (InterfaceComponent c) { component = c; }
public void doOperation() { component.doOperation(); }
public void newOperation() { System.out.println("Do Nothing"); }
}
class DecoratorComma extends Decorator {
public DecoratorComma(InterfaceComponent c) { super(c); }
public void doOperation() { System.out.print(","); super.doOperation(); }
public void newOperation() { System.out.println("New comma operation"); }
}
class DecoratorHello extends Decorator {
public DecoratorHello(InterfaceComponent c) { super(c); }
public void doOperation() { System.out.print("Hello"); super.doOperation(); }
public void newOperation() { System.out.println("New hello operation"); }
}
class Main {
public static void main (String... s) {
Decorator c = new DecoratorHello(new DecoratorComma(new MainComponent()));
c.doOperation(); // Результат выполнения программы "Hello,World!"
c.newOperation(); // New hello operation
}
}
JavaScript:
// создаем класс, с набором свойств и методов
class Car {
constructor(cost) {
this.cost = cost;
}
getCost() {
return `Стоимость автомобиля: ${this.cost}`;
}
}
// далее создаем декоратор, который принимает экземпляр класса
// и расширяет его, в данном случае добавляем свойство color
const colorCar = (car, color) => {
car.color = color;
return car;
}
const car = new Car(16000);
// оборачиваем инстанс car в декоратор, и получаем расширенный класс
colorCar(car, 'orange')
console.log(car); // Car { cost: 16000, color: 'orange' }
console.log(car.getCost()); // Стоимость автомобиля: 16000
Заключение
Паттерны проектирования — это решения распространенных проблем при разработке кода. Их знание и использование позволяет экономить время, используя готовые решения, стандартизировать код и повысить общий словарь.
В зависимости от того, какие задачи решают паттерны проектирования, они делятся на три вида: порождающие, структурные и поведенческие.
Структурные паттерны связаны с композицией объектов или тем, как сущности могут использовать друг друга. Три самых популярных из них:
Facade, или Фасад, предоставляет упрощенный интерфейс для сложной системы. Используется в библиотеках и позволяет описать их так, чтобы пользователю не нужно было вникать в их реализацию.
Adapter, или Адаптер позволяет объектам с несовместимыми интерфейсами работать вместе. Используется, если мы работаем со сторонней библиотекой или компонентом, доступ к изменениями методов которого у нас отсутствует.
Decorator, или Декоратор, позволяет добавлять объектам новые функции с помощью обертки без создания отдельного класса. Им пользуются каждый раз, когда нужно добавить логику в уже созданные объекты и библиотеки, менять которые нельзя.
В следующих статьях мы подробнее расскажем про поведенческие паттерны и разберем самые популярные из них.
Материалы для дополнительного изучения
«Паттерны объектно-ориентированного программирования» Э. Гамма, Р. Хелм, Р. Джонсон, Дж. Влиссидес — та самая книга от авторов «Банды четырех».
Refactoring.guru — Электронная книга о паттернах и принципах проектирования
статьи по теме
-
ЧитатьОткрыт набор на подготовку к ЕГЭ и ОГЭ по математике, физике и информатике 2025 года22.08.2024
-
ЧитатьЛайфхаки при использовании Java29.02.2024
-
ЧитатьС чего начать путь React-разработчику14.12.2023
-
ЧитатьЛайфхаки при изучении React24.10.2023
-
ЧитатьС чего начать путь Go-разработчику15.03.2023
-
ЧитатьGit: гайд для начинающих21.10.2022