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

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

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

 

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

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

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

Поведенческие паттерны

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

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

К ним относятся:

  • Template Method, или Шаблонный метод;
  • Iterator, или Итератор;
  • Observer, или Наблюдатель;
  • Chain of Responsibility, или Цепочка обязанностей;
  • Command, или Команда;
  • Mediator, или Посредник;
  • Memento, или Хранитель;
  • Visitor, или Посетитель;
  • Strategy, или Стратегия;
  • State, или Состояние.

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

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

Template Method (Шаблонный метод)

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

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

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

Когда нужен: используется для избежания дублирования кода. Например, при описании шаблонов.

Паттерн состоит из двух частей: 

  1. Абстрактный класс, который описывает общие свойства и методы. 
  2. Конкретный класс, который переопределяет методы описанные в абстрактном классе или добавляет свои.

 

Как создать:

  1. Создать абстрактный класс.
  2. В нем создать абстрактные методы (шаги алгоритма) и final-метод, вызывающий абстрактные методы (структура алгоритма)
  3. Создать реализацию абстрактного класса и переопределить абстрактные методы.

 

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

Java:

abstract class AbstractHouse {
public final void build() {
pourFoundation();
putTheDoor();
buildRoof();
}

private void pourFoundation() { System.out.println("Заливка фундамента"); }

abstract void putTheDoor();
abstract void buildRoof();
}

public class ConcreteHouse extends AbstractHouse {
@Override
void putTheDoor() { System.out.println("Вставка деревянной двери"); }

@Override
void buildRoof() { System.out.println("Постройка крыши с окном"); }
}

JavaScript:

// создаем абстрактный класс Car, которому присваиваем общее свойство
// cost и метод getCost
class Car {
constructor(cost) {
this.cost = cost;
}

getCost() {
return `Стоимость автомобиля: ${this.cost}`;
}
}

// создаем подклассы от класса Car, которые расширяют
// его поведение
class SportCar extends Car {
constructor(cost, speed) {
super(cost);
this.speed = speed;
}

getSpeed() {
return `Скорость: ${this.speed}`
}
}

class Truck extends Car {
constructor(cost, capacity) {
super(cost);
this.capacity = capacity;
}

getCapacity() {
return `Вместимость: ${this.capacity}`
}
}

const sport = new SportCar(16000, 300);
const truck = new Truck(20000, 1000);

// как мы видим, теперь у инстанса класса SportCar
// есть метод getSpeed, а у инстанса Truck метод
// getCapacity, но также у обоих инстансов есть
// метод абстрактного класса Car
console.log(sport.getCost()); // Стоимость автомобиля: 16000
console.log(sport.getSpeed()); // Скорость: 300
console.log(truck.getCapacity()); // Вместимость: 1000

Iterator (Итератор)

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

Проще говоря, Iterator нужен для последовательного перебора составных элементов коллекции, не раскрывая их внутреннего представления. 

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

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

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

  1. Итератор — описывает интерфейс для доступа и обхода элементов коллекции.
  2. Конкретный итератор — реализует алгоритм обхода какой-то конкретной коллекции. 

 

Как создать: 

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

 

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

Java:

interface Iterator<T> {
T next();
void first();
void last();
boolean hasNext();
}

public class ListIterator<T> implements Iterator<T> {
private List<T> list;
private int index = 0;

public ListIterator(List<T> list) {
this.list = list;
}
public void first() { index = 0; }
public void last() { index = list.size(); }
public T next() {
T obj = list.get(index);
index++;
return obj;
}
public boolean hasNext() { return index < list.size(); }
}

JavaScript:

class Iterator {
constructor(data) {
this.index = 0;
this.data = data;
}

next() {
if(this.hasNext()) {
return {
value: this.data[this.index++],
done: false
}
}

return {
value: undefined,
done: true
}
}

hasNext() {
return this.index < this.data.length;
}
}

const cars = ['BMW', 'Audi', 'Honda'];

const carsIterator = new Iterator(cars);

console.log(carsIterator.next()); // {value: 'BMW', done: false}
console.log(carsIterator.next()); // {value: 'Audi', done: false}
console.log(carsIterator.hasNext()); // true
console.log(carsIterator.next()); // {value: 'Honda', done: false}
console.log(carsIterator.hasNext()); // false

Observer (Наблюдатель)

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

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

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

Когда нужен: часто используется при проектировании библиотек, которые управляют состоянием приложения. Также является основой для различных классов, способных реагировать на изменения и уведомлять своих подписчиков об этих изменениях: например, NotificationCenter в iOS или LiveData в Android. 

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

Паттерн подразумевает наличие трех участников:

  • подписчик (subscriber), являющийся интерфейсом/протоколом, который определяет методы для отправки оповещения;
  • издатель (publisher), внутреннее состояние которого интересует подписчиков; именно издатель содержит механизм подписки, который включает в себя список подписчиков и методы для подписки/отписки;
  • конкретные подписчики (concrete subscribers), реализующие интерфейс/протокол подписчика и несущие в себе конкретную бизнес-логику. 

Как создать:

  1. Создаем наблюдаемый (observable) объект, а точнее субъект (subject), который должен предоставлять интерфейс для регистрации и дерегистрации наблюдателей (listeners).
  2. Создаем один или несколько наблюдателей, которые должны обладать открытым методом, через который будет происходить оповещение об изменении состояния субъекта. Этот метод часто называют notify.
  3. Если наблюдателей достаточно много, для упрощения работы с ними можно использовать коллекцию (collection of observers).

 

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

Java: 

interface Observable {
void registerObserver(Observer o);
void removeObserver(Observer o);
void notifyObservers();
}

class ConcreteObservable implements Observable {
private List<Observer> parishioners = new ArrayList<>();
private String newsChurch;

public void setNewsChurch(String news) {
this.newsChurch = news;
notifyObservers();
}

public void registerObserver(Observer o) { parishioners.add(o); }
public void removeObserver(Observer o) { parishioners.remove(o); }
public void notifyObservers() { parishioners.forEach(o -> o.update(newsChurch)); }
}

interface Observer {
void update(String news);
}

public class ConcreteObserver implements Observer {
private String name;

public ConcreteObserver(String name, Observable o) {
this.name = name;
o.registerObserver(this);
}

public void update(String news) { System.out.println(name + " узнал новость: " + news); }
}

JavaScript:

// создаем класс Observer, в котором есть 3 метода
// subscribe для добавления подписчиков
// unsubscribe для удаления подписчиков
// и fire для оповещения подписчиков об изменениях
class Observer {
constructor() {
this.observers = [];
}

subscribe(fn) {
this.observers.push(fn);
}

unsubscribe(fn) {
this.observers = this.observers.filter((subscriber, key) => subscriber !== fn);
}

fire(data) {
this.observers.forEach(subscriber => subscriber(data));
}
};

let clickCount = 0;

const clickObserver = new Observer();

const getMousePosition = (event) => {
console.log(`x:${event.clientX} y:${event.clientY}`);
};

const getCount = () => {
console.log(++clickCount);

// обратите внимание на эту часть кода,
// здесь мы удаляем подписку при достижении
// счетчика 5, и в консоль больше не будет выводиться
// значение clickCount
if(clickCount === 5) {
clickObserver.unsubscribe(getCount);
}
}

// здесь мы добавляем подписчика, который будет
// выводить в консоль координаты мышки во
// время клика
clickObserver.subscribe(getMousePosition);

// здесь мы добавляем подписчика, который будет
// увеличивать счетчик на 1 и выводить его результат в консоль
clickObserver.subscribe(getCount);

// далее добавляем событие, при котором observer
// будет вызывать всех подписчиков и передавать им
// event
document.addEventListener('click', (event) => {
clickObserver.fire(event);
});

Заключение

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

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

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

Три самых популярных из них: 

Template Method, или Шаблонный метод, описывает скелет алгоритма, перекладывая ответственность за некоторые его шаги на подклассы. Паттерн позволяет подклассам переопределять шаги алгоритма, не меняя его общей структуры.

Iterator, или Итератор, нужен для последовательного перебора составных элементов коллекции без раскрытия их внутреннего представления. Стоит использовать для перебора сложных составных объектов коллекции, если необходимо скрыть детали от клиента.

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

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

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

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