Только недавно заинтересовался темой DDD и есть некоторые вопросы по слоям в DDD, что в них должно находится, а так же по архитектуре кода DDD.
Ниже я описал (как я понял) в каких слоях что находитя и за что они отвечают.
Возможно это не совсем правильно и я буду благодарен если кто-то меня поправит если что-то не так.
Слои DDD
Слой UI
Компоненты:
- Контроллеры
- ViewModel
- Формы
Действия
- Валидация входных данных
- Проверка прав доступа
- Логика представления
- Передача данных через DTO в Application сервисы
Слой Application
Компоненты
- Сервис
Действия
- Взаимодействие с Domain слоем через сущности и Domain сервисы
- Передача данных через DTO в вышестоящие слои
- Определение границ транзакции
- Инициализация приложения
Слой Domain
Компоненты
- Сущности
- Domain сервисы
- Интерфейсы репозиториев
Действия
- Проверка данных на соотв. бизнес-требованиям
- Описание взаимосвязий между сущностями
Слой Infrastructure
Компоненты
- Репозитории (реализации интерфейсов)
- Хелперы
- Фреймворк
- DAO (Doctrine)
- Кеширование (Redis, memcached)
- Mailer (почта)
- Logger
- Поиск (Sphinx, Elasticsearch)
- Proxy к сторонним сервисам (SOAP, HTTP)
Действия
- Обеспечение работоспособности всего приложения
Далее я набросал абстрактынй пример кода, что бы понять как правильно выстраивать архитектуру приложения которое основывается на DDD. Возможно не совсем правильно, буду благодарен за замечания
Вот абстрактынй пример задачи:
Допусим есть форма обратной связи у которой есть следующие бизнес требования:
1. Отправить запрос можно только один раз в минуту
2. Отправить запрос может только авторизованный пользователь
3. Отправить запрос может только пользователь с платным аккаунтом и положительным балансом
4. Форма должна быть провалидирована
5. По умолчанию в форме подставляется текущий город пользователя
6. Данные из формы сохраняются в БД
7. При сохранении данных отправляется сообщение администратору сайта
Пример реализации (абстрактный код):
Код: Выделить всё
// UI/Controller/FeedbackController.php
class FeedbackController {
public function action(Request $request, FeedbackService $service, GeoIpService $geoIP, Session $session) {
if ($this->user->isGuest) { // пункт 2 из бизнес-требований
throw new HttpForbbiden();
}
if (time() - $session->lastRequestTime < 60) {
throw new HttpForbbiden(); // пункт 1 из бизнес-требований
}
$form = new FeedbackForm();
$errors = false;
if ($request->isPost) {
if ($form->validate()) { // пункт 4 из бизнес-требований
$dto = $form->getDto();
try {
$service->send($this->user->id, $dto);
$session->lastRequestTime = time();
} catch (DomainException $e) {
$errors = $e->getMessage();
}
} else {
$errors = $form->errors;
}
} else {
$form->city = $geoIP->city; // пункт 5 из бизнес-требований
}
$this->render($form, $errors);
}
}
// UI/Form/FeedbackForm.php
class FeedbackForm {
public $username;
public $city;
public $email;
public $message;
public function validate()
{
// логика валидации формы
}
public function getDto()
{
// возвращаем FeedbcakDto
}
}
// Application/Dto/FeedbackDto.php
class FeedbackDto {
public $name;
public $city;
public $email;
public $message;
}
// Domain/Entity/Feedback.php
class Feedback {
public int $id;
public string $name;
public string $city;
public string $email;
public string $message;
}
// Domain/Entity/Feedback.php
class User {
public UserAccount $account;
}
// Domain/Entity/UserAccount.php
class UserAccount {
public int $id;
public int $userId;
public int $balance = 0;
public bool $isPaid = false;
}
// Infrastructure/DB/FeedbackRepository.php
FeedbackRepository impliments FeedbackRepositoryInterface
{
public function save(Feedback $feedback)
{
// код сохранения
}
}
// Application/Service/FeedbackService.php
class FeedbackService {
private $mailer;
private $repository;
private $dao;
public function __construct(Mailer $mailer, FeedbackRepository $repository, Dao $dao) {
$this->mailer = $mailer;
$this->repository = $repository;
$this->dao = $dao;
}
public function send(User $user, FeedbackDto $dto) {
if (!$user->account->isPaid || $user->account->ballance == 0) { // пункт 3 из бизнес-требований
throw new DomainException("User can't send message");
}
try {
$this->dao->beginTransaction();
// пункт 6 из бизнес-требований
$feedback = new Feedback();
$feedback->email = $dto->email;
$feedback->message = $dto->message;
$feedback->city = $dto->city;
$this->repository->save($feedback);
$this->dao->commit();
// пункт 7 из бизнес-требований
$this->mailer->send([
'to' => 'admin@site'
'from' => $dto->email,
'message' => $dto->message
]);
} catch(Exception $e) {
$this->dao->rollback();
}
}
}
- Где должен быть механизм сборки DTO? В отдельном классе типа FeedbackDtoAssembler?
- Где проверять условие, что запрос можно сделать только раз в минуту? Сейчас сделал в контролере, но кажется это должно быть на уровне domain.
- Где проверять условие, что у пользователя платный аккаунт и баланс положительный? Сейчас сделал это в сервисе FeedbackService на прикладном уровне, но мне кажется это должно быть на доменном уровне в каком-то domain сервисе.
- Где отсылать сообщение администратору сайта? На прикладном уровне или на уровне domain (хотя на этом уровне нельзя так сделать потому что на уровне domain мы ничего не знаем об инфраструктуре и сервисе отправки писем Mailer)
- Должны ли сервисы прикладного уровня (например FeedbackService) обязательно иметь интерфейсы, а сама реализация сервиса должна находится на уровне инфраструктуры? Увидел такой подход, но толком не понял какую пользу оно принесет.