Патерны проектирования
Важно понимать! Не все патерны идеальны или всегда нужны:
- часть паттернов появилась из-за ограничений старых языков
- в современных языках некоторые вещи решаются проще
- иногда паттерны только усложняют код
Порождающие
- 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в рабочем коде - хочется держать создание объектов в одном месте
Пример
Есть генерация отчета. Формат отчета может быть разный:
- 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-запроса приходит много данных:
- имя
- телефон
- адрес
- город
- И так далее.
Без 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 - все алгоритмы внутри
А:
- один общий сервис
- разные алгоритмы отдельно
- нужный алгоритм передается в сервис

