Патерны проектирования

Важно понимать! Не все патерны идеальны или всегда нужны:

  • часть паттернов появилась из-за ограничений старых языков
  • в современных языках некоторые вещи решаются проще
  • иногда паттерны только усложняют код

Порождающие

  • Singleton (GoF)
  • Factory Method (GoF)
  • Abstract Factory (GoF)

  • Builder (GoF)
  • Prototype (GoF)
  • Dependency Injection
  • Service Locator
  • Object Pool
  • Multiton

Simple Factory

Ref: Factory comparison
Сложность: ★☆☆
Популярность: ★★★

Simple Factory обычно реализуют как отдельный класс или метод, задача которого - создать и вернуть нужный объект по входным данным. Это не отдельный GoF паттерн. Это не строгая структура, а прием.

На практике обычно это:

  • либо отдельный класс SomeFactory
  • либо статический метод SomeFactory::create()
  • либо (реже) обычный метод внутри другого класса

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

Из чего состоит
  • есть код, которому нужен объект
  • есть несколько возможных классов
  • есть один простой метод, который возвращает нужный объект
Когда полезен
  • нужно выбрать один класс из нескольких
  • выбор зависит от строки, типа, настройки
  • не хочется писать if/else с new в рабочем коде
  • хочется держать создание объектов в одном месте

Пример

Есть генерация отчета. Формат отчета может быть разный:

  • PDF
  • CSV
  • JSON

Без simple factory

class ReportManager {
	public function create_report( string $format ) {
		if ( 'pdf' === $format ) {
			return new PdfReport();
		}

		if ( 'csv' === $format ) {
			return new CsvReport();
		}

		if ( 'json' === $format ) {
			return new JsonReport();
		}

		return null;
	}
}

Проблемы:

  • выбор и создание класса находится в рабочем коде
  • если таких мест станет много, логика создания начнет дублироваться
  • при добавлении нового типа отчета придется править код в разных местах

С simple factory

Классы
class PdfReport {}

class CsvReport {}

class JsonReport {}
Фабрика
class ReportFactory {
	public static function create( string $format ) {
		if ( 'pdf' === $format ) {
			return new PdfReport();
		}

		if ( 'csv' === $format ) {
			return new CsvReport();
		}

		if ( 'json' === $format ) {
			return new JsonReport();
		}

		return null;
	}
}
Использование
$report = ReportFactory::create( 'csv' );
Важно
  • не считается отдельным GoF паттерном
  • часто вообще выглядит как один статический метод
  • Находится в шаге от того, чтобы стать Factory Method (GoF).

Структурные

  • Adapter (GoF)
  • Bridge (GoF)
  • Composite (GoF)
  • Decorator (GoF)
  • Facade (GoF)
  • Flyweight (GoF)
  • Proxy (GoF)
  • Module
  • Repository
  • Data Mapper
  • Active Record
  • Anti-Corruption Layer

DTO

Ref: DTO
Сложность: ★☆☆
Популярность: ★★★

DTO - это объект, который нужен только для передачи данных.

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

Из чего состоит:

  • есть объект с полями
  • в нем лежат только данные
  • его используют, чтобы передавать данные между слоями приложения
Когда полезен DTO
  • нужно передать набор связанных данных одним объектом
  • не хочется таскать по коду большие массивы
  • хочется иметь понятную и стабильную структуру данных
  • нужно отделить внутренние данные модели от того, что уходит наружу

Пример

Есть форма оформления заказа. Из HTTP-запроса приходит много данных:

  • имя
  • email
  • телефон
  • адрес
  • город
  • И так далее.

Без DTO

class CheckoutController {
	public function create_order( array $request_data ) {
		$email = $request_data['email'] ?? '';
		$phone = $request_data['phone'] ?? '';
		$city  = $request_data['city'] ?? '';

		// и так далее

		// передаем данные дальше в сервис
	}
}

Проблемы:

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

С DTO

class CheckoutRequestDto {
	public readonly string $first_name;
	public readonly string $last_name;
	public readonly string $email;
	public readonly string $phone;
	public readonly string $address_line_1;
	public readonly string $address_line_2;
	public readonly string $city;

	public function __construct( array $data ) {
		$this->first_name      = (string) ( $data['first_name'] ?? '' );
		$this->last_name       = (string) ( $data['last_name'] ?? '' );
		$this->email           = (string) ( $data['email'] ?? '' );
		$this->phone           = (string) ( $data['phone'] ?? '' );
		$this->address_line_1  = (string) ( $data['address_line_1'] ?? '' );
		$this->address_line_2  = (string) ( $data['address_line_2'] ?? '' );
		$this->city            = (string) ( $data['city'] ?? '' );
	}
}
Использование
class CheckoutController {
	public function create_order( array $request_data ) {
		$request_dto = new CheckoutRequestDto( $request_data );

		$email = $request_dto->email;
		$city  = $request_dto->city;

		// передаем DTO дальше в сервис создания заказа
	}
}
Самая короткая суть

из HTTP-запроса пришел массив
мы завернули его в DTO
дальше работаем не с сырым массивом, а с объектом с понятными свойствами

Поведенческие

  • Command (GoF)
  • State (GoF)
  • Chain of Responsibility (GoF)
  • Mediator (GoF)
  • Memento (GoF)
  • Iterator (GoF)
  • Visitor (GoF)
  • Template Method (GoF)
  • Publish-Subscribe
  • Event Bus
  • Middleware
  • Pipeline
  • Null Object
  • Specification

Observer (GoF)

Ref: Observer
Сложность: ★★☆
Популярность: ★★★

Наблюдатель - это паттерн, когда один объект сообщает другим объектам, что у него что-то изменилось.

Из чего состоит:

  • есть субъект - объект, за которым наблюдают
  • есть наблюдатели - объекты, которые подписаны на его изменения
  • когда состояние субъекта меняется, он уведомляет всех подписчиков
Когда полезен Observer
  • после одного события нужно запускать несколько действий
  • количество таких действий может расти
  • неудобно жестко связывать основной код с уведомлениями и интеграциями

Пример

Есть заказ в интернет-магазине. Когда статус заказа меняется, нужно выполнить разные действия:

  • отправить email
  • записать событие в лог
  • отправить данные в CRM

Без Observer

class OrderService {
	public function update_status( Order $order, string $status ) {
		$order->set_status( $status );

		$mailer = new EmailNotifier();
		$mailer->send( $order );

		$logger = new OrderLogger();
		$logger->log( $order );

		$crm = new CrmNotifier();
		$crm->sync( $order );
	}
}

Проблемы:

  • сервис знает слишком много о побочных действиях
  • при добавлении нового действия надо менять основной код
  • логика обновления заказа смешивается с уведомлениями

С Observer

Интерфейс наблюдателя
interface OrderObserver {
	public function update( Order $order ): void;
}
Наблюдатели
class EmailNotifier implements OrderObserver {
	public function update( Order $order ): void {
		// send email
	}
}

class OrderLogger implements OrderObserver {
	public function update( Order $order ): void {
		// write log
	}
}

class CrmNotifier implements OrderObserver {
	public function update( Order $order ): void {
		// sync crm
	}
}
Субъект
class Order {
	private $status = '';
	private $observers = array();

	public function attach( OrderObserver $observer ): void {
		$this->observers[] = $observer;
	}

	public function set_status( string $status ): void {
		$this->status = $status;
		$this->notify();
	}

	private function notify(): void {
		foreach ( $this->observers as $observer ) {
			$observer->update( $this );
		}
	}

	public function get_status(): string {
		return $this->status;
	}
}
Использование
$order = new Order();

$order->attach( new EmailNotifier() );
$order->attach( new OrderLogger() );
$order->attach( new CrmNotifier() );

$order->set_status( 'completed' );
Почему тут Observer реально нужен

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

То есть:

  • Order знает, что его состояние изменилось и нужно всех об этом уведомить
  • наблюдатели сами знают, что делать при изменении
Самая короткая суть

Не:

  • один класс
  • сам меняет состояние
  • сам вызывает все дополнительные действия

А:

  • один объект меняет состояние
  • подписчики отдельно реагируют на это
  • новые реакции можно добавлять без изменения основной логики

Strategy (GoF)

Ref: Strategy
Сложность: ★☆☆
Популярность: ★★★

Стратегия - это паттерн, когда одну и ту же задачу можно выполнить разными способами, и эти способы выносятся в отдельные классы.

Из чего состоит:

  • есть общий процесс (сервис, контроллер, контекст)
  • в одном месте внутри него может меняться алгоритм
  • этот алгоритм подставляется как отдельный объект (стратегия)
Когда полезна стратегия
  • задача одна, но способов выполнения несколько
  • эти способы могут меняться
  • плохо держать все варианты в одном классе через условия

Пример

Есть экспорт данных. Общий процесс:

  • взять данные
  • преобразовать
  • отдать результат

Но формат экспорта бывает разный:

  • CSV
  • JSON
  • XML

Без стратегии

class UserExportService {
	public function export( array $users, string $format ) {
		if ( 'csv' === $format ) {
			// export to csv
		} elseif ( 'json' === $format ) {
			// export to json
		} elseif ( 'xml' === $format ) {
			// export to xml
		}
	}
}

Проблемы:

  • растет if/else
  • вся логика свалена в одно место

Со стратегией

Интерфейс
interface ExportStrategy {
	public function export( array $users ): string;
}
Реализации
class CsvExport implements ExportStrategy {
	public function export( array $users ): string {
		return 'csv data';
	}
}

class JsonExport implements ExportStrategy {
	public function export( array $users ): string {
		return wp_json_encode( $users );
	}
}

class XmlExport implements ExportStrategy {
	public function export( array $users ): string {
		return 'xml data';
	}
}
Сервис
class UserExportService {
	private $strategy;

	public function __construct( ExportStrategy $strategy ) {
		$this->strategy = $strategy;
	}

	public function export_users( array $users ): string {
		if ( ! $users ) {
			return '';
		}

		return $this->strategy->export( $users );
	}
}
Использование
$service = new UserExportService( new JsonExport() );

$result = $service->export_users( $users );
Почему тут стратегия реально нужна

Потому что UserExportService отвечает за саму задачу экспорта пользователей, а конкретный формат экспорта может меняться.

То есть:

  • UserExportService знает, что нужно экспортировать
  • стратегия знает, как именно это представить
Самая короткая суть

Не:

  • один класс
  • куча if/else
  • все алгоритмы внутри

А:

  • один общий сервис
  • разные алгоритмы отдельно
  • нужный алгоритм передается в сервис