Оригинал: Stop Wasting Money With the Wrong Software Architecture
Перевод для канала Мы ж программист
Я долгое время занимался разработкой программного обеспечения. Я создал множество приложений с нуля, а также присоединился к проектам в середине, когда архитектура уже была готова.
Чему я научился из всех этих проектов, так это:
Независимо от того, насколько чисто начинается проект, он становится запутанным, как только к команде присоединяются новые разработчики.
Но настоящая проблема заключается не в уровне опыта разработчиков. Настоящая проблема заключается в начальной архитектуре программного проекта.
Базовая архитектура
Фреймворки типа Spring Boot, Symfony или Laravel помогают быстро стартовать.
Они дают вам прстую структуру папок, в которые вы можете положить любые:
- Контроллеры для UI / API
- Сервисы для логики
- Сущности/Репозитории для базы данных
Это всё хорошо в начале. Выглядит просто. Вы создаете CustomerService, OrderService, может быть еще EmailService, и всё готово.
Но несколько месяцев спустя вот что происходит:
- Папка services/ переполняется.
- Вы больше не понимаете, что в реальности делает каждый конкретный сейрвис — за исключением, разве что, догадок по имени.
- Вы прыгаете от файла к файлу, пытаясь исправить баг.
Восхождение божественных классов
Сервисные классы вырастают быстро.
Каждый разработчик добавляет новые методы в тот же сервисный файл.
Результат:
- Код-ревью занимает больше времени.
- Больше конфликтов при мердже.
- Много людей работает с одним и тем же файлом.
- Качество падает.
Это стоит компании много денег:
- Дольше исправлять ошибки.
- Дольше проходят ревью.
- Код сложнее изменить.
Разделение ухудшит ситуацию?
В какой-то момент команда говорит:
Ладно, OrderService имеет 1000 строк, давайте поделим его на OrderCreationService и OrderValidationService.
Звучит здраво, так?
Но если у вас было 50 сервисов до этого, теперь будет 300.
Даже если вы организуете их по подпапкам, это превратится в лес классов.
Будем честны…
Это то, что мы видим ежедневно, открывая нашу IDE.
Я прав? Или это байки?
Можно ли это сделать лучше?
Скажем, мы хотим организовать код нашего проекта немного по-другому, чем структура по-умолчанию, которую дает нам фреймворк.
Если говорить о ежедневной работе, вы заметите, что:
Вы всегда работаете над Пользовательскими сценариями (Use Cases) системы.
В один день вы исправляете баг в Регистрации пользователя, в другой – расширяете Валидацию заказа.
Не будет ли проще для всех, если мы вместо группировки кода по Сервисам сгруппируем его по Сценариям?
Что если каждый сценарий (case) будет в свое собственной папке, и там же будет все связанные файлы?
Тогда мы будем мгновенно понимать, куда смотреть, если нужно сделать правку.
Результат будет заметным:
- Меньше конфликтов мерджа — потому что разработчики раотают в отдельных папках сценриев, не в единой папке Service/.
- Быстрее отладка — потому что вы знаете, где живет логика, и вам не нужно перебирать 20 нерелевантных сервисов.
- Проще правки — потому что каждый сценарий самодостаточен и не влияет на несвязанные вещи.
- Чище тесты — потому что вы тестируете изолированный сценарий без побочных эффектов.
- Меньше когнитивная нагрузка — потому что структура соотвествует тому, как ваш мозг воспринимает систему.
Идея такого рода архитектуры не нова.
Фактически это концепция, которая была представлена много лет назад Робертом Мартином (Robert C. Martin) в его книге “Clean Architecture: A Craftsman’s Guide to Software Structure and Design”.
Но все же — почему мы игнорируем это?
Почему мы считаем, что быстрая разработка лучше, чем разработка стабильной и поддерживаемой системы?
В конце концов, мы — разработчики — те, кто больше всего страдает от хаоса в коде.
Мы те, кто вынужден фиксить баги под давлением.
Мы те, кто объясняет продуктовым менеджерам, почему “маленькое изменение” занимает 5 дней.
И вот в чем соль:
Большую часть времени мы и есть причина плохого кода.
Но как выглядит эта волшебная архитектура?
Архитектура, о которой все говорят, но очень немногие реально понимают и еще меньше тех, кто использует?
Я много читал об этом.
Я смотрел бесчисленные видео на YouTube, анализировал массу проектов на GitHub и читал десятки постов в блогах.
И вот что я заметил:
Многих разработчиков останавливает ментальный сдвиг.
Они привыкли к структуре папок, которую дает им фреймворк, а Чистая Арихтектура ломает это.
“Зачем нам это? Это только все осложняет…” — говорят они.
И да, это так:
Типичная сервис-ориентированная структура проще в в использовании поначалу. Вы просто смешиваете вашу доменную логику и инфраструктуру в одном месте. Всё доступно отовсюду. Быстро. Грязно. Готово.
Мы даже не можем представить, что может быть лучший подход.
Но что если…
Что если мы построим нашу систему так, что каждый слой будет иметь свою чистую ответственность?
Где не важно, какой фреймворк запускает логику?
Это суть Чистой архитектуры (Clean Architecture):
Если ваша логика правильно разделена, она больше не зависит от инфраструктуры.
Вы можете построить вашу логику на PHP, используя Symfony. И да — никто так, конечно, не делает — но вы можете переключаться на Laravel, и ваша логика продолжит работать.
“Но зачем мне это?”
Хороший вопрос. Но возможно… это неправильный вопрос.
Вы правы: ни один менеджер не будет переплачивать, чтобы заменить Symfony на Laravel.
Но вот правильный вопрос:
“Мой код зависим от инфраструктуры, в которой он запускается?”
И ответ должен быть НЕТ.
Ваша бизнес-логика не должна завистеть от фреймворка или базы данных. Она должна быть тестируема простыми юнит-тестами. Она должа быть гибкой и надолго.
Вот что делает софт поддерживаемым. И вот что помогает софту выжить.
Но… Я все еще не пойму — зачем отделять логику от инфраструктуры?
Да, я знаю — это самый большой сдвиг в сознании для многих разработчиков.
Но давайте представим. Вы построили вашу бизнес-логику несколько лет назад, и она полностью зависит от фреймворка или от конкретной БД.
Затем фреймворк выпускает обратно несовместимое обновление. И теперь вы проводите дни и даже недели, пытаясь заставить ваше приложение просто снова заработать.
В какой-то момент это становится слишком дорого обновлять.
И что же вы делаете?
Вы просто оставляете ваше приложение работать на устаревшем фреймоворке, надеясь, что ничего не сломается.
Сколько из нас работают с такими системами сейчас?
Ну… я тоже, если честно.
Но если бы мы отделили логику приложения от фреймворка, это не было бы проблемой.
Но как? Как это реально должно выглядеть?
Теперь давайте перейдем к реальному волшебству разделения отвественности — с простым примером кода.
Представим, что мы строим новую систему. И почти как в любом проекте мы начнем с процесса Регистрации пользователя.
С чтоки зрения современного фреймворка типа Symfony или Spring Boot мы делаем что-то вроде этого:
- Создаем сущность User для пользователя
- Создаем UserRepository для доступа к БД
- Создаем UserService со всей логикой
- Создаем UserController для API
- И, конечно, пишем некоторые Unit Tests и Integration Tests
Стандартная структура папок Symfony:
src/
├── Controller/
│ └── UserController.php ← Обработка HTTP-запросов
├── Entity/
│ └── User.php ← Сущность Doctrine для БД
├── Repository/
│ └── UserRepository.php ← Репозиторий Doctrine
├── Service/
│ └── UserService.php ← Бизнес-логика создания пользователей
tests/
├── Unit/
│ └── Service/
│ └── UserServiceTest.php ← Unit tests для логики
├── Integration/
│ └── Controller/
│ └── UserControllerTest.php ← Тесты для API
Вот и все. Простая схема. Ничегого особенного.
Выглядит чисто и быстро — но так ли это?
Поначалу все выглядит хорошо.
Вы создаете несколько классов, соединяете их между собой, пишете тесты и двигаетесь дальше.
Но после нескольких недель или месяцев что-то происходит.
Логика разрастается. Сервисный класс растет.
Внезапно вы обнаруживаете себя скачущим между контроллером, сущностью, сервисом и репозиторием в надежде понять, как работет один пользовательский сценарий.
И вот мы опять пришли к этому — тот же хаос, которого мы пытаемся избежать.
Так что надо сделать по-другому?
Давайте вернемся на шаг назад и взглянем на реальную структуру того, что мы строим.
Мы не строим Сервисы. Мы строим Сценарии.
Нам не нужно заботиться о фреймворке, не на этом уровне. Нам нужно заботиться о том, что делает наш код.
Поэтому вместо грппировки по Техническому слою (включающему Controller, Entity, Service), мы сгруппируемся по Бизнес-отвественности.
И тут появляется Чистая архитектура (Clean Architecture).
Структура Clean Architecture — пример на Symfony
src/
├── Domain/
│ ├── Exception/
│ │ └── EmailAlreadyUsedException.php ← Нарушение бизнес-правила: email уже существует
│ │ └── InvalidEmailException.php ← Валидация email на доменном уровне
│ │ └── InvalidPasswordException.php ← Валидация пароля на доменном уровне
│ ├── Model/
│ │ └── User.php ← Основная модель пользователя / value object (без зависимостей)
│ ├── Repository/
│ │ └── UserRepositoryInterface.php ← Абстракция хранилища для операций с пользователями
│ ├── Security/
│ │ └── PasswordHasherInterface.php ← Абстракция для хэширования паролей
│ └── Validator/
│ └── EmailValidatorInterface.php ← Абстракция для логики валидации email
│ └── PasswordValidatorInterface.php ← Абстракция для логики валидации пароля
├── Application/
│ └── UseCase/
│ └── RegisterUser/
│ └── RegisterUserCommand.php ← Контейнер входящих данных (DTO) для пользовательского сценария
│ └── RegisterUserHandler.php ← Выполнение бизнес-логики регистрации пользователя
├── Infrastructure/
│ ├── Controller/
│ │ └── UserController.php ← HTTP-контроллер (входная точка для пользовательского сценария)
│ ├── Entity/
│ │ └── User.php ← Сущность Doctrine, отражающая схему БД
│ ├── Mapper/
│ │ └── UserMapper.php ← Соотвествие между доменной моделью и сущностью
│ ├── Repository/
│ │ └── UserRepository.php ← Реализация доменного репозитория с помощью Doctrine
│ ├── Security/
│ │ └── SimpleBcryptHasher.php ← Хэшер паролей, специфичный для инфраструктуры
│ └── Validator/
│ └── SymfonyEmailValidator.php ← Реализация email-валидатора на Symfony
│ └── SymfonyPasswordValidator.php ← Реализация валидатора паролей на Symfony
├── Tests/
│ ├── Unit/
│ │ ├── Application/
│ │ │ └── UseCase/
│ │ │ └── RegisterUser/
│ │ │ └── RegisterUserHandlerTest.php ← Тест логики обработчиков (напр., валидация, save call)
│ │ └── Infrastructure/
│ │ └── Mapper/
│ │ └── UserMapperTest.php ← Тест корректности соотвествий (модель ↔ сущность)
│
│ └── Integration/
│ ├── Infrastructure/
│ │ ├── Controller/
│ │ │ └── UserControllerTest.php ← Тест полного цикла запрос-ответ с валидациями
│ │ └── Repository/
│ │ └── UserRepositoryTest.php ← Тест хранилища БД
Теперь у нас есть гораздо больше файлов, и на первый взгляд это выглядит, как перебор.
Но вот компромисс:
Больше файлов дают больше ясности.
Каждый отвественный имеет свое место. Каждый класс делает только одну вещь.
И, что более важно:
Мы точно знаем, куда смотреть, если что-то сломалось или нужны правки.
Эта структура — не про “корпоративность”, это про масштабирование кодовой базы и более быстрое и безопасное взаимодействие.
Давайте углубимся в примеры кода, чтобы внести больше ясности в концепцию.
Доменный слой
Это наш слой абстрации. Он содержит:
- Models / Value Objects
- Interfaces
- Business Exceptions
Он следует принципам Clean Architecture и не знает ничего о Приложении или Инфраструктуре.
Это значит:
Вы не можете включать классы из этих слоев внутрь Домена.
// src/Domain/Model/User.php
namespace App\Domain\Model;
readonly class User
{
public function __construct(
public string $email,
public string $hashedPassword,
public \DateTimeImmutable $createdAt,
public ?int $id = null,
) {}
}
Модель User мы используем во всех внутренних процессах.
Мы только соотнесем ее с сущностью, когда захотим сохранить в БД.
Что мы получили?
- Домен остается чистым и независимым
- Никаких аннотаци Doctrine или логики БД в бизнес коде
// src/Domain/Repository/UserRepositoryInterface.php
namespace App\Domain\Repository;
use App\Domain\Model\User;
interface UserRepositoryInterface
{
public function findByEmail(string $email): ?User;
public function save(User $user): User;
}
Интерфейс Репозитория определяет контракт, который нам нужен в Домене.
Он работет только для Domain User Model, не для Сущности.
Что мы получили?
- Отвязались от Doctrine или логики БД
- Легко делать моки и тестировать
- Сфокусировались на бизнес-поведении, не на технологии
// src/Domain/Exception/EmailAlreadyUsedException.php
namespace App\Domain\Exception;
class EmailAlreadyUsedException extends \RuntimeException
{
public function __construct(string $email)
{
parent::__construct(sprintf("Email '%s' is already in use.", $email));
}
}
// src/Domain/Exception/InvalidEmailException.php
namespace App\Domain\Exception;
class InvalidEmailException extends \RuntimeException
{
public function __construct(string $reason)
{
parent::__construct("Invalid email: $reason");
}
}
// src/Domain/Exception/InvalidPasswordException.php
namespace App\Domain\Exception;
class InvalidPasswordException extends \RuntimeException
{
public function __construct(string $reason)
{
parent::__construct("Invalid password: $reason");
}
}
Домен имеет собственные исключения.
Они выбрасываются бизнес-логикой, не Symfony или какой-то библиотекой валидации.
Почему?
Потому что:
- Мы хотим сделать unit test для чистого определения причин сбоев
- Мы не хотим полагаться на то, что кто-то выбрасывает в Инфраструктуре
- Мы хотим говорить на языке Домена, а не ValidationException и т.д.
// src/Domain/Security/PasswordHasherInterface.php
namespace App\Domain\Security;
interface PasswordHasherInterface
{
public function hash(string $plainPassword): string;
}
Домену нужен хэшированный пароль, но ему не важно как он хэширован.
Мы используем интерфейс и позволим Инфраструктуре сделать эту работу.
Фреймворк (Symfony, Laravel…) обычно уже имеет эту логику, мы просто хотим обернуть и использовать ее.
// src/Domain/Validator/EmailValidatorInterface.php
namespace App\Domain\Validator;
interface EmailValidatorInterface
{
public function validate(string $email): void;
}
// src/Domain/Validator/PasswordValidatorInterface.php
namespace App\Domain\Validator;
interface PasswordValidatorInterface
{
public function validate(string $password): void;
}
Такая же концепция:
Мы не изобретаем колесо — у каждого фреймворка уже есть своя валидация.
Мы определяем интерфейсы в ДОмене и используем существующие инструменты в Инфраструктуре.
Почему?
- Экономит время
- Поддерживает Домен независимым
- Позволяет тестируемые и консистентные исключения (если мы обернем их)
Слой Приложения
Это слой, где живет наша бизнес-логика.
Она инкапсулирована в Use-Cases и не знает ничего об Инфраструктуре.
Это ключевой момент.
Мы хотим протестировать слой с помощью unit tests и быть уверенными, что он ведет себя в точности как ожидается.
Инфраструктура используется позже — только для выполнения бизнес-логики, не для опредления.
Это настоящее разделение отвественности.
Согласно Clean Architecture, этот слой может использовать:
- Классы из Домена
- Свои внутренние классы
Но никогда из Инфраструктуры.
Направление зависимостей — всегда внутрь.
// src/Application/UseCase/RegisterUser/RegisterUserCommand.php
namespace App\Application\UseCase\RegisterUser;
readonly class RegisterUserCommand
{
public function __construct(
public string $email,
public string $plainPassword,
) {}
}
Зачем нам команда?
Просто: Мы хотим скомпоновать все входные данные для этого сценария в единый объект.
Это как именованный входящий пакет — структурированно и предсказуемо. Это делает обработку проще, легче в тестировании, более расширяемой в будущем.
// src/Application/UseCase/RegisterUser/RegisterUserHandler.php
namespace App\Application\UseCase\RegisterUser;
use App\Domain\Model\User;
use App\Domain\Repository\UserRepositoryInterface;
use App\Domain\Security\PasswordHasherInterface;
use App\Domain\Validator\EmailValidatorInterface;
use App\Domain\Validator\PasswordValidatorInterface;
use App\Domain\Exception\EmailAlreadyUsedException;
use App\Domain\Exception\InvalidEmailException;
use App\Domain\Exception\InvalidPasswordException;
readonly class RegisterUserHandler
{
public function __construct(
private UserRepositoryInterface $userRepository,
private PasswordHasherInterface $passwordHasher,
private EmailValidatorInterface $emailValidator,
private PasswordValidatorInterface $passwordValidator
) {
}
public function handle(RegisterUserCommand $command): User
{
try {
$this->emailValidator->validate($command->email);
} catch (\Throwable $e) {
throw new InvalidEmailException($e->getMessage());
}
try {
$this->passwordValidator->validate($command->plainPassword);
} catch (\Throwable $e) {
throw new InvalidPasswordException($e->getMessage());
}
if ($this->userRepository->findByEmail($command->email)) {
throw new EmailAlreadyUsedException($command->email);
}
$user = new User(
email: $command->email,
hashedPassword: $this->passwordHasher->hash($command->plainPassword),
createdAt: new \DateTimeImmutable()
);
return $this->userRepository->save($user);
}
}
Этот обработчик — мозг нашего пользовательского сценария. Он получает данные, валидирует их и сохраняет пользователя.
Заметили что-нибудь?
Мы работем только с интерфейсами из Домена — не с сервисами Symfony, не с Doctrine.
И нам не важно, как они реализованы. Вот в чем суть.
Вы можете поменять алгоритм хэширования пароля. Вы можете выбрать другую библиотеку валидации.
Обработчику не важно.
Он ожидает поведение, не реализацию.
Это Clean.
В нашем случае мы не использовали интерфейс для RegisterUserHandler. Почему?
“Используйте интерфейсы, когда у вас есть много реализаций или вам нужны обратные зависимости.
В остальных случаях — не абстрагируйтесь преждевременно.”
– Uncle Bob, Clean Architecture
У нас есть один простой сервис.
Никаких инъекций динамической стратегии. Никаких переключений. Мы не в мульти-доменном мире DDD сейчас. Поэтому — без интерфейсов.
Clean Architecture – не про содание большего количества файлов и слоев.
Это про создание нужного слоя в нужное время.
Но что если…?
Если у вас модульный монолит с несколькими доменами, тогда да — определяйте контракт (интерфейс) для таких обработчиков.
Почему?
Так другие домены смогут использовать интерфейс без завязки на реализацию.
Это хорошо для разделения между ограниченными контекстами (bounded contexts).
При исключениях
Можем ли мы доверить разработчикам в Инфраструктуре выбрасывать правильные исключения?
Конечно…
Но это не стратегия.
Вот почему мы перехватываем все исключения от валидаторов и конвертируем их в Доменные исключения.
Это делает наш уровень Приложения:
- Консистентным
- Независимым от фреймворка
- Более подходящим для тестирования
- Предсказуемым на длинной дистанции
Слой Инфраструктуры
Это слой, который выполняет нашу бизнес-логику.
Нам нужно определить инструменты, необходимы для исполнения наших сценариев:
- Это HTTP-контроллер?
- Вызов API через API Platform?
- Или вокер, получающий сообщений из RabbitMQ?
Не важно.
Что важно: все это живет за пределами бизнес-логики.
Это настоящее разделение отвественности.
Слой Инфраструктуры знает о слоях Домена и Приложения Domain and Application, и использует их, чтобы запустить приложение. А вот обратное — не верно.
Поскольку ментальный сдвиг понятен, вы наконец ощутите, почему Clean Architecture работает.
// src/Infrastructure/Controller/UserController.php
namespace App\Infrastructure\Controller;
use App\Domain\Exception\EmailAlreadyUsedException;
use App\Domain\Exception\InvalidEmailException;
use App\Domain\Exception\InvalidPasswordException;
use App\Application\UseCase\RegisterUser\RegisterUserHandler;
use App\Application\UseCase\RegisterUser\RegisterUserCommand;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\Routing\Attribute\Route;
#[AsController]
#[Route('/users', methods: ['POST'])]
final readonly class UserController
{
public function __construct(private RegisterUserHandler $handler){}
public function __invoke(Request $request): JsonResponse
{
$data = json_decode($request->getContent(), true);
$command = new RegisterUserCommand(
email: $data['email'] ?? '',
plainPassword: $data['password'] ?? ''
);
try {
$user = $this->handler->handle($command);
} catch (InvalidEmailException|InvalidPasswordException $e) {
return new JsonResponse(['error' => $e->getMessage()], 422);
} catch (EmailAlreadyUsedException $e) {
return new JsonResponse(['error' => $e->getMessage()], 409);
} catch (\Throwable) {
return new JsonResponse(['error' => 'Unexpected error'], 500);
}
$response = new JsonResponse(null, 201);
$response->headers->set('Location', sprintf('/users/%d', $user->id));
return $response;
}
}
Контроллер простой и понятный.
Он получает HTTP-запрос, строит команду, вызывает обработчик и возвращает ответ.
Вот и всё.
// src/Infrastructure/Entity/User.php
namespace App\Infrastructure\Entity;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[ORM\Table(name: 'user')]
class User
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private int $id;
#[ORM\Column(type: 'string', unique: true)]
private string $email;
#[ORM\Column(type: 'string', length: 255)]
private string $password;
#[ORM\Column(type: 'datetime_immutable')]
private \DateTimeImmutable $createdAt;
// getters and setters ...
}
Тут наш пользователь сохраняется в базу данных.
Бизнес-логика не заботится об этом. Она только взаимодействует с доменной моделью — не с сущностью.
// src/Infrastructure/Repository/UserRepository.php
namespace App\Infrastructure\Repository;
use App\Domain\Model\User;
use App\Domain\Repository\UserRepositoryInterface;
use App\Infrastructure\Mapper\UserMapper;
use App\Infrastructure\Entity\User as DoctrineUser;
use Doctrine\Persistence\ManagerRegistry;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
class UserRepository extends ServiceEntityRepository implements UserRepositoryInterface
{
public function __construct(
ManagerRegistry $registry,
private readonly UserMapper $userMapper,
) {
parent::__construct($registry, DoctrineUser::class);
}
public function save(User $user): User
{
$entity = $this->userMapper->toEntity($user);
$this->getEntityManager()->persist($entity);
$this->getEntityManager()->flush();
$this->getEntityManager()->refresh($entity);
return $this->userMapper->fromEntity($entity);
}
public function findByEmail(string $email): ?User
{
$entity = $this->findOneBy(['email' => $email]);
return $entity ? $this->userMapper->fromEntity($entity) : null;
}
}
Этот репозиторий реализует наш доменный интерфейс и использует Doctrine под капотом.
Но наша бизнес-логика никогда не видит это.
Так мы можем сохранять низкую связанность.
// src/Infrastructure/Mapper/UserMapper.php
namespace App\Infrastructure\Mapper;
use App\Domain\Model\User as DomainUser;
use App\Infrastructure\Entity\User as DoctrineUser;
readonly class UserMapper
{
public function toEntity(DomainUser $user): DoctrineUser
{
$entity = new DoctrineUser();
if ($user->id !== null) {
$entity->setId($user->id);
}
$entity->setEmail($user->email);
$entity->setPassword($user->hashedPassword);
$entity->setCreatedAt($user->createdAt);
return $entity;
}
public function fromEntity(DoctrineUser $entity): DomainUser
{
return new DomainUser(
email: $entity->getEmail(),
hashedPassword: $entity->getPassword(),
createdAt: $entity->getCreatedAt(),
id: $entity->getId()
);
}
}
Этот маленткий класс делает грязную работу: соотвествие (mapping) между доменной моделью и сущностью БД.
Представьте: однажды вы переключились на MongoDB.
Догадайтесь, что нужно поменять?
Только Mapper. Бизнес-логика остаентся нетронутой.
Это мощно.
// src/Infrastructure/Security/SimpleBcryptHasher.php
namespace App\Infrastructure\Security;
use App\Domain\Security\PasswordHasherInterface;
readonly class SimpleBcryptHasher implements PasswordHasherInterface
{
public function hash(string $plainPassword): string
{
return password_hash($plainPassword, PASSWORD_BCRYPT);
}
}
И снова — домен не заботится о том, как хэшируется пароль.
Он просто ожидает, что пароль будет захэширован.
Теперь вы можете использовать любые инрументы Symfony — bcrypt, argon2, sodium… что хотите.
// src/Infrastructure/Validator/SymfonyEmailValidator.php
namespace App\Infrastructure\Validator;
use App\Domain\Validator\EmailValidatorInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Component\Validator\Constraints\Email;
use Symfony\Component\Validator\Constraints\NotBlank;
readonly class SymfonyEmailValidator implements EmailValidatorInterface
{
public function __construct(private ValidatorInterface $validator) {}
public function validate(string $email): void
{
$violations = $this->validator->validate($email, [
new NotBlank(),
new Email(),
]);
if (count($violations) > 0) {
throw new \InvalidArgumentException((string)$violations);
}
}
}
// src/Infrastructure/Validator/SymfonyPasswordValidator.php
namespace App\Infrastructure\Validator;
use App\Domain\Validator\PasswordValidatorInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\Length;
readonly class SymfonyPasswordValidator implements PasswordValidatorInterface
{
public function __construct(private ValidatorInterface $validator) {}
public function validate(string $password): void
{
$violations = $this->validator->validate($password, [
new NotBlank(),
new Length(['min' => 8]),
]);
if (count($violations) > 0) {
throw new \InvalidArgumentException((string)$violations);
}
}
}
Тут мы делегируем системе валидации Symfony — зачем изобретать колесо?
Пусть фреймворк сделает то, в чем он хорош.
Наши бизнес-логика не пострадает.
Это и есть идея Clean Architecture.
Заключение
Clean Architecture — не о том, чтобы писать больше кода. Это о том, чтобы писать лучший код.
Да, у вас будет больше файлов.
Да, поначалу это кажется “слишком много структуры”.
Но взамен вы получите:
- Чистое разделение отвественности
- Тестируемая и надежная бизнес-логика
- Свобода замены инфраструктуры без воздействия на ядро
- Удобство в том, что каждый сценарий изолирован и предсказуем
Это не о “корпоративности” — это о целеустремленности.
Вы меняете краткосрочную скорость на долгосрочную поддерживаемость. И это выгодная сделка.