как Разбивать базу проекта на Domain и Application Logic

grigori

( ͡° ͜ʖ ͡°)
Команда форума
Рядом сидит мастер Вова. (Чаще Дима, но неважно). 5 лет назад он закончил курсы по PHP, и взял кредит на машину. Он увидел модель User! У модели есть метод get(). Чтобы вывести список всех пользователей, можно просто взять список пользователей, и написать в цикле $U = User::find($user_id);
Отлично! Задача сделана, тестировщик принял.
Через год начинаются проблемы. Выводить одного юзера можно, выводить всех по циклу - нельзя.

Потом решают, что у пользователя может быть несколько email-адресов. Метод create() ждет один адрес, и вызывается в 10 местах. Метод get возвращал одномерный массив, и вызывается в 50 местах. А теперь вместо массива надо использовать структуру. Реализация занимает месяц.

CRUD для вывода списка в админке - нормально. Для сложной логики - нет.
 
Последнее редактирование:

WMix

герр M:)ller
Партнер клуба
ActiveRecord это когда entity имеет ссылку на connection когда она сама себя записывает
rest это просто стиль http запросов недодуманый graphql
rest+crud имеет смысл жить, типо быстро накидать
проблема только в том что логика в контроллере, те в эту секунду смешалось все, логика, http, база данных, люди, кони. это как лапша из 4х языков
 

WMix

герр M:)ller
Партнер клуба
это да но в ресте все видят крад, такое ограниченое представление домена (достал, показал, изменил, удалил) отсюда и проблемы, те нагенерить админку на коленке не вдаваясь в суть вопроса. таких пхпмайадминов нафигачат и доказывают что дешево и решает все вопросы
 

Вурдалак

Продвинутый новичок
Это достаточно странное заблуждение, что для админки подходит CRUD. К примеру, @Adelf как-то рассказывал про случай. Я тоже на своей практике знаю кучу кейсов в админке, где это серьёзно вредило и мы вынуждены были делать серьёзный рефакторинг. Админка так или иначе взаимодействует с теми же сущностями, что и пользователь, поэтому странно полагать, что для пользователя мы должны чётко выделять команды/события, а для админов — нет. Ну да, бывают вещи, которые нужны чисто для внутреннего пользования, но это выходит за пределы нашего интересного разговора. Когда задача просто наговнокодить, ну уж я как-нибудь справлюсь, это мы все умеем. :)

Отмечу, что REST — это не только CRUD, там вполне могут быть кастомные ресурсы, см., например, https://en.wikipedia.org/wiki/HATEOAS:
Код:
<account>
   <account_number>12345</account_number>
   <balance currency="usd">100.00</balance>
   <link rel="deposit" href="https://bank.example.com/accounts/12345/deposit" />
   <link rel="withdraw" href="https://bank.example.com/accounts/12345/withdraw" />
   <link rel="transfer" href="https://bank.example.com/accounts/12345/transfer" />
   <link rel="close" href="https://bank.example.com/accounts/12345/close" />
</account>
Другой вопрос, что в моих глазах REST действительно ассоциируется с CRUD, мы это уже обсуждали. Я по возможности топлю за честный CQRS-RPC API. Единственное, тут может быть проблема с количеством запросов: ну не хочу я в большинстве случаев возвращать что-то из методов-команд (речь про внешний API), особенно когда видно, что возвращаемые данные — исключительно из-за требований UI. Если я не ошибаюсь, то это как-то решено в GraphQL (до сих пор его толком не изучил), где можно (вроде бы) строить цепочки из мутаций и query-запросов (т.е. в рамках одного HTTP-запроса выполнять все необходимые действия: например, что-то получить из query, подставить в мутацию, сделать ещё один query для получения результата). Это очень круто.
 

fixxxer

К.О.
Партнер клуба
Я по возможности топлю за честный CQRS-RPC API. Единственное, тут может быть проблема с количеством запросов: ну не хочу я в большинстве случаев возвращать что-то из методов-команд (речь про внешний API), особенно когда видно, что возвращаемые данные — исключительно из-за требований UI. Если я не ошибаюсь, то это как-то решено в GraphQL (до сих пор его толком не изучил), где можно (вроде бы) строить цепочки из мутаций и query-запросов (т.е. в рамках одного HTTP-запроса выполнять все необходимые действия: например, что-то получить из query, подставить в мутацию, сделать ещё один query для получения результата). Это очень круто.
Я эту проблему, помнится, решал безо всяких GraphQL небольшой модификацией батчей в jsonrpc2, добавив туда возможности ссылаться в параметрах на результаты другого элемента батча (по ID) и псевдокоманды типа Batch.match/Batch.repeat/Batch.filter.

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

WMix

герр M:)ller
Партнер клуба
Это достаточно странное заблуждение, что для админки подходит CRUD
конечно не подходят, но есть куча ненужных vo к примеру производитель, группа товаров и тд, когда табличка это id+name, там можно быстренько нагенерить чистый crud. других действий и не будет.
а сам по себе rest он вполне себе неплохо вписывается в домен, по старым примерам
Код:
post /user/42/rename
name=Vasia
другое дело он ограничен, если это не id а некий поисковый запрос (чел который в 42 родился, фамилия на П, с родинкой на правом полупопии)
тут язык нужен и graphql это (описание) решает
 

Вурдалак

Продвинутый новичок
Batch.match/Batch.repeat/Batch.filter
Что они делают? Например, у меня есть запрос на получение последних поисковых настроек и stateless-метод поиска:
Код:
{"jsonrpc": "2.0", "method": "ui.search.get_last_criteria", "id": 1}
{"jsonrpc": "2.0", "method": "business.search.search", "params": {"name": "Foo"}, "id": 2}
Как будет выглядеть запрос «возьми из last_criteria name и передай в business.search.search»? Тут как-то без уродства не получится.
 

fixxxer

К.О.
Партнер клуба
А, забыл самое важное, давно это было :) Если ключ в params начинается с $, тогда эвалится селектор.
Код:
{"jsonrpc": "2.0", "method": "business.search.search", "params": {"$name": "1.path.to.name"}, "id": 2}
 
Последнее редактирование:

ivanov77

Новичок
Тогда просто начни с написания кода, который легко тестировать. По TDD.
Только это должен быть честный юнит-тест, а не что-то, что инициализирует половину фреймворка и требует доступ к базе данных.
Чистый юнит тест для реалий Yii будет непросто сделать, поэтому таких радикальных изменений пока не планируется. Codeception сам подгружает yii2, везде доступно Yii::$app, а AR модели жестко запрашивают базу, даже для получения имен столбцов. Но смотрю вы тут уже о своем говорите, а мой вопрос не заинтересовал, @Adelf молчит
 

Вурдалак

Продвинутый новичок
Но смотрю вы тут уже о своем говорите, а мой вопрос не заинтересовал, @Adelf молчит
У тебя слишком много вопросов за раз, некоторые слишком абстрактные («где какие объекты создаваться должны»), просто лень отвечать. Если вкратце говорить про service layer, то:
1. Это просто внутренний API твоего приложения, который не завязан на конкретный UI.
2. web controller — это UI. Формы, сессии — это тоже UI. Чтобы представить что должно быть в контроллере, а что в SL, нужно представить как бы ты мог написать то же бизнес-действие в консольной команде, например.
3. Валидация может быть и в формах web controller'а и внутри SL одновременно. Она может дублироваться. Различные security проверки на права могут быть как в контроллере, так и в специальном сервисе, который делегирует выполнение service layer (это может быть нужно, если, например, есть старые пользовательские контроллеры и точно такие же API-методы для пользовательского API и нужно переиспользование).
4. Service layer должен быть максимально stateless: никаких Yii::$user, сессий, и прочего «текущего состояния». Если ты захочешь, например, в админке или в консольной команде от имени пользователя отправить сообщение, то должно быть достаточно указать userId. Как ты понимаешь, Yii::$user где-то внутри бизнес-логики будет этому сильно мешать, т.к. его либо не будет, либо он может указывать на другого пользователя (на админа, например).
 

fixxxer

К.О.
Партнер клуба
Чистый юнит тест для реалий Yii будет непросто сделать, поэтому таких радикальных изменений пока не планируется. Codeception сам подгружает yii2, везде доступно Yii::$app, а AR модели жестко запрашивают базу, даже для получения имен столбцов. Но смотрю вы тут уже о своем говорите, а мой вопрос не заинтересовал, @Adelf молчит
Я не знаю про устройство Yii AR, но если там нельзя даже сделать new User() без обращения к БД, то просто выкинь это говно. Но я подозреваю, что не все так плохо.

А вообще AR вполне можно тестировать, никто же не заставляет вызывать методы типа save(), нам же методы модели надо протестировать, а не методы AR.
 

Вурдалак

Продвинутый новичок
Хотел я было тут описать что я бы сделал если бы мне пришлось работать в проекте с Yii2 (ушёл бы из проекта), но тут внезапно понял, что похоже в Yii даже нет поддержки SELECT .. FOR UPDATE: https://github.com/yiisoft/yii2/issues/11730

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

Вурдалак

Продвинутый новичок
Нашёл костыль через findBySql.

В общем, я бы скорее всего работал с ActiveRecord Yii2 как-то так:
  • отказался бы от изменений свойств снаружи (вероятно, пришлось бы перекрыть __set());
  • добавил бы транзакционности;
  • записывал бы события внутри сущности;
  • отказался бы от различных валидаторов от Yii внутри сущности (http://www.yiiframework.com/doc-2.0/yii-base-model.html#rules()-detail), использовал бы вместо этого https://github.com/beberlei/assert + валидировал бы формы;
  • объяснил бы коллегам, почему Yii нельзя использовать и либо поменял бы проект, либо поменял бы фреймворк.

PHP:
public function handle(RenameVkUser $command)
{
    $now = ...; // new DateTime()

    VkUser::getDb()->transaction(
        function() use ($command, $now) {
            $vkUser = VkUser::findWithLock($command->userId);
            $vkUser->rename($command->newName, $now);
            $vkUser->save();
        }
    );
}

// ...

final class VkUser extends ActiveRecord
{
    // ...

    public static function findWithLock(): VkUser
    {
        $sql = 'SELECT ... FROM ' . self::tableName() . ' WHERE id = ... FOR UPDATE';
        return self::findBySql($sql)->one(); // or throw VkUserWasNotFound exception
    }

    public function __set($name, $value)
    {
        throw new \LogicException('You shall not pass');
    }

    public function rename(string $newName, DateTime $now): void
    {
        Assert::notEmpty($newName);

        if (
            $now->toTimestamp() - $this->lastTimeChanged < 86400
            && $this->registeredAt - $now->toTimestamp() > 3600
        ) {
            $this->record(new VkUserNameSentForModeration($this->id, $newName));
        } else {
            $this->name = $newName;

            $this->record(new VkUserChangedName($this->id, $newName));
        }
    }

    public function afterSave($insert, $changedAttributes)
    {
        parent::afterSave($insert, $changedAttributes);
        EventDispatcher::getInstance()->dispatch($this->releaseEvents());
    }
}
— вот теперь метод rename() можно покрыть unit-тестами, тут доступ к БД не нужен, достаточно проверять, что нужные события происходят в зависимости от. Если же там нет логики, то и тестировать unit-тестами нечего.

P.S. Хотя я вот понял, что наверное afterSave() тоже будет вызываться внутри транзакции и нужно сильно постараться, чтобы это исправить. Ещё одна причина не связываться с этим фреймворком.
 

ivanov77

Новичок
Я не знаю про устройство Yii AR, но если там нельзя даже сделать new User() без обращения к БД, то просто выкинь это говно. Но я подозреваю, что не все так плохо.
При new User() обращения к базе не будет, но вот при любом обращении к св-ву $model->property уже будет.
Не знаю как в других ORM, но в yii не надо указывать имена свойств в классе, а только имя таблицы, пример класса , и имена св-в достаются автоматом из схемы таблицы в БД.
 

ivanov77

Новичок
@Вурдалак, спасибо за развернутый ответ, уже какое то правило.
Но момент, вы говорите:
2. web controller — это UI. Формы, сессии — это тоже UI. Чтобы представить что должно быть в контроллере, а что в SL, нужно представить как бы ты мог написать то же бизнес-действие в консольной команде, например.
Почему web-controller и сессии - это UI?
Вот читаю у Крэга Лармана(Применение UML 2.0 и шаблонов проектирования) про слои, он это все относит к какому то уровню приложения, насколько я из дальнейшего понял это C в MVC (красным надписи я добавил):

Т.е. функция web-controllera будет получить данные от V, определить что с ними делать и передать обработку уже на M (сервисы и модели). Жаль все это терминологически путано, вот тут человек по сути тоже о об этих Сервисах говорит, но называет он их "Сервисы уровня приложения", хотя они не с этого(как на картинке) уровня приложения получается а с уровня бизнес логики приложения (M)
 

ivanov77

Новичок
Нашёл костыль через findBySql.

В общем, я бы скорее всего работал с ActiveRecord Yii2 как-то так:
  • отказался бы от изменений свойств снаружи (вероятно, пришлось бы перекрыть __set());
Да ну ладно, что это за AR тогда будет.
Первый пример в вики о AR говорит что это и как используется, поменяешь эту основу, тогда уже и переименовывать надо для ubiquitous language :)
 

Вурдалак

Продвинутый новичок
Почему web-controller и сессии - это UI?
Вот читаю у Крэга Лармана(Применение UML 2.0 и шаблонов проектирования) про слои, он это все относит к какому то уровню приложения, насколько я из дальнейшего понял это C в MVC (красным надписи я добавил)
<...>
Т.е. функция web-controllera будет получить данные от V, определить что с ними делать и передать обработку уже на M (сервисы и модели). Жаль все это терминологически путано, вот тут человек по сути тоже о об этих Сервисах говорит, но называет он их "Сервисы уровня приложения", хотя они не с этого(как на картинке) уровня приложения получается а с уровня бизнес логики приложения (M)
MVC — это UI pattern, там есть, соответственно, смещение акцентов в сторону UI. Примерное соотношение такое:
Код:
┌─────────────────┐      ┌─────────────────┐   ┌──────────────┐
│                 │      │                 │   │              │
│                 │  ┌──>│  Domain layer   │<┐ │              │
│                 │  │   │                 │ │ │              │
│      Model      │──┤   └─────────────────┘ │ │              │
│                 │  │   ┌─────────────────┐ │ │              │
│                 │  │   │   Application   │ │ │              │
│                 │  └──>│ (Service) layer │<┼─│Infrastructure│
└─────────────────┘      └─────────────────┘ │ │              │
┌─────────────────┐      ┌─────────────────┐ │ │              │
│      View       │──┐   │                 │ │ │              │
└─────────────────┘  │   │  Presentation   │ │ │              │
┌─────────────────┐  ├──>│   layer (UI)    │<┘ │              │
│   Controller    │──┘   │                 │   │              │
└─────────────────┘      └─────────────────┘   └──────────────┘
В MVC одним словом «модель» называют всю основную логику приложения — application/service layer и domain. В многослойной архитектуре одним словом «presentation» (aka «UI») называют view и controller.

Популяризация термина MVC привела к тому, что из-за каких-то когнитивных искажений программисты считают центром архитектуры HTTP-контроллеры. Контроллеры/view — это UI stuff. Центром приложения должна быть модель, а не презренный UI. Я стараюсь вообще не использовать термин MVC, потому что я вырос из того возраста, когда контроллеры и view казались верхом архитектуры. Нужно расставлять приоритеты правильно.
 
Последнее редактирование:
Сверху