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

 

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

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

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

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

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

Структурные паттерны

Согласно Википедии, структурные шаблоны (structural patterns) — шаблоны проектирования, в которых рассматривается вопрос о том, как из классов и объектов образуются более крупные структуры.

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

  • Facade, или Фасад
  • Adapter, или Адаптер
  • Decorator, или Декоратор
  • Bridge, или Мост
  • Composite, или Компоновщик
  • Front controller, или Единая точка входа
  • Flyweight, или Приспособленец, или Лекговес
  • Proxy, или Заместитель

3 самых популярных структурных паттерна

По мнению разработчиков MediaSoft Facade, Adaptor и Decorator — это самые используемые структурные паттерны в разработке. Давайте разберемся, с какими задачами они помогают справляться, и посмотрим на примеры их реализации. 

Facade (Фасад)

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

Проще говоря: Facade предоставляет упрощенный интерфейс для сложной системы. 

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

Когда нужен: Используется в библиотеках и позволяет описать их так, чтобы пользователю не нужно было вникать в их реализацию. 

Структура паттерна:

  1. Фасад — интерфейс для легкого доступа. Предоставляет только тот функционал, который нужен клиенту, и скрывает всё остальное.
  2. Сложная система из классов и методов.

 

Как создать:

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

 

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

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 позволяет объектам с несовместимыми интерфейсами работать вместе.

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

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

Существует два варианта этого паттерна: 

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

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

Как создать:

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

 

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

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 — структурный шаблон проектирования, предназначенный для динамического подключения дополнительного поведения к объекту.

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

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

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

Структура: 

  1. Компонент с заданным интерфейсом
  2. Декоратор, который оборачивает компонент и добавляет новое поведение.

 

Как создать:

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

 

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

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 — Электронная книга о паттернах и принципах проектирования