29 вопросов
S - Single Responsibility: один класс - одна причина для изменения. O - Open/Closed: открыт для расширения, закрыт для модификации. L - Liskov Substitution: подтипы должны быть заменяемы базовым типом. I - Interface Segregation: много узких интерфейсов лучше одного "толстого". D - Dependency Inversion: зависить от абстракций, не от конкретных классов.
У класса должна быть только одна причина для изменения. Если класс отвечает и за отчеты, и за рассылку - при смене формата отчетов или канала рассылки придется менять один и тот же класс.
// Плохо: один класс и сохраняет, и шлет письмо
class UserRegistration {
public function register($data) {
$user = $this->saveUser($data);
$this->sendEmail($user);
}
}
// Хорошо: регистрация оркестрирует, сохранение и письмо - отдельные сервисыСущности открыты для расширения, закрыты для модификации. Новое поведение добавлять через новые классы/стратегии, а не правкой существующего кода.
// Расширение через новые классы, без изменения Processor
interface Handler { public function supports($request): bool; public function handle($request): mixed; }
class Processor {
public function __construct(private array $handlers) {}
public function process($request) {
foreach ($this->handlers as $h) {
if ($h->supports($request)) return $h->handle($request);
}
}
}Подтипы должны быть подставляемы вместо базового типа без нарушения корректности программы. Поведение подкласса не должно противоречить контракту базового класса.
Пример нарушения: Rectangle и Square. Если Square переопределяет setWidth/setHeight так, что меняет и другую сторону - код, ожидающий Rectangle (независимые стороны), сломается при подстановке Square.
Следствие: подкласс не должен ужесточать предусловия или ослаблять постусловия.
Клиенты не должны зависеть от интерфейсов, которые не используют. Лучше много маленьких интерфейсов, чем один большой.
// Плохо: классу нужен только save, но он вынужден реализовать delete, find
interface Repository { function save($e); function delete($e); function find($id); }
// Хорошо: разделить по ролям
interface Persister { function save($e); }
interface Finder { function find($id); }
interface Remover { function delete($e); }Толстый интерфейс ведет к пустым методам или лишним зависимостям.
Модули верхнего уровня не должны зависеть от модулей нижнего уровня; оба должны зависеть от абстракций. Абстракции не должны зависеть от деталей; детали - от абстракций.
// Плохо: контроллер зависит от конкретной реализации
class OrderController { public function __construct(private MySqlOrderRepo $repo) {} }
// Хорошо: зависимость от интерфейса
class OrderController { public function __construct(private OrderRepositoryInterface $repo) {} }Резолв конкретной реализации - в конфигурации (DI-контейнер).
DRY (Don't Repeat Yourself) - каждое знание в системе должно иметь единственное представление. Дублирование усложняет поддержку.
KISS (Keep It Simple, Stupid) - решение должно быть максимально простым. Избегать лишней сложности.
YAGNI (You Aren't Gonna Need It) - не добавлять функциональность "на будущее", пока она не нужна. Меньше кода - меньше багов и затрат.
Вместо того чтобы спрашивать объект о данных и принимать решение снаружи, нужно говорить объекту, что делать (передавать команды). Данные и поведение держать вместе.
// Ask: логика снаружи
if ($order->getStatus() === 'paid') { $order->setShipped(true); }
// Tell: логика внутри доменного объекта
$order->markAsShipped(); // внутри проверка статуса и обновлениеУкрепляет инкапсуляцию и переносит поведение в домен.
Объект должен взаимодействовать только с "ближайшими" объектами: своими полями, аргументами методов, созданными внутри объектами. Не вызывать цепочки типа a.getB().getC().doSomething().
// Нарушение
$street = $user->getAddress()->getCity()->getStreet();
// Соблюдение: делегирование
$street = $user->getStreet(); // User сам обращается к Address/CityСнижает связанность, но может приводить к раздуванию интерфейсов - баланс по ситуации.
Предпочтительнее композиция (включать объекты и делегировать), чем наследование для повторного использования поведения. Наследование создает жесткую связь и может приводить к взрывному росту иерархии.
// Наследование: хрупко
class LoggingRepository extends UserRepository { ... }
// Композиция: гибко
class LoggingUserRepository implements UserRepositoryInterface {
public function __construct(private UserRepositoryInterface $inner, private Logger $logger) {}
}Наследование уместно для истинной иерархии типов (is-a), не для переиспользования кода.
Low coupling - модули мало зависят друг от друга. Изменение одного не тянет изменения других. Достигается через интерфейсы, DI, событийную модель.
High cohesion - элементы внутри модуля связаны одной задачей; модуль делает одно понятное дело. Высокая связность упрощает понимание и поддержку.
Цель: низкая связанность между модулями, высокая связность внутри.
Архитектура с разделением на слои по зависимостям: зависимости направлены внутрь, к домену. В центре - сущности и use cases, снаружи - адаптеры (БД, UI, API).
Слои: Entity -> Use Case -> Interface Adapters (presenters, gateways) -> Frameworks and Drivers. Правило: внутренние слои не знают о внешних. Домен не зависит от фреймворка и БД.
Вариант чистой архитектуры: домен в центре, вокруг него слой доменных сервисов (use cases), затем адаптеры (репозитории, внешние API), снаружи - инфраструктура и UI. Зависимости только внутрь. Инфраструктура и БД - детали реализации, подключаются снаружи.
Приложение - шестиугольник: внутри логика, снаружи адаптеры. Порты - интерфейсы (входящие: API, CLI; исходящие: репозиторий, внешний сервис). Адаптеры реализуют порты. Домен не зависит от способа вызова и от конкретной БД/сервиса.
// Порт (исходящий)
interface PaymentGateway { function charge(Order $order): void; }
// Адаптер
class StripePaymentAdapter implements PaymentGateway { ... }Организация по фичам (срезам), а не по техническим слоям. Один срез - одна фича: от API/UI до БД. Папки по use case: CreateOrder, CancelOrder, а не Controllers, Services, Repositories.
Плюсы: изменения локализованы в одном месте, проще онбординг, меньше кросс-зависимостей между "слоями".
Разделение приложения на горизонтальные слои с четкими обязанностями: Presentation (контроллеры, ввод/вывод), Application/Service (бизнес-логика, оркестрация), Domain (сущности, правила), Infrastructure (БД, внешние API). Зависимости сверху вниз; нижний слой не знает о верхнем. Прост в понимании, хорошо подходит для CRUD-приложений.
// Controller -> Service -> Repository
class UserController { public function __construct(private UserService $svc) {} }
class UserService { public function __construct(private UserRepository $repo) {} }
class UserRepository { public function __construct(private PDO $db) {} }Отличие от Clean/Hexagonal: layered допускает прямую зависимость от конкретных классов, а не только от интерфейсов. Подходит для старта; при росте сложности переходят к Clean/Hexagonal.
Подход к разработке, фокус на доменной модели и языке предметной области. Ключевые элементы: Ubiquitous Language, Bounded Context, Entity, Value Object, Aggregate, Domain Event, Repository, Domain Service. Сложная логика живет в домене, не в сервисах или БД.
Явная граница, внутри которой действует единая модель и единый язык. Один и тот же термин в разных контекстах может означать разное (например, Order в заказе и в доставке). Контексты взаимодействуют через четкие контракты (API, события), а не общей БД.
Единый язык команды и кода: термины домена используются в разговоре, в названиях классов, методов, полей. Избегаем технического жаргона там, где можно сказать по-доменному. Код и документация на одном языке.
Кластер сущностей и value objects с единой границей согласованности. Имеет корень (Aggregate Root) - единственная точка входа для изменений. Внешний мир ссылается только на корень; изменения внутри агрегата проходят через него. Границы агрегата - граница транзакции и консистентности.
Entity - идентифицируется по id, имеет жизненный цикл, может меняться. Две сущности с одинаковыми полями - разные объекты, если id разные.
Value Object - определяется только значениями полей, неизменяемый (immutable). Нет идентичности: два VO с одинаковыми полями взаимозаменяемы. Примеры: Money, Address, DateRange.
Событие, произошедшее в домене и значимое для других частей системы. Агрегат генерирует событие при изменении; подписчики реагируют (обновляют read model, шлют уведомления, запускают саги). Реализует слабую связь между контекстами и eventual consistency.
class OrderPlaced { public function __construct(public OrderId $orderId, public Money $total) {} }Фасилитированная воркшоп-практика для моделирования домена: участники (бизнес и разработка) на стикерах выписывают события, команды, агрегаты, политики, внешние системы. Порядок во времени и связи между стикерами выявляют процесс и границы контекстов. Результат - общая картина и язык для DDD.
Переключатели функциональности в рантайме без деплоя. Включение/выключение фичи по конфигу, окружению, пользователю или проценту. Используются для постепенного раската, A/B тестов, отката без релиза. Реализация: флаги в БД/конфиге, проверки в коде; системы типа LaunchDarkly, Unleashed.
Методология для SaaS: код в Git, зависимости явно объявлены, конфиг в окружении, бэкенд-сервисы как подключаемые ресурсы, стадии сборки/релиза/запуска разделены, приложение без состояния (сессии во внешнем хранилище), логи - поток событий, масштабирование через процессы. Цель - переносимость, масштабируемость и устойчивость деплоев.
Повторное выполнение операции дает тот же результат, что и однократное. Для API: повторный запрос с тем же идемпотентным ключом не создает дубликатов (например, двойной платеж). Реализация: ключ в заголовке, проверка по ключу в БД перед выполнением; для мутаций - проверка "уже выполнено".
Гарантия того, что при отсутствии новых изменений все реплики/системы со временем придут к одному состоянию. В отличие от сильной консистентности (линейность, сразу видно все записи), обновления распространяются асинхронно. Применяется в распределенных системах, CQRS, кешах. Компенсации и идемпотентность помогают жить с временными расхождениями.
Stateless - сервис не хранит состояние между запросами; каждый запрос обрабатывается независимо. Масштабирование горизонтальное, отказоустойчивость проще. Сессию выносят в Redis/БД.
Stateful - состояние хранится на сервере (сессии в памяти, sticky sessions). Сложнее масштабировать и балансировать. Используется когда нужна тесная привязка к состоянию (например, WebSocket-сервер).
Генерация кода (скрипты, шаблоны, аннотации + кодоген) уменьшает рутину и ошибки: DTO, мапперы, API-клиенты из OpenAPI, миграции из схемы. Применять когда шаблон повторяется и правила формальны. Минусы: дополнительный шаг сборки, сложнее отлаживать сгенерированное. Не заменяет хорошие абстракции и библиотеки.