Pull to refresh
77
0
Vladimir @vkhorikov

User

Send message

На русском книга тоже есть :) https://www.ozon.ru/product/printsipy-yunit-testirovaniya-horikov-vladimir-211424826

Переводил сам.

Валидация в одном месте — application services layer. Но она либо делегируется доменным объектам (через вызов их CanDo методов), либо нет (к примеру проверка имейла юзера на уникальность идет напрямую к базе или к репозиторию).
Хорошая статья. Пара комментариев/дополнений с моей стороны:

1. Очень хороший поинт насчет того, что с конвейерами подход с резалтами не работает — слишком много нагромождений (at least in C#), и это явно не то как авторы asp.net видели для себя обработку ошибок из middleware. Поэтому сценарий с декораторами — исключение из общего правила. При этом единственное, я не встречал других сценариев где такая работа с exceptions была бы оправдана.

2. По поводу типов исключений. Я не думаю что стоит создавать более 1 кастомного исключения (если только это не special case scenario as in #1 above). В статье говорится о преоразовании ItemNotFoundException в 404. Это как раз классический случай, где надо преобразовывать отсутствие объекта (Single/First) в return value и работать дальше уже с ним.

К примеру, может быть use case когда запись по данному Id обязана находиться в базе и если ее нет — это является исключительной ситуацией. В других кейсах это может не быть исключительной ситуацией. И если всегда бросать ItemNotFound, то нельзя будет отличить исключительную ситуацию (500) от не-исключительной (404). Здесь немного более подробно на эту тему: enterprisecraftsmanship.com/2017/01/31/rest-api-response-codes-400-vs-500

Рекомендую всегда возвращать Maybe из репозиториев/гейтвеев и потом уже решать кидать исключение или возвращать 404.

3.
Но не менее справедливо, что в императивных языках (к которым относится C#) повсеместное использование Result приводит к плохо читаемому коду, засыпанному конструкциями языка настолько, что с трудом можно разглядеть исходный сценарий.
Код получается более verbose, да, но он при этом становится наоборот, более читаемым благодаря явной логике ветвений. Опять же, по аналогии с goto: можно переписать метод с кучей if-ов на использование goto и тогда код будет плоский, без indentations. Только читать его станет намного сложнее.

4. И еще один отличный поинт про Application Service, валидацию, и инкапсуляцию в доменный слой. Я обычно делегирую все (возможнные) проверки слою домена через паттерн Do/CanDo. Получается примерно так:

class DomainClass
{
  public Result CanTransfer()
  {
     return Result.Ok();
  }
  
  public void Transfer()
  {
    Guard.Require(CanTransfer().IsSuccess); // кидает исключение в случае false
    
    /* ... */
  }
}
State machine-ы делать на F# — одно удовольствие. Рекомендую этот курс Марка Симана если еще не смотрели: app.pluralsight.com/library/courses/fsharp-type-driven-development/table-of-contents
Да, но не обязательно отдельными методами, property setters тоже подойдут.
Если «набросать структуру», то очень легко ошибиться
Стурктура как раз-таки должна быть легковесной (отсюда слово «набросать»), чтобы ее можно было легко менять. Итерировать дизайн нужно в любом случае, разница в том, есть ли у вас при этом тесты и если есть — насколько хрупкие они.
Тесты с моками далеко не всегда оказываются такими уж хрупкими
Тесты с моками не хрупки только когда они заменяют собой external systems (bus, БД и т.д). Если они мочат внутренности доменной модели — они завязываются на детали имплементации и значит становятся хрупкими. Сторонники mockist подхода (как минимум те, кого я встречал, включая авторов GOOS книги) не делают такого разделения и как правило мочат всё подряд — и внешние системы и внутренности самой доменной модели.
Классика — это всегда bottom-up, Кент Бек и ко не писали про моки в оригинале, лондонская школа выработалась позже. Сочетать то, что описано в книге с классикой кстати возможно, но не так как вы описали (и это соответственно не будет полноценным top-down). Можно начать с набросков доменной модели (без тестов), затем после того как структура более-менее понятна — написать первый end-to-end тест и прокладывать себе путь к его исполнению путем классического bottom-up. Получится эдакий двух-уровневый TDD (как описано в книге), но без моков и без преждевременного распределения ответственностей. Проблема в top-down подходе в том, что если вы неверно выделелили эти ответственности, то отрефакторить их довольно сложно, т.к. из-за моков тесты становятся завязаны на детали имплементации.

вы не против разработки с моками? Вы просто призываете их удалить после того, как «реализация готова»?
Это я к тому, что если вы большой приверженец подхода сверзу-вниз, то это тоже не повод оставлять моки в конечной имплементации.
Чем руководствовались авторы сказать трудно. В книге кстати заметно как они испытывают трудности с циклами, т.к. возникают проблемы при «собирании» всех взаимодействующих классов воедино в composition root.

возможно, компоненты действительно должны взаимодействовать «в обе стороны», а вы просто прячете это за возможностями какого-нибудь фреймворка
Как я уже упомянул, код проекта довольно несложен, никаких фреймворков за исключением UI не используется. Должны или нет взаимодействовать в обе стороны — на мой вгляд неверная постановка вопроса. Нужно смотреть на то, можно ли сделать так, чтобы они работали только в одну сторону. Если можно — значит так и нужно делать.
Действительно ли ваша система проще? Мне пока вы не смогли это продемонстрировать.
Мне кажется, это довольно легко увидеть глядя на диаграммы + код. В альтернативной версии в два раза меньше классов и интерфейсов (при этом код внутри самих классов либо такой же по размеру, либо меньше), плюс он не имеет недостатков, описанных в статье, таких как наличие циклических зависимостей.

Фукнциональность проекта идентична оригиналу, как по части самого кода так и по части тестов его покрывающих, фреймворки (кроме UI) не используются.

Это моки заставили авторов наделать столько классов (что сомнительно) или (что более вероятно) они принимали во внимание какие-то аспекты, которые вы по каким-то причинам откинули?
Опять же, это легко проверить посмотрев на код.
Вопрос по больше части сводится к top-down vs bottom-up подходу к разработке. Mockist подход действительно помогает при top-down, т.к. позволяет «мочить» несущественные детали. Я бы не сказал, что один из подходов позволяет решать задачи проектирования лучше другого. Лично я больше тяготею к классическому bottom-up, но также понимаю людей, которые предпочитают top-down.

При разработке top-down без моков действительно никак. Но при этом эти моки не обязательно оставлять после того как реализация готова. Такие тесты можно отрефакторить и заменить тестами без моков, что я собственно и сделал в статье.
Причем здесь слой доступа к данным вообще?
>Как это нет, когда по вашей ссылке:
Если вы про это: " preventing unauthorized parties' direct", то это о сокрытии информации, а не о соблюдении инвариантов.

>что по вашей логике есть нарушение инкапсуляции
Да, верно, внутри GetAdminAddress — такие же правила что и в DoSomething()

>Хотя геттер — тоже метод, но он даёт мне возможность выбирать любые варианты
Опять же — тут дело в наличии бизнес логики и в том, где она находится

>Потом, я захотел найти не-администратора с зарплатой до 100 рублей и фамилией Нафтазаров, и выяснить его стаж, а не адрес. Ещё один метод писать?
Да, либо еще один метод, либо параметр в существующий, в зависимости от ситуации
Дело не в технических отличиях метода и проперти, дело в наличии бизнес-логики и в том, где она хранится.
Нет, инкапсуляция — это объединение данных с логикой + сокрытие информации от клиентов: en.wikipedia.org/wiki/Encapsulation_(computer_programming). Защита внутреннего состояние объекта — это соблюдение инвариантов и больше в сторону контрактного программирования.

>Правильно?
Поясните вопрос, не уверен что понял.

>(1)Если у клиента Customer открыты Employees, что нам мешает их опрашивать?
Ничего до тех пор пока логика опроса не базируется полностью на данных из Customer. К примеру опрос с целью сохранения части кастомеров во внешней коллекции по каким-то признакам — не является нарушением инкапсуляции. Пример выше:
var boss = customer.Employees.Max(x => x.Salary).FirstOrDefault();
— является

>(2)Что, если класс Customer писали не мы, а другая компания?
Если доступа к классу нет, то тут уж ничего не поделаешь. Это тем не менее также будет или не будет являться нарушением в зависимости от (1)

>Что, если надо будет получить не адрес, а телефон? Не у админа, а у Семён Семёныча?
Нужно больше данных, непонятно что конкретно вы имеете ввиду
Потому что оно находится внутри класса Customer.

Разверну ответ. Для многих подобный код считается нормой:
var boss = customer.Employees.Max(x => x.Salary).FirstOrDefault();


Тем не менее, это также является нарушением инкапсуляции, т.к. здесь логика по определению «босса» отвязана от данных, с которыми эта логика работает. Правильным с т.з. принципов инкапсуляции решением будет вынести эту логику в класс Customer.
>Здесь уже спрашивали, но я спрошу ещё раз, раз речь про инкапсуляцию: как она нарушается?
Инкапсуляция нарушается тем, что метод DoSomething делает суждения полностью базируясь на внутренностях класса Customer. Это по сути определение инкапсуляции: если данные полностью принадлежат одному классу, то методы по работе с этими данными должны также быть в этом классе. Второй пример показывает восстановление инкапсуляции — логика по получению адреса админа перенесена в Customer.
> устраняя необходимость в проверках на нал
Имеется ввиду что второй случай — это и инкапсуляция, и отсутвие проверок на null в самом методе, первый случай — только отсуствие проверок на null.

По поводу того, что внутри GetAdminAddress будет такие примерно такой же код:
return Employees
        ?.SingleOrDefault(x => x.IsAdmin)?.Address?.ToString();

В данном случае речь не о том, что оператор нельзя использовать (использовать его можно), а о том, что он скрывает проблемы в дизайне первоначального метода. Т.е. первый случай его использования нарушает принципы инкапсуляции, второй — нет, т.к. Customer обращается ко своим внутренним членам.
Самый первый вариант — отойти от CQS в данном конкретном случае и таки вернуть объект вместе с результатом выполнения команды.
Второй вариант — как предложили выше — генерировать Ids на стороне клиента. В таком случае клиент сможет делать запросы по этому Id с использованием queries

Information

Rating
Does not participate
Works in
Registered
Activity