Вопрос по слоям DDD и архитектуре проектов

Обсуждаем, как правильно строить приложения
Ответить
troublegum
Сообщения: 27
Зарегистрирован: 2010.12.17, 11:33
Контактная информация:

Вопрос по слоям DDD и архитектуре проектов

Сообщение troublegum »

Всем привет!

Только недавно заинтересовался темой 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) обязательно иметь интерфейсы, а сама реализация сервиса должна находится на уровне инфраструктуры? Увидел такой подход, но толком не понял какую пользу оно принесет.
anton_z
Сообщения: 483
Зарегистрирован: 2017.01.15, 15:01

Re: Вопрос по слоям DDD и архитектуре проектов

Сообщение anton_z »

troublegum писал(а): 2020.07.10, 12:58 - Где должен быть механизм сборки DTO? В отдельном классе типа FeedbackDtoAssembler?
Можно в форме, можно в контроллере. Городить отдельный билдер для DTO нет смысла.
troublegum писал(а): 2020.07.10, 12:58 - Где проверять условие, что запрос можно сделать только раз в минуту? Сейчас сделал в контролере, но кажется это должно быть на уровне domain.
В контроллере. Ограничение рейта это интерфейсная вещь, она необходима и вытекает из взаимодействия с внешним миром и поэтому должна находиться в слое UI. HTTP middleware для этого самое то.
troublegum писал(а): 2020.07.10, 12:58 - Где проверять условие, что у пользователя платный аккаунт и баланс положительный? Сейчас сделал это в сервисе FeedbackService на прикладном уровне, но мне кажется это должно быть на доменном уровне в каком-то domain сервисе.
Проверка целиком опирвается на данные класса User. Вывод: инкапсулировать в методе класса User. Вызывать проверку можно в App service или Domain service.

troublegum писал(а): 2020.07.10, 12:58 - Где отсылать сообщение администратору сайта? На прикладном уровне или на уровне domain (хотя на этом уровне нельзя так сделать потому что на уровне domain мы ничего не знаем об инфраструктуре и сервисе отправки писем Mailer)
Вы всегда можете закрыть Mailer интерфейсом, который будет в домене. Такой интерфейс будет аналогичен интерфейсам репозиториев. Хорошо отсылать сообщения в оюработчике доменных событий, он может быть на любом уровне и даже за границами контекста.
troublegum писал(а): 2020.07.10, 12:58 - Должны ли сервисы прикладного уровня (например FeedbackService) обязательно иметь интерфейсы, а сама реализация сервиса должна находится на уровне инфраструктуры? Увидел такой подход, но толком не понял какую пользу оно принесет.
Нет. Так делают, когда надо декорировать App services. Если вам не надо, не стоит делать интерфейсы.

Код: Выделить всё

$feedback->email = $dto->email;
$feedback->message = $dto->message;
$feedback->city = $dto->city;	
Вместо этого лучше использовать конструктор сущности, передав параметры туда, если хотите RichDomainModel. Если Anemic - то все нормально.

P.S. Уже и формы обратной связи по DDD делают, жесть. Для задач такого типа это абслютно ненужно.
troublegum
Сообщения: 27
Зарегистрирован: 2010.12.17, 11:33
Контактная информация:

Re: Вопрос по слоям DDD и архитектуре проектов

Сообщение troublegum »

Спасибо за ваш ответ!
P.S. Уже и формы обратной связи по DDD делают, жесть. Для задач такого типа это абслютно ненужно.
Я всего лишь привел абстрактный пример, конечно для одной формы применение DDD это overhead :)

Еще есть вопросы:

Допустим у нам надо вывести несколько списков статей на разных страницах сайта:

Есть такие списки:

1) Список всех статей с рубриками, с тегами, с автором и кол-вом комментариев
2) Список последних статей просто с автором и кол-вом комментариев
3) Список статей без всего (рубрик, тегов и т.д.)

Вопрос! Как правильно надо реализовывать получение данных для представления?

Пример кода как можно это сделать в сервисе:

Код: Выделить всё

// Application/Post/PostService.php
class PostService
{
	// См. п1.
	public function getAllWithRubricsAndTagsAndAuthorAndCommentCount():array
	{
		$dtos = [];
		foreach ($this->postRepository->findAll(...) as $post) {
			$dtos[] = PostDto::create($post);
		}
		
		return $dtos; // возвращаем массив DTO статей
	}
	
	// п2.
	public function getAllWithAuthorAndCommentCount(): array
	{
		return $dtos; // возвращаем массив DTO статей
	}
	
	// п3. 
	public function getAll(): array
	{
		return $dtos; // возвращаем массив DTO статей
	}
}

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

Поэтому мне кажется что надо параметризировать запросы данных списков. Где-то видел, что это называется реализация через передачу Option-объекта (он содержит параметры выборки) сервису.

Пример как это выглядит:

Код: Выделить всё

// Application/Post/PostOptions.php
class PostOptions
{
	// Указываем параметры выборки
	public $withCommentsCount = false;
	public $withAuthor = false;
	public $withRubric = false;
}

// UI/Controller/PostController.php
class PostController
{
	public function indexAction()
	{
		$optionListWhithAuthorAndRubric = new PostOptions();
		$optionListWhithAuthorAndRubric->withAuthor = true;
		$optionListWhithAuthorAndRubric->withRubric = true;
		$listWhithAuthorAndRubric = $this->postService->getAll($optionListWhithAuthorAndRubric); // Получаем список с авторами и рубриками
		
		$list = $this->postService->getAll(new PostOptions()) // Получаем список статей без всего
	}
}

// Application/Post/PostService.php
class PostService
{
	
	public function getAll(PostOptions $options):array
	{
		$entities = $this->postRepository->findAllByOptions($options);
		// ...
		return $dtos; // возвращаем массив DTO статей согласно переданным Option
	}
Но тут тогда нужно что бы и репозитории тоже умели отдавать данные из БД по переданным параметрам.

Вопрос!
* Нужно ли для каждого списка делать отдельный класс DTO? Мне кажется это излишним и опять же это ведет к тому что у нас прикладной уровень зависит от тех данных которые нужны слою представления.
* Какой подход более правильный?
Аватара пользователя
ElisDN
Сообщения: 5841
Зарегистрирован: 2012.10.07, 10:24
Контактная информация:

Re: Вопрос по слоям DDD и архитектуре проектов

Сообщение ElisDN »

Не используйте эти же сущности и репозитории для вывода списков на сайте. Выводите через прямые запросы как в viewtopic.php?p=248689#p248689
anton_z
Сообщения: 483
Зарегистрирован: 2017.01.15, 15:01

Re: Вопрос по слоям DDD и архитектуре проектов

Сообщение anton_z »

ElisDN писал(а): 2020.07.15, 09:34 Не используйте эти же сущности и репозитории для вывода списков на сайте. Выводите через прямые запросы как в viewtopic.php?p=248689#p248689
И самому результаты JOIN разбирать, типы данных приводить, да ну... Уже ORM есть, маппинги прописаны, зачем повторяться? DRY.
Отдельную ReadModel можно делать, но для этого нужны основания. Я не вижу особого смысла делать отдельный ReadModel без полноценного CQRS на уровне хранилищ. Может переубедите? :D
troublegum писал(а): 2020.07.14, 19:50
Поэтому мне кажется что надо параметризировать запросы данных списков. Где-то видел, что это называется реализация через передачу Option-объекта (он содержит параметры выборки) сервису.
Ага, а потом запилить свой доменный QueryLanguage =) Не стоит оно того, только если забавы ради
troublegum писал(а): 2020.07.14, 19:50
Вопрос! Как правильно надо реализовывать получение данных для представления?
В вашем примере вы делаете один PostService. Я бы сделал для каждой страницы свой сервис, так как это будет лучше соответсвовать принципу открытости/закрытости, так как в случае добавления новых страниц вам не придется менять сущетсвующие классы, а можно будет просто добавить новые (в ООП это основной способ борьбы со сложностью внесения изменений). Если у сервисов начнет появляться дублирующийся код - композиция в помощь. Один общий класс сервиса будет обладать низкой cohesion, так как методы ничем не связаны. Параметризацию с помощью Options не стал бы делать по той же причине (OCP). Ее можно сделать закрытой для изменения, но для этого придется мутить что-то вроде своего QueryLanguage, но задача не стоит таких усилий.
troublegum писал(а): 2020.07.14, 19:50
Но если следовать такому подходу, то у нас получается что прикладной слой зависит от слоя призентации. Так как на каждый чих слоя презентации мы создаем новый метод в сервисе прикладного слоя.
Классы предоставляют контракты не просто так, а для того чтобы ими пользовались. В конечном счете контракт класса определяется пользователями этого контракта ну или обощенными/генерализованными предположениями, как этот контракт будет использоваться в клиентском коде. От этого не уйдешь.
troublegum писал(а): 2020.07.14, 19:50
* Нужно ли для каждого списка делать отдельный класс DTO? Мне кажется это излишним и опять же это ведет к тому что у нас прикладной уровень зависит от тех данных которые нужны слою представления.
Я бы вообще из сервсов сущности возвращал, применительно к решаемой задаче. Никакого криминала тут нет. Вышележащему слою можно зависть от нижележащего.
troublegum
Сообщения: 27
Зарегистрирован: 2010.12.17, 11:33
Контактная информация:

Re: Вопрос по слоям DDD и архитектуре проектов

Сообщение troublegum »

Спасибо за ваш ответ! :)

Я думаю, что можно сделать так:

* Для каждой страницы сделать отдельный сервис: IndexPageService, PostListService, PostViewService, AuthorPostListService. Так действительно будет лучше.
* Для получения данных для списков статей сделать свой отдельный репозиторий: PostListRepository у которого есть методы для получения каждого списка, методы будут возвращать просто массивы данных (не сущности или модели, так как для представления нам нужно просто данные) из БД в соотвествии с условиями, которые в сервисе преобразуются в DTO.

Код: Выделить всё

class PostListRepository { 
	// Возвращаем просто массив с данными (не сущности или модели) для списка статей с рубриками и авторами
	public function getListWithRubricAndAuthor(): array
	{
		return $data;
	}
	
	/ Возвращаем просто массив с данными (не сущности или модели) для списка статей с тегами
	public function getListWithTags(): array
	{
		return $data;
	}
}
Аватара пользователя
ElisDN
Сообщения: 5841
Зарегистрирован: 2012.10.07, 10:24
Контактная информация:

Re: Вопрос по слоям DDD и архитектуре проектов

Сообщение ElisDN »

anton_z писал(а): 2020.07.16, 02:49 И самому результаты JOIN разбирать, типы данных приводить, да ну... Уже ORM есть, маппинги прописаны, зачем повторяться? DRY.
А потом вдруг надо в списке постов вывести comments_сount и last_comment_date. И костыли поплыли.
anton_z
Сообщения: 483
Зарегистрирован: 2017.01.15, 15:01

Re: Вопрос по слоям DDD и архитектуре проектов

Сообщение anton_z »

ElisDN писал(а): 2020.07.16, 20:05 А потом вдруг надо в списке постов вывести comments_сount и last_comment_date. И костыли поплыли.
Доктрина не умеет relation count и relation с доп. условием?
А вы предлагаете руками делать разбор JOIN и приведение типов, это не большие костыли?
По-моему это просто разные способы решения одной и той же задачи со своими плюсами и минусами. А вы сразу "костыли".
Последний раз редактировалось anton_z 2020.07.17, 02:26, всего редактировалось 3 раза.
anton_z
Сообщения: 483
Зарегистрирован: 2017.01.15, 15:01

Re: Вопрос по слоям DDD и архитектуре проектов

Сообщение anton_z »

troublegum писал(а): 2020.07.16, 18:46
не сущности или модели, так как для представления нам нужно просто данные
По мне так сущности это и есть данные. Не понимаю, почему вы не хотите их вместо DTO вернуть, ну да ладно.
troublegum писал(а): 2020.07.16, 18:46
* Для получения данных для списков статей сделать свой отдельный репозиторий: PostListRepository у которого есть методы для получения каждого списка, методы будут возвращать просто массивы данных (не сущности или модели, так как для представления нам нужно просто данные) из БД в соотвествии с условиями, которые в сервисе преобразуются в DTO.

Код: Выделить всё

class PostListRepository { 
	// Возвращаем просто массив с данными (не сущности или модели) для списка статей с рубриками и авторами
	public function getListWithRubricAndAuthor(): array
	{
		return $data;
	}
	
	/ Возвращаем просто массив с данными (не сущности или модели) для списка статей с тегами
	public function getListWithTags(): array
	{
		return $data;
	}
}
Такой репозиторий это будет тот же PostService. Он также не будет закрыт для изменения и с ростом количества страниц этот класс будет разрастаться. Я бы внутри сервисов заюзал бы репы доктрины, составил бы нужные запросы с помощью QueryBuilder. В итоге, что бы я получил:

1. Репозиторий и QueryBuilder закрыты для изменения при добавлении страниц.
2. Слой сервисов, которые удовлетворяют SRP, причем с добавлением страниц добавляются новые сервисы.
3. Сущности, которые представляют отдельные единицы данных. Петрушка с дополнительными стат. значениями будет иметь место, сущности не закрыты для изменения в этом плане, но цена добавления DTO и кастомного маппинга на них, либо вообще запросов на голом SQL перевешивает данный минус. Чтобы однозначно принять тут решение нужно больше информации о кейсе.
Аватара пользователя
ElisDN
Сообщения: 5841
Зарегистрирован: 2012.10.07, 10:24
Контактная информация:

Re: Вопрос по слоям DDD и архитектуре проектов

Сообщение ElisDN »

anton_z писал(а): 2020.07.17, 01:31 Доктрина не умеет relation count и relation с доп. условием?
Умеет только если эта связь в ней прописана. И делает это через JOIN с GROUP BY, поэтому если БД в строгом режиме, то передавайте привет неверному LIMIT и ошибкам негруппировки каждого поля. Или если что-то не прописано, то произвольный JOIN или подзапрос уже сложно сделать через DQL.
anton_z писал(а): 2020.07.17, 01:31 А вы предлагаете руками делать разбор JOIN и приведение типов, это не большие костыли?
А что сложного? Вот быстро и нативно:

Код: Выделить всё

public function latest(int $limit): array
{
    $stmt = $this->connection->createQueryBuilder()
        ->select([
            'p.id',
            'p.title',
            '(SELECT COUNT(c.*) FROM comments c WHERE c.post_id = p.id) AS comments_count'
        ])
        ->from('posts', 'p')
        ->orderBy('p.date')
        ->setMaxResults($limit)
        ->execute();

    return $stmt->fetchAll(FetchMode::ASSOCIATIVE);
}
Это весь ручной мэппинг безо всяких GROUP BY.

По типам колическо выдаст числом, а не строкой. А если нужна типизация объекта, а не ассоциативный массив, то фетчим в DTO:

Код: Выделить всё

$stmt->setFetchMode(FetchMode::CUSTOM_OBJECT, LatestPost::class);

return $stmt->fetchAll();
anton_z писал(а): 2020.07.17, 01:31 По-моему это просто разные способы решения одной и той же задачи со своими плюсами и минусами. А вы сразу "костыли".
Да. Разные способы. Либо через нативный запрос на три поля, либо через перегонку всех данных рефлексией через тяжеловесную Doctrine.

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

Если же как у вас структура чтения совпадает со структурой сущностей в БД, то можно для рендера брать напрямую те же сущности.
anton_z
Сообщения: 483
Зарегистрирован: 2017.01.15, 15:01

Re: Вопрос по слоям DDD и архитектуре проектов

Сообщение anton_z »

ElisDN писал(а): 2020.07.17, 21:25
Это весь ручной мэппинг безо всяких GROUP BY.
ElisDN писал(а): 2020.07.17, 21:25
Да. Разные способы. Либо через нативный запрос на три поля, либо через перегонку всех данных рефлексией через тяжеловесную Doctrine.

Хех, как вы передернули, свели общую задачу разбора результатов запросов к одному простому случаю :D Так делают профессиональные спорщики из Интернета :D Вы конкретно задачу подсчета количества комментов показали, а я в общем про разбор результатов запросов. Если нам нужно, например, преобразование дат в DateTimeImmutable сделать, или есть JSON-колонки, которые нужно десерияализовать, то это будет не так просто. Если нужно строки вместе со связанными строками при помощи JOIN вытащить? И это будет несложно, ну ну...

ElisDN писал(а): 2020.07.17, 21:25
Если проект разбит или собирается быть разбит на модули с разнесением сущностей по ограниченным контекстам, то такие сущности без связей для чтения бесполезны.
Если. Тут не говорится. что комментарии и статьи будут в разных контекстах. Исходим из задачи, или всегда из пушки по воробушкам?
Аватара пользователя
ElisDN
Сообщения: 5841
Зарегистрирован: 2012.10.07, 10:24
Контактная информация:

Re: Вопрос по слоям DDD и архитектуре проектов

Сообщение ElisDN »

anton_z писал(а): 2020.07.18, 00:14 Если нам нужно, например, преобразование дат в DateTimeImmutable сделать
Для вывода из API в JSON чаще всего это лишний раз преобразовывать через объект не нужно.

Шаблонизаторы тоже спокойно воспринимают даты строкой без DateTime или Carbon. Но с внедрением JS-фреймворков в приложения шаблонизаторы уже мало актуальны.
anton_z писал(а): 2020.07.18, 00:14 или есть JSON-колонки, которые нужно десериализовать, то это будет не так просто. Если нужно строки вместе со связанными строками при помощи JOIN вытащить? И это будет несложно, ну ну...
Тогда будет на одну строку больше:

Код: Выделить всё

return [
    'id' => $row['id'],
    'title' => $row['title'],
    'tags' => json_decode($row['tags'))
];
или в более сложных будет array_map(fn, json_decode($row['tags'))).
anton_z писал(а): 2020.07.18, 00:14 Если. Тут не говорится. что комментарии и статьи будут в разных контекстах.
Если будет действительно построение доменной модели по DDD, то они и будут в отдельных контекстах. А DDD без разделения на контексты противоречит самому DDD.
anton_z писал(а): 2020.07.18, 00:14 Исходим из задачи, или всегда из пушки по воробушкам?
Исходим из раздела, вопроса и названия темы.

Если вам это не нужно для ваших задач или не нравится по личным причинам, то и не отвечайте в темах о DDD.
anton_z
Сообщения: 483
Зарегистрирован: 2017.01.15, 15:01

Re: Вопрос по слоям DDD и архитектуре проектов

Сообщение anton_z »

ElisDN писал(а): 2020.07.18, 18:56
или в более сложных будет array_map(fn, json_decode($row['tags'))).
Это уже замена функционала ORM.
ElisDN писал(а): 2020.07.18, 18:56
Тогда будет на одну строку больше:
Результаты JOIN "потеряли", тоже одной строкой разберете? :D Или выяснится что уже и JOIN не нужен, так же как DateTime?
ElisDN писал(а): 2020.07.18, 18:56
Если будет действительно построение доменной модели по DDD, то они и будут в отдельных контекстах. А DDD без разделения на контексты противоречит самому DDD.
Что то я уже неуверен в том что вы DDD правильно понимаете. Решение будет зависеть от проекта и требований, а не потому что "это DDD, детка".

ElisDN писал(а): 2020.07.18, 18:56
Если вам это не нужно для ваших задач или не нравится по личным причинам, то и не отвечайте в темах о DDD.
Этот учительский "опус" оставлю без внимания.
Аватара пользователя
ElisDN
Сообщения: 5841
Зарегистрирован: 2012.10.07, 10:24
Контактная информация:

Re: Вопрос по слоям DDD и архитектуре проектов

Сообщение ElisDN »

anton_z писал(а): 2020.07.19, 00:13 Это уже замена функционала ORM.
Это зависит от вашего понимания объекта в ORM.

Сложные ORM используем по назначению для мэппинга реляционных данных на доменные объекты с бизнес-логикой. На агрегаты по юзкейсам. С рефлексией, Unit of Work, транзакциями, оптимистичными блокировками и прочими плюшками ORM, нужными для работы с агрегатами в модели записи.

Для чтения же бизнес-логика и эти навороты нам практически не нужны. Вместо объектов там хватит простого PDO и примитивных структур.
anton_z писал(а): 2020.07.19, 00:13 Результаты JOIN "потеряли", тоже одной строкой разберете? :D Или выяснится что уже и JOIN не нужен, так же как DateTime?
Ещё добавлю пару строк и в итоге получу такой примитивный код:

Код: Выделить всё

$posts = $this->connection->createQueryBuilder()
    ->select([
        't.id',
        't.title',
        'a.name as author_name',
    ])
    ->from('posts', 'p')
    ->innerJoin('p', 'authors', 'a', 'p.author_id = a.id')
    ->setMaxResults($limit)
    ->execute()->fetchAll(FetchMode::ASSOCIATIVE);

$tags = $this->connection->createQueryBuilder()
    ->select('t.name')->from('tags', 't')
    ->andWhere('t.post_id IN :posts')
    ->setParameter(':posts', array_column($posts, 'id'), Connection::PARAM_INT_ARRAY)
    ->execute()->fetchAll(FetchMode::ASSOCIATIVE)

return array_map(fn (array $post) => [
    'id' => $post['id'],
    'title' => $post['title'],
    'author' => [
        'name' => $post['author_name'],
    ],
    'tags' => array_filter($tags, fn (array $tag) => $tag['post_id'] === $post['id']),
], $posts)
Без необходимости грузить и гонять по сети все остальные поля поста и автора. И при необходимости спокойно оберну кешированием. Если для вас этот код "слишком сложный", то увы.

Или от JOIN-ов перейду на Application Side JOIN в контроллере:

Код: Выделить всё

$posts = $this->posts->last($limit);
$authors = $this->authors->allByIds(array_column($posts, 'id'));
$tags = $this->tags->allByIds(array_column($posts, 'id'));

return array_map(fn (array $post) => [
    'post' => $post,
    'author' => first(array_filter($authors, fn(array $author) => $author['id'] === $post['author_id'])),
    'tags' => array_filter($tags, fn (array $tag) => $tag['post_id'] === $post['id']),
], $posts)
anton_z писал(а): 2020.07.19, 00:13 Решение будет зависеть от проекта и требований, а не потому что "это DDD, детка".
От этого зависит первоначально использовать DDD или нет. И уже:

- Если нет, то делаем какой-нибудь CRUD. Что бы было легко только сначала, а потом как-нибудь разберёмся.
- Если же выбрали DDD, то делаем сразу по DDD, чтобы с контролем сложности было относительно легко всегда.
anton_z писал(а): 2020.07.19, 00:13 Что то я уже неуверен в том что вы DDD правильно понимаете.
Я понимаю DDD глобально как подход проектирования приложения через анализ предметной области с определением контекстов с единым языком в каждом для разбиения модели на поддомены по этим контекстам. С построением карты взаимодействия контекстов. Как и изначально завещал Эванс. А не как примитивные низкоуровневые вопросы вроде "как замапить json".

После подробного бизнес-анализа сама сущность Post может быть разнесена по контекстам на шесть агрегатов Post/Url, Post/Taxonomy, Post/Content, SEO/Page, Rating/Subject, Linking/Item. Также Author может быть разнесена на Auth/User, Profile, Rating и Subscriber. Со связями только по идентификаторам и с записью каждой в свою таблицу.

В итоге для отображения в UI в виджете LastPosts намного проще сделать нативный запрос с нужными SELECT и JOIN, чем возиться с запросом и склейкой десятка сущностей через ORM.

Если поддомены не выделены и сущность Post одна и напрямую связана с Author, то проще использовать её через ORM.

А если делать по DDD, где сущность Post разнесена по контекстам, то выбор в пользу разделения моделей записи с Doctrine и модели чтения с PDO естественен и удобен. Тогда прогон через бизнес-сущности и обратно для рендеринга при чтении является оверхэдом и сильно привязывает нас к самим сущностям.

Раздельный подход даёт независимость от структуры агрегатов. И позволяет легко читать те же структуры как из SQL-базы, так и легко переключиться на чтение готовых массивов из ElasticSearch или Redis.
anton_z
Сообщения: 483
Зарегистрирован: 2017.01.15, 15:01

Re: Вопрос по слоям DDD и архитектуре проектов

Сообщение anton_z »

ElisDN писал(а): 2020.07.18, 18:56
Если вам это не нужно для ваших задач или не нравится по личным причинам, то и не отвечайте в темах о DDD.
ElisDN писал(а): 2020.07.19, 12:08
Без необходимости грузить и гонять по сети все остальные поля поста и автора. И при необходимости спокойно оберну кешированием. Если для вас этот код "слишком сложный", то увы.
Надо же, какой токсичный человек, все время собеседника куда-то послать или унизить пытается.

Для меня этот код просто бесполезен. Для решения этой задачи есть отличные инструменты, которые очень удобно использовать. С ORM этот запрос может быть заменен парой срок. Экономия в строках в масштабах проекта будет очень большая.
ElisDN писал(а): 2020.07.19, 12:08
- Если нет, то делаем какой-нибудь CRUD. Что бы было легко только сначала, а потом как-нибудь разберёмся.
- Если же выбрали DDD, то делаем сразу по DDD, чтобы с контролем сложности было относительно легко всегда.
Вы реально не видите среднего? Либо CRUD либо DDD?

ElisDN писал(а): 2020.07.19, 12:08 Как и изначально завещал Эванс.
Во-во еще на чьи-то увещевания давайте помолимся.
ElisDN писал(а): 2020.07.19, 12:08 После подробного бизнес-анализа сама сущность Post может быть разнесена по контекстам на шесть агрегатов Post/Url,..
Я не понимаю зачем вы так многословно это всё расписываете. Мне это не нужно. Если у меня будет такая задача, я решу ее без ваших рекомендаций. Обсуждаем же не это вообще.
Аватара пользователя
ElisDN
Сообщения: 5841
Зарегистрирован: 2012.10.07, 10:24
Контактная информация:

Re: Вопрос по слоям DDD и архитектуре проектов

Сообщение ElisDN »

anton_z писал(а): 2020.07.19, 14:58 Надо же, какой токсичный человек, все время собеседника куда-то послать или унизить пытается.
Если в каждую тему по DDD будете с порога залетать со своими:
anton_z писал(а): 2020.07.19, 14:58 Из пушки по воробушкам... Для меня этот код просто бесполезен... Мне это не нужно... Ну ну... Давайте помолимся...
провоцирующими срач, то вас каждый раз и будут посылать. Это предсказуемо.
anton_z
Сообщения: 483
Зарегистрирован: 2017.01.15, 15:01

Re: Вопрос по слоям DDD и архитектуре проектов

Сообщение anton_z »

ElisDN писал(а): 2020.07.19, 21:13 будут посылать. Это предсказуемо.
Это вы будете, потому что вы такой. За всех вы говорить не можете, вы не знаете, что они думают. По себе людей не судят. Мне человек спасибо сказал, между прочим, почитайте.
ElisDN писал(а): 2020.07.19, 21:13
Из пушки по воробушкам... Для меня этот код просто бесполезен... Мне это не нужно... Ну ну... Давайте помолимся...
И это вас спровоцировало на унижения? Я про вас и про вашу личность ничего не говорил, пока вы не начали пытаться меня унизить и отправить куда-то из темы. Остается только развести руками и оставить вас вместе с вашими опусами.
Ответить