Specification pattern

Обсуждаем, как правильно строить приложения
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

Re: Specification pattern

Сообщение zelenin »

slavcodev писал(а):И еще, вот написал ты в N сервисах билдер типа этого

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

class MyService
{
  public function handle()
  {
    $query = new QueryBuilder();
    $query->page(2, 15)
      ->equal(['status' => Post::STATUS_PUBLISHED])
      ->sign(['date' , '>', '2015-01-01'])
      ->like(['name' , 'отпуск']);

    $posts = $repository->getPostsByQuery($query);
  }
} 
И поменялся у тебя домен, и нет больше "name" атрибута, есть "firstName" + "lastName", вот весело будет..
обычно. Изменили сущность в домене - изменили же в домене репо и спеку.
slavcodev писал(а): Всего-то надо было в репозиторий добавить

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

public function getPostsBySearchString($q) : array;
И в нем рулить какие поля можно делать like а по каким нет.
не совсем понимаю как данный метод полноценно заменит QueryObject.
Понимаешь, в данной теме мы уже не про теорию, а про практику, в которой в методе как минимум должна быть пагинация. Плюс, мне кажется, ты споришь с одним из вариантов предложенной реализации, в то время как я про идею QueryObject - как оно будет реализовано с практической точки зрения, я не навязываю.
Аватара пользователя
slavcodev
Сообщения: 3134
Зарегистрирован: 2009.04.02, 21:42
Откуда: Valencia
Контактная информация:

Re: Specification pattern

Сообщение slavcodev »

Остальные два (сейчас посмотрел) тоже не имеет ничего общего с DDD, а являются врапперами над SQL (Like, Equals и тд). Разбрасывание бизнес логикой, а это как раз и есть бизнес логика как выбирается та или иная группа сущностей

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

$specification = new CollectionSpecification([    
    new PageCondition(2, 15), // 2-я страница по 15 
    new EqualCondition(['status' => Post::STATUS_PUBLISHED]),
    new SignCondition(['date' , '>', '2015-01-01']),
    new LikeCondition(['name' , 'отпуск']),
]);
за пределы домена, в сервисы приложения, не ошибка, но и не ДДД, не обманывайте себя. Внутри репозитория, да может быть как особенная имплементация билдера для поиска в БД.
Жду Yii 3!
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

Re: Specification pattern

Сообщение zelenin »

slavcodev писал(а):Остальные два (сейчас посмотрел) тоже не имеет ничего общего с DDD, а являются врапперами над SQL (Like, Equals и тд).
я хотел написать, что реализация должна быть абстрактной от sql, но упустил, т.к. мы про идею, а не про реализацию.

Идея - в передаче в репозиторий QueryObject (на основе UL) и поиска на его основе.

Выглядеть это может примерно вот так:

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

$query = (new PostQuery)
    ->nameEqual('Новости отпуска')
    ->dateEarlierThan(new Date('2015-05-12'));

$postRepository->find($query);
Аватара пользователя
slavcodev
Сообщения: 3134
Зарегистрирован: 2009.04.02, 21:42
Откуда: Valencia
Контактная информация:

Re: Specification pattern

Сообщение slavcodev »

zelenin писал(а):Понимаешь, в данной теме мы уже не про теорию, а про практику, в которой в методе как минимум должна быть пагинация.
Не зная бизнес условий, вряд ли можно говорить о точном решении, но если условие будет что в домене нужна пагинация для результата поиска, то будет что-то вроде

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

public function getSegmentOfPostsBySearchString($q, $limit, $offset) : array;
public function getSegmentOfPostsBySearchString($q, $page, $pageSize) : array;
или чуть сложнее

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

public function getPostsPaginationBySearchString($q) : Pagination;
Жду Yii 3!
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

Re: Specification pattern

Сообщение zelenin »

slavcodev писал(а):
zelenin писал(а):Понимаешь, в данной теме мы уже не про теорию, а про практику, в которой в методе как минимум должна быть пагинация.
Не зная бизнес условий, вряд ли можно говорить о точном решении, но если условие будет что в домене нужна пагинация для результата поиска, то будет что-то вроде

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

public function getSegmentOfPostsBySearchString($q, $limit, $offset) : array;
public function getSegmentOfPostsBySearchString($q, $page, $pageSize) : array;
 
или чуть сложнее

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

public function getPostsPaginationBySearchString($q) : Pagination;
 
то есть твое мнение, что QueryObject не нужен. Ок.

Тем не менее QueryObject - это вариант, не нарушающий ddd (если следовать в его составлении UL, а не транслировать sql-понятия) и помогающий избежать многословности в репозитории.
Аватара пользователя
slavcodev
Сообщения: 3134
Зарегистрирован: 2009.04.02, 21:42
Откуда: Valencia
Контактная информация:

Re: Specification pattern

Сообщение slavcodev »

zelenin писал(а):Идея - в передаче в репозиторий QueryObject (на основе UL) и поиска на его основе.
Идея может быть интересной (ну может не Query Builder) это должно называться. Возможно, такая абстракция была бы интересной и удобной.
zelenin писал(а):Выглядеть это может примерно вот так:

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

$query = (new PostQuery)
    ->nameEqual('Новости отпуска')
    ->dateEarlierThan(new Date('2015-05-12'));

$postRepository->find($query);
Но пока вот это не выглядит как что-то что сделано на основе UL.
Не бывает в UL задач типа: "Найти по имени, дате ниже Х, по атрибуту У, по группе атрибутов Z" и тд.
Тут куча деталей, в UL детали не должны появляться.

Максимум может быть например в API фильтр по столбцам, но тогда это один метод в реопзитории "getPostsSpecifiedByFields(array $values)".

Вообщем думаю мои мысли я высказал, но никого не останавливаю решать свои задачи по желанию.
Думаю можно не продолжать спор об этом.
Пища для обдумывания.
Жду Yii 3!
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

Re: Specification pattern

Сообщение zelenin »

slavcodev писал(а):Не бывает в UL задач типа: "Найти по имени, дате ниже Х, по атрибуту У, по группе атрибутов Z" и тд.
а как может выглядеть UL на урле /posts/Title%201 ?
или /posts/?filter=date<=2015-06-01
Аватара пользователя
slavcodev
Сообщения: 3134
Зарегистрирован: 2009.04.02, 21:42
Откуда: Valencia
Контактная информация:

Re: Specification pattern

Сообщение slavcodev »

Если я правильно понял первый, то все это "выбор постов с фильтром по свойствам", это один метод репозитория, который проверит что фильтр верный и может быть использован, подготовит запрос и выберет посты из БД.
Жду Yii 3!
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

Re: Specification pattern

Сообщение zelenin »

slavcodev писал(а):Если я правильно понял первый, то все это "выбор постов с фильтром по свойствам", это один метод репозитория, который проверит что фильтр верный и может быть использован, подготовит запрос и выберет посты из БД.
это мнение программиста, а не UL. Доменный эксперт скажет: тут мы будем видеть пост, полученный по имени.

/posts/?filter=date<=2015-06-01
Доменный эксперт: есть возможность фильтровать посты по дате. Если указано date<=2015-06-01, то выбираем посты с датой публикации ранее указанной даты.

так что это UL как он есть:

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

$query = (new PostQuery)
    ->nameEqual('Новости отпуска')
    ->dateEarlierThan(new Date('2015-05-12'));
выбрать по названию или отфильтровать по дате - это более человеческий язык, чем "выбор постов с фильтром по свойствам".

Тем не менее спорить на тему реализации не хочу - понятна идея: QueryObject должен быть реализован на UL, поскольку это часть доменного слоя.
sda
Сообщения: 334
Зарегистрирован: 2013.12.19, 09:29

Re: Specification pattern

Сообщение sda »

slavcodev склоняюсь к вашему мнению. Оставлю тогда как и было, несколько методов для различных выборок внутри репозитория. После чтения разных источников, это мне представляется наиболее верным. К тому же у Фаулера в книге PoEAA как вы и сказали QueryObject инстранциируется внутри метода репозитория, но не передается как аргумент метода и я не встречал в его книге советов, чтобы так делать.
Аватара пользователя
slavcodev
Сообщения: 3134
Зарегистрирован: 2009.04.02, 21:42
Откуда: Valencia
Контактная информация:

Re: Specification pattern

Сообщение slavcodev »

Если я правильно понял, то что zelenin предложил , не является на самом деле QueryObject, думаю просто название не верное выбрал, какой-нибудь PostSpecification наверное бы меньше путаницы бы внес. Хотя я по прежнему думаю такой билдер выносить за переделы имплементации доменной модели, не очень удобно. Но попробовать можно все, шишки на лбу болят, но с пользой. Сам весь в синяках :)
Жду Yii 3!
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

Re: Specification pattern

Сообщение zelenin »

slavcodev писал(а):Если я правильно понял, то что zelenin предложил , не является на самом деле QueryObject, думаю просто название не верное выбрал, какой-нибудь PostSpecification наверное бы меньше путаницы бы внес.
ну если Specification - это фильтр после запроса в классическом понимании, то нам нужна абстракция над запросом в хранилище. Пусть это будет просто Query/RepositoryQuery/PostRepositoryQuery/PostQuery. Мы составляем Query, передаем его в репозиторий, который является query executor.
slavcodev писал(а):Хотя я по прежнему думаю такой билдер выносить за переделы имплементации доменной модели, не очень удобно.
я такого не предлагал. Это будет в рамках домена. Или ты про создание подобной библиотеки общего применения?
slavcodev писал(а):Но попробовать можно все, шишки на лбу болят, но с пользой. Сам весь в синяках :)
а собственно какие еще могут быть варианты, если есть проблема - абстракция над запросом в хранилище? либо не абстрагироваться и делать на каждый запрос по методу либо абстрагироваться и пользоваться одним методом (это не отменяет возможности добавить дополнительные методы).
Аватара пользователя
slavcodev
Сообщения: 3134
Зарегистрирован: 2009.04.02, 21:42
Откуда: Valencia
Контактная информация:

Re: Specification pattern

Сообщение slavcodev »

zelenin писал(а):Мы составляем Query, передаем его в репозиторий, который является query executor.
Вот это я и называю выход за пределы домена. Если ты query собираешь за пределами репозитория и посылаешь его параметром в него, это за пределами домена.
Жду Yii 3!
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

Re: Specification pattern

Сообщение zelenin »

slavcodev писал(а):
zelenin писал(а):Мы составляем Query, передаем его в репозиторий, который является query executor.
Вот это я и называю выход за пределы домена. Если ты query собираешь за пределами репозитория и посылаешь его параметром в него, это за пределами домена.
логика не ясна. репозиторий, сущность, query - это все домен, и пользуюсь я этим в домене.
sda
Сообщения: 334
Зарегистрирован: 2013.12.19, 09:29

Re: Specification pattern

Сообщение sda »

zelenin подскажи еще по поводу агрегатов. В книгах пишут, что один агрегат может содержать ссылку на другой агрегат. Но как это должно выглядеть на практике, агрегат должен содержать внутри себя id на другой агрегат, так чтоли? Как мне сделать так, чтобы клиент получил данные из двух этих связанных агрегатов.
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

Re: Specification pattern

Сообщение zelenin »

sda писал(а):zelenin подскажи еще по поводу агрегатов. В книгах пишут, что один агрегат может содержать ссылку на другой агрегат. Но как это должно выглядеть на практике, агрегат должен содержать внутри себя id на другой агрегат, так чтоли? Как мне сделать так, чтобы клиент получил данные из двух этих связанных агрегатов.
вряд ли тебе нужен агрегат. скорее всего сущность из другого агрегата.
То есть у тебя есть агрегат User. Есть агрегат Order. User имеет в себе подсущность Order. По ее id ты можешь получить из репозитория агрегат Order.
sda
Сообщения: 334
Зарегистрирован: 2013.12.19, 09:29

Re: Specification pattern

Сообщение sda »

У меня сложнее. Game -> Team -> Player -> User. И в браузер нужно отдать примерно такой json

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

{
    teams: [
        {
            id: 1,
            players: [
                {
                    enemy_id: 3,
                    user: {
                        id: 1
                        login: 'user1',
                    }
                },
                {
                    enemy_id: 4,
                    user: {
                        id: 2,
                        login: 'user2'
                    }
                }
            ]
        },
        {
            id: 2,
            players: [
                {
                    enemy_id: 1,
                    user: {
                        id: 3
                        login: 'user3',
                    }
                },
                {
                    enemy_id: 2,
                    user: {
                        id: 4,
                        login: 'user4'
                    }
                }
            ]
        }
    ]
}
Team и Player это части агрегата Game, но User отдельная сущность. Player хочет обладать информацией из сущности User.
Вот не пойму как это все правильно загружать из базы данных. В Player добавить поле user_id и хранить там иденитификатор и по этому user_id потом загружать сущность User для каждого Player ? То есть что-то такое?

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

class Player {
    private $team_id;
    protected $user_id;
    private $enemy_id;
    
    public function _construct($team_id, $user_id, $enemy_id) {
        $this->team_id = $team_id;
        $this->user_id = $user_id;
        $this->enemy_id = $enemy_id;
    }
    
    public function getEnemyId() {
        return $this->$enemy_id;
    }
}

class PlayerProxy extends Player {
    public function getUser() {
        $this->user = $this->userRepository->findById($this->user_id);
    }
}
 
Так? Тогда меня смущает, что $this->userRepository->findById($this->user_id) будет делать запрос в базу для каждого объекта Player который есть в массиве players объекта Teams, которые есть в массиве teams объекта Game. В примере выше 2 команды по 2 участника = 2 * 2 = 4 запроса типа $this->userRepository->findById($this->user_id). Если команд будет 10 по 100 участников в каждой, то уже получится 1 000 запросов.

Или здесь нужна жадная загрузка вместо lazy load ?
Аватара пользователя
ElisDN
Сообщения: 5845
Зарегистрирован: 2012.10.07, 10:24
Контактная информация:

Re: Specification pattern

Сообщение ElisDN »

sda писал(а):И в браузер нужно отдать примерно такой json
Ну так это для вывода на сайте выборка, а не для логики домена. Отделите домен от выборок. В сущности домена храните только user_id. А для выборок сделайте отдельный репозиторий с нужными полями и JOIN-ами.
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

Re: Specification pattern

Сообщение zelenin »

ElisDN писал(а):
sda писал(а):И в браузер нужно отдать примерно такой json
Ну так это для вывода на сайте выборка, а не для логики домена
у нас все для вывода обычно. Домен дергается после взаимодествия с реквестом извне - заполненная форма, запрос в апи или консоль.
ElisDN писал(а):Отделите домен от выборок
тогда и домен не нужен раз мы им не будем пользоваться.
ElisDN писал(а):В сущности домена храните только user_id. А для выборок сделайте отдельный репозиторий с нужными полями и JOIN-ами.
и все, архитектура сломана, т.к. проект стал разделен на предметную область и отдельный апи, который с предметной областью больше не пересекается.
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

Re: Specification pattern

Сообщение zelenin »

sda писал(а):У меня сложнее. Game -> Team -> Player -> User. И в браузер нужно отдать примерно такой json

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

{
    teams: [
        {
            id: 1,
            players: [
                {
                    enemy_id: 3,
                    user: {
                        id: 1
                        login: 'user1',
                    }
                },
                {
                    enemy_id: 4,
                    user: {
                        id: 2,
                        login: 'user2'
                    }
                }
            ]
        },
        {
            id: 2,
            players: [
                {
                    enemy_id: 1,
                    user: {
                        id: 3
                        login: 'user3',
                    }
                },
                {
                    enemy_id: 2,
                    user: {
                        id: 4,
                        login: 'user4'
                    }
                }
            ]
        }
    ]
}
 
Team и Player это части агрегата Game, но User отдельная сущность. Player хочет обладать информацией из сущности User.
Вот не пойму как это все правильно загружать из базы данных. В Player добавить поле user_id и хранить там иденитификатор и по этому user_id потом загружать сущность User для каждого Player ? То есть что-то такое?

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

class Player {
    private $team_id;
    protected $user_id;
    private $enemy_id;
    
    public function _construct($team_id, $user_id, $enemy_id) {
        $this->team_id = $team_id;
        $this->user_id = $user_id;
        $this->enemy_id = $enemy_id;
    }
    
    public function getEnemyId() {
        return $this->$enemy_id;
    }
}

class PlayerProxy extends Player {
    public function getUser() {
        $this->user = $this->userRepository->findById($this->user_id);
    }
}
Так? Тогда меня смущает, что $this->userRepository->findById($this->user_id) будет делать запрос в базу для каждого объекта Player который есть в массиве players объекта Teams, которые есть в массиве teams объекта Game. В примере выше 2 команды по 2 участника = 2 * 2 = 4 запроса типа $this->userRepository->findById($this->user_id). Если команд будет 10 по 100 участников в каждой, то уже получится 1 000 запросов.

Или здесь нужна жадная загрузка вместо lazy load ?
мне кажется (я не знаю суть проекта), вам надо выделить три модуля: GameModule с доменом, отвечающим за игры, AppModule, в контексте которого у User есть принадлежащие ему Game из GameModule, и ApiModule, который в частности отдает данные, которые берет из двух репозиториев двух других модулей, трансформируя полученные сущности в свой респонс. То есть это все разные домены.
Ответить