Введение
В рамках предыдущих статей мы описали: область применения, методологические основы, пример архитектуры и структуры. В данной статье, я хотел бы рассказать как описывать процессы, о принципах сбора требований, чем отличаются бизнес требования от функциональных, как перейти от требований — к коду. Рассказать о принципах применения Вариантов Использования (Use Case) и как они нам могут помочь. Разобрать на примерах варианты реализации шаблонов проектирования Interactor и Service Layer.

Примеры приведенные в статье даны с использованием нашего решения LunaPark, оно поможет вам с первыми шагами в описанных подходах.
Отделяем функциональные требования от бизнес требований.
Снова и снова случается так, что многие бизнес-идеи на самом деле не превращаются в конечный, намеченный продукт. Зачастую это происходит из-за неспособности понять разницу между бизнес-требованиями и функциональными требованиями, что в конечном итоге, приводит к несоответствующему сбору требований, ненужной документации, задержкам проекта и крупным проектным сбоям.
Или иногда мы сталкиваемся с ситуациями, в которых, хотя окончательное решение отвечает потребностям клиентов, но каким-то образом бизнес-цели не достигаются.
Поэтому крайне важно разделить бизнес-требования и функциональные требования, до того момента, как вы начнете их определять. Давайте разберем пример.
Предположим, мы пишем приложение для компании по доставке пиццы, и мы решили сделать систему по отслеживанию курьеров. Бизнес требования звучат следующим образом:
"Внедрить веб-систему и систему отслеживания сотрудников на базе мобильных устройств, которая фиксирует курьеров на их маршрутах и повышает эффективность за счет мониторинга активности курьеров, их отсутствия на работе и производительности труда."
Тут можно выделить ряд характерных признаков, которые будут указывать, что это требования от бизнеса:
- бизнес-требования всегда написаны с точки зрения клиента;
- это широкие требования высокого уровня, но все же ориентированные на детали;
- они не являются целями компании, но помогают компании достичь целей;
- отвечают на вопросы «почему» и «что». Что хочет компания получить? И почему ей это нужно.
Функциональные требования — это Действия, которые система должна выполнить, для реализации бизнес-требований. Таким образом, функциональные требования связаны с разрабатываемым решением или программным обеспечением. Сформулируем функциональные требования для вышеуказанного примера:
- система должна отображать долготу и широту сотрудника через GPS/ГЛОНАСС;
- система должна отображать позиции сотрудников на карте;
- система должна позволять менеджерам отправлять уведомления своим подчиненным на местах.
Выделим следующие особенности:
- функциональные требования всегда пишутся с точки зрения системы;
- они более конкретные и подробные;
- именно благодаря выполнению функциональных требований, разрабатывается, эффективное решение, отвечающее потребностям бизнеса и целям клиента;
- отвечают на вопрос «как». Как система решает бизнес требования.
Следует сказать пару слов о нефункциональных требованиях (также известных как «требования к качеству»), которые накладывают ограничения на дизайн или реализацию (например, требования к производительности, безопасности, доступности, надежности). Такие требования отвечают на вопрос «какой» должна быть система.
Разработка — это перевод бизнес требований в функциональные. Прикладное программирование — это реализация функциональных требований, а системное — нефункциональных.
Варианты использования (Use cases)
Реализация функциональных требований является, зачастую, самой сложной в коммерческих системах. В чистой архитектуре функциональные требования реализуются через слой Use Case.
Но для начала, я хочу обратится к первоисточнику. Ивар Якобсон — автор определения Use Case, один из авторов UML, и методологии RUP, в своей статье Use-Case 2.0 The Hub of Software Development выделяет 6 принципов применения Вариантов использования:
- сделайте их простыми через повествование;
- имейте стратегический план, осознайте картину целиком;
- сфокусируйтесь на значении;
- выстраивайте систему по слоям;
- поставляйте систему пошагово;
- удовлетворяйте потребности команды.
Кратко рассмотрим каждый из этих принципов, нам они пригодятся для дальнейшего понимания. Ниже идет мой вольный перевод, с сокращениями и вставками, настоятельно рекомендую ознакомиться и с оригиналом.
Простота через повествование
Повествование — часть нашей культуры; это самый простой и эффективный способ передачи знаний, информации одного человека — другому. Это лучший способ сообщить о том, что должна делать система, и помочь команде сосредоточиться на общих целях.
Варианты использования отражают цели системы. Чтобы понять Вариант Ис��ользования, мы рассказываем, повествуем некую историю. История рассказывает о том, как достичь цели и как решить проблемы, возникающие на пути. Варианты использования, как сборник рассказов, предоставляют способ идентифицировать и охватить все разные, но связанные истории простым, всеобъемлющим способом. Это позволяет легко собирать, распространять и понимать требования системы.
Данный принцип коррелирует с партерном Общий язык (Ubiques language) из DDD подхода.
Понимание картины в целом
Независимо от того, какую систему вы разрабатываете, большую, маленькую, программную, аппаратную или бизнес-систему, понимание общей картины очень важно. Без понимания системы в целом вы не сможете принимать правильные решения о том, что включать в систему, что исключать, сколько это будет стоить и какую пользу это принесет.
Ивар Якобсон предлагает задействовать диаграмму вариантов использования, что очень удобно для сбора требований. Если требования собраны и ясны, то лучшим вариантом будет Контекстная карта (Context map) Эрика Эванса. Зачастую, Scrum подход интерпретируют так, что люди не тратят время на стратегический план, считая планирование, дальше чем на две недели, пережитком прошлого. Пропаганда Джеффа Сазерленда обрушилась на waterflow, а люди закончившие двухнедельные курсы подготовки скрам-мастеров, допущенные к управлению проектами, сделали свое дело. Но здравый смысл, осознает важность стратегического планирования. Не нужно делать детальный стратегический план, но он должен быть.
Фокус на значении
Пытаясь понять, как будет использоваться система, всегда важно сосредоточиться на ценности, которую она предоставит своим пользователям и другим заинтересованным сторонам. Ценность формируется только в том случае, когда система используется. Поэтому гораздо лучше сосредоточиться на том, как система будет применяться, чем на длинных списках функций или возможностей, которые она может предложить.
Варианты использования обеспечивают этот фокус, помогая вам сконцентрироваться на том, как система будет задействована конкретным пользователем для достижения его цели. Варианты использования охватывают множество способов применения системы: те, которые успешно достигают целей, и те, которые решают любые возникающие сложности.
Далее автор приводит замечательную схему, на которую следует обратить самое пристальное внимание:

На схеме показан вариант использования, «Снятие наличных в банкомате». Самый простой способ достижения цели описывается в Основном Направлении (Basic flow). Другие случаи описываются как Альтернативные Направления (alternative flow). Эти направления помогают с повествованием, структурируют систему и помогают с написанием тестов.
Послойное построение
Большинство систем требуют большой работы, прежде чем они будут готовы к использованию. У них много требований, большинство из которых зависят от других требований, они должны быть реализованы, прежде чем требования будут выполнены и оценены.
Большая ошибка создать такую систему за раз за один раз. Система должна быть построена из кусочков, каждый из которых имеет четкую ценность для пользователей.
Эти идеи перекликаются с подходами гибкой разработки и с идеями Доменов (Domain).
Пошаговый вывод продукта на рынок
Большинство программных систем развиваются на протяжении многих поколений. Они не производятся за один раз; они построены в виде серии выпусков, каждое из которых построена на предыдущем выпуске. Даже сами релизы часто не выходят за раз, а развиваются через серию промежуточных версий. Каждый шаг предоставляет наглядную, пригодную для использования версию системы. Это тот способ, которым должны быть созданы все системы.
Удовлетворять потребности команды
К сожалению, не существует универсального решения проблем разработки программного обеспечения; разные команды и разные ситуации требуют разных стилей и разных уровней детализации. Независимо от того, какие методы и приемы вы выберете, вы должны убедиться, что они достаточно адаптируемые для удовлетворения текущих потребностей команды.
Эрик Эванс в своей книге призывает не тратить много времени на описания всех процессов через UML. Достаточно использовать любые наглядные схемы. Разным командам, разным проектам требуется разная степень детализации, об это говорит и сам автор UML.
Реализация
В чистой архитектуре Робертом Мартином дается следующее определение Вариантов использования :
These use cases orchestrate the flow of data to and from the entities, and direct those entities to use their Critical Business Rules to achieve the goals of the use case.
Попробуем воплотить эти идеи в код. Давайте вспомним схему из третьего принципа применения Вариантов использования и возьмем ее за основу. Рассмотрим действительно сложный бизнес-процесс: «Приготовление пирога с капустой».
Давайте попробуем его декомпозировать:
- проверить наличие продуктов;
- взять их со склада;
- замесить тесто;
- дать тесту подняться;
- подготовить начинку;
- сделать пирог;
- испечь пирог.
Всю эту последовательность мы реализуем через Интерактор (Interactor), а каждый шаг будет реализован через функцию или Функциональный объект (Functional Object) на Сервисном слое (Service Layer).
Последовательность действий (Interactor)

Я очень рекомендую начинать разработку сложного бизнес-процесса именно с Последовательности действий. Точнее не так, вы должны определить Доменную область, к которой относится бизнес-процесс. Уточнить все требования бизнеса. Определить все Сущности, которые задействованы в процессе. Задокументировать требования и определения каждой Сущности в базе знаний.
Расписать все на бумаге по шагам. Иногда потребуется Диаграмма последовательности (sequence diagram). Ее автор тот же, кто придумал Варианты использования (Use Case) — Ивар Якобсон. Диаграмма была придумана им, когда он разрабатывал систему обслуживания телефонных сетей для компании Эриксон, взяв за основу схему реле. Мне очень нравится эта диаграмма, и термин Sequence, на мой взгляд более выразителен, чем термин Interactor. Но ввиду большей распространенности последнего, будем использовать привычный термин — Interactor.
Небольшая подсказка, когда вы описывайте бизнес-процесс хорошим подспорьем для вас, может стать, основное правило документооборота: «В результате любой хозяйственной деятельности, должен быть составлен документ». К примеру, мы разрабатываем систему скидок. Предоставляя скидку, мы по факту, с точки зрения бизнеса, заключаем договор между компанией и клиентом. В этом договоре должны быть прописаны все условия. То есть в домене DiscountSystem, у вас будет Entites::Contract. Не привязывайте скидку к клиенту, а создайте Сущность Контракт, где описываются правила ее предоставления.
Вернемся к описанию нашего бизнес-процесса, после того, как он стал прозрачен для всех лиц задействованных в его разработке, и все ваши знания зафиксированы. Я рекомендую начать написание кода именно с Последовательности действий.
Шаблон проектирования Последовательности действий отвечает за:
- последовательность выполнения Действий;
- координацию передаваемых данных между Действиями;
- обработку ошибок совершаемых Действиями во время их выполнения;
- возвращение результата совокупности совершенных Действий;
- ВАЖНО: самая главная ответственность этого шаблона проектирования — реализация бизнес логики.
На последней ответственности хотелось бы остановиться подробнее, если у нас имеется какой-то сложный процесс — мы должны описать его так, чтобы было понятно, что происходит не вдаваясь в технические детали. Вы должны описать его настолько выразительно, насколько это позволяет вам ваши навыки программирования. Доверьте этот класс самому опытному члену вашей команды.
Вернемся к пирогу: попробуем описать процесс его приготовления через Interactor.
Реализация
Привожу пример реализации, с нашим решением LunaPark, которое мы представили в предыдущей статье.
module Kitchen module Sequences class CookingPieWithСabbage < LunaPark::Interactors::Sequence TEMPERATURE = Values::Temperature.new(180, unit: :cel) def call! Services::CheckProductsAvailability.call list: ingredients dough = Services::BeatDough.call from: Repository::Products.get(beat_ingredients) filler = Services::MakeСabbageFiller.call from: Repository::Products.get(filler_ingredients) pie = Services::MakePie.call dough, with: filler bake = Services::BakePie.new pie, temp: TEMPERATURE sleep 5.min until bake.call pie end private attr_accessor :beat_ingredients, :filler_ingredients attr_accessor :pie def ingredients_list beat_ingredients_list + filler_ingredients_list end end end end
Как мы видим, метод call! описывает всю бизнес-логику процесса выпечки пирога. И его удобно использовать для понимания логики приложения.
Также, мы лег��о можем описать процесс выпечки рыбного пирога, заменив MakeСabbageFiller на MakeFishFiller. Тем самым, мы очень быстро меняем бизнес-процесс, без существенных доработок кода. И также, мы можем оставить обе Последовательности одновременно, масштабируя бизнес-кейсы.
Договоренности
- Метод
call!является обязательным методом, он описывает порядок Действий. - Каждый параметр инициализации может описываться через сеттер или
attr_acessor:
class Foo < LunaPark::Interactors::Sequence # ... private attr_accessor :bar end Foo.call(bar: 42)
- Остальные методы должны быть приватными.
Пример использования
beat_ingredients = [ Entity::Product.new :flour, 500, :gr, Entity::Product.new :oil, 50, :gr, Entity::Product.new :salt, 1, :spoon, Entity::Product.new :milk, 150, :ml, Entity::Product.new :egg, 1, :unit, Entity::Product.new :yeast, 1, :spoon ] filler_ingredients = [ Entity::Product.new :cabbage, 500, :gr, Entity::Product.new :salt, 1, :spoon, Entity::Product.new :pepper, 1, :spoon ] cooking = CookingPieWithСabbage.call( beat_ingredients: beat_ingredients, filler_ingredients: filler_ingredients ) # В случае успеха: cooking.success? # => true cooking.fail # => false cooking.fail_message # => '' cooking.data # => Entity::Pie # Если пирог сгорел: cooking.success? # => false cooking.fail # => true cooking.fail_message # => 'The pie burned out' cooking.data # => nil
Процесс представлен через объект и мы имеем все необходимые методы для его вызова — прошел ли вызов успешно, возникла ли какая-то ошибка в процессе вызова, и если произошла, то какая?
Обработка ошибок
Если сейчас вспомнить третий принцип применения Use Case, обратим внимание на то, что кроме Основного направления, у нас были еще и Альтернативные направления. Это ошибки, которые мы должны обработать. Рассмотрим пример: мы конечно не хотим чтобы события пошли подобным образом, но ничего не можем с этим поделать, суровая реальность такова, что пироги периодически сгорают.
Interactor перехватывает все ошибки унаследованные от класса LunaPark::Errors::Processing.
Как нам уследить за пирогом? Для этого определим ошибку Burned в Действие BakePie.
module Kitchen module Errors class Burned < LunaPark::Errors::Processing; end end end
И во время выпечки, проверим, что наш пирог не сгорел:
module Kitchen module Services class BakePie < LunaPark::Callable def call # ... rescue Errors::Burned, 'The pie burned out' if pie.burned? # ... end end end end
В этом случае сработает перехватчик ошибок, и мы сможем разобраться с ними в Эндпоинтах.
Ошибки, не унаследованные от Processing, воспринимаются как системные и будут перехвачены на уровне сервера. Если не обозначенные другие условия, то пользователь получит 500 ServerError.
Практика использования
1. Старайтесь описывать все вызовы в методе call!
Не следует реализовывать каждое Действие отдельным методом, это делает код более раздутым. Приходится просматривать весь класс несколько раз, чтобы понять как он работает. Испортим рецепт выпечки пирога:
module Service class CookingPieWithСabbage < LunaPark::Interactors::Sequence def call! check_products_availability make_cabbage_filler make_pie bake end private def check_products_availability Services::CheckProductsAvailability.call list: ingredients end # ... end end
Используйте вызов действий прямо в классе. Такой подход с точки зрения ruby может показаться непривычным, так он выглядит более читабельным:
class DrivingStart < LunaPark::Interactors::Sequence def call! Service::CheckEngine.call Service::StartUpTheIgnition.call car, with: key Service::ChangeGear.call car.gear_box, to: :drive Service::StepOnTheGas.call car.pedals[:right] end end
2. По возможности используйте метод класса call
# good - Обычно, экземпляр класса Действия, редко используется. # Логично использовать сокращенную запись. Sequence::RingingToPerson.call(params) # good - Тем не менее, есть возможность создавать экземпляр объекта Действиe, # что может быть полезно, когда нам нужно переиспользовать его, # с учетом внутреннего состояния. ring = Sequence::RingingToPerson.new(person) unless ring.success? ring.call sleep 5.min end
3. Не создавайте Функциональные объекты ради типизации кода, смотрите по ситуации
# bad - мы решили делать всю логику в Функциональных объектах, чтобы # сделать более легкой Последовательность действий. module Services class BuildUser < LunaPark::Callable def initialize(first_name:, last_name:, phone:) @first_name = first_name @last_name = last_name @phone = phone end def call Entity::User.new( first_name: first_name, last_name: last_name, phone: phone ) end private attr_reader :first_name, :last_name, :phone end end module Sequences class RegisteringUser < LunaPark::Interactors::Sequence attr_accessor :first_name, :last_name, :phone def call! user = Service::BuildUser.call(first_name: first_name, last_name: last_name, phone: phone) end end end # good - не следует писать отдельный класс, действуем практичнее. # Хотя при такой реализации следует задуматься о тестировании, # возможно необходимо вынести метод в Сервисный слой. module Sequences class RegisteringUser < LunaPark::Interactors::Sequence attr_accessor :first_name, :last_name, :phone def call! user #... end private def user @user = Entity::User.new( first_name: first_name, last_name: last_name, phone: phone ) end end end
Сервисный слой (Service Layer)

Порядок действий (Interactor), как мы говорили, описывает бизнес-логику на самом верхнем уровне. Сервисный слой (Service layer) уже раскрывает детали реализации функциональных требований. Если мы говорим о приготовлении пирога, то на уровне Порядка действий (Interactor) мы говорим просто "замешиваем тесто", не вдаваясь в детали как его замесить. Процесс замеса описывается на Сервисном уровне. Вернемся к первоисточнику, большой синей книге:
В прикладной предметной области бывают такие операции, которым нельзя найти естественное место в объекте типа Сущности (Entity) или Объекта-Значения (Value object). Они по своей сути являются не предметами, а видами деятельности. Но поскольку в основе нашей парадигмы моделирования лежит объектный подход, мы попробуем превратить их в объекты.
В этом месте легко совершить распространенную ошибку: отказаться от попытки поместить операцию в подходящий для нее объект, и таким образом, прийти к процедурному программированию. Но если насильно поместить операцию в объект с чуждым ей определением, от этого сам объект утратит чистоту замысла, станет труднее для понимания и рефакторинга. Если в простом объекте реализовать много сложных операций, он может превратиться в непонятно что, занятое непонятно чем. В таких операциях часто участвуют другие объекты предметной области и между ними выполняется согласование для выполнения совместной задачи. Дополнительная ответственность создает цепочки зависимости между объектами, смешивая понятия, которые можно было бы рассматривать независимо.
При выборе места реализации того или иного функционала, всегда пользуйтесь здравым смыслом. Ваша задача — сделать модель более выразительной. Разберем пример, "Нам нужно нарубить дрова" :
module Entities class Wood def chop # ... end end end
Такой метод будет являться ошибкой. Дрова сами себя не нарубят, нам потребуется топор:
module Entities class Axe def chop(sacrifice) # ... end end end
Если мы используем упрощенную бизнес-модель, этого будет достаточно. Но если процесс нужно смоделировать более детально, нам понадобится человек, который будет рубить эти дрова, и возможно, некоторое бревно, которое будет использоваться в качестве подставки для осуществления процесса.
module Entities class Human def chop_firewood(wood, axe, chock) # ... end end end
Как вы уже наверное догадались, это не самая удачная идея. Не все из нас занимаются рубкой дров, это не прямая обязанность человека. Мы часто видим насколько перегружены модели в Ruby on Rails, хранящие в себе подобную логику: получение скидки, добавление товара в корзину, снятие денег к бала��су. Эта логика относится не к сущности, а к процессу в котором задействована эта сущность.
module Services class ChopFirewood # ... end end
После того, как мы разобрались, какую логику мы храним в Службах попробуем реализовать один из них. Чаще всего службы реализуются через методы или функциональные объекты.
Функциональные объекты
Функциональный объект выполняет одно функциональное требование. В самом примитивном виде функциональный объект имеет один единственный публичный метод — call.
module Serivices class Sum def initialize(x, y) @x = x @y = y end def call x + y end def self.call(x,y) new(x,y).call end private attr_reader :x, :y end end
Такие объекты имеют ряд преимуществ: они лаконичны, их очень просто тестировать. Есть и недостаток, таких объектов может получиться большое количество. Есть несколько способов сгруппировать подобные объекты, мы в части своих проектов делим их по типу:
- Сервисный объект (Service) — объект, создает новый объект;
- Команда (Command) — изменяет текущий объект;
- Вахтер (Guard) — возвращает ошибку если, что-то пошло не так.
Сервисный Объект (Service)
В нашей реализации Service — реализует функциональное требование и всегда возвращает значение.
module KorovaMilkBar module Services class FindMilk < LunaPark::Callable GLASS_SIZE = Values::Unit.wrap '200g' def initialize(fridge:) @fridge = fridge end def call fridge.shelfs.find { |shelf| shelf.has?(GLASS_SIZE, of: :milk) } end private attr_reader :fridge end end end FindMilk.call(fridge: the_red_one) # => #<Glass: ... >
Команда (Command)
В нашей реализации Command — выполняет одно Действие, изменяет объект, в случае успеха возвращает true. По факту, Команда не создает объект, а изменяет существующий.
module KorovaMilkBar module Commands class FillGlass < LunaPark::Callable def initialize(glass, with:) @glass = glass @content = with end def call glass << content true end private attr_reader :fridge end end end glass = Glass.empty milk = Milk.new(200, :gr) glass.empty? # => true FillGlass.call glass, with: milk # => true glass.empty? # => false
Вахтер (Guard)
Вахтер, выполняет логическую проверку и в случае провала выдает ошибку обработки. Такой тип объекта никак не влияет на Основное направление, но переключает нас на Альтернативное направление, если что-то пошло не так.
При подаче молока важно убедится, что оно свежее:
module KorovaMilkBar module Guards class IsFresh < LunaPark::Callable def initialize(product) @products = products end def call products.each do |product| raise Errors::Rotten, "#{product.title} is not fresh" if product.expiration_date > Date.today end nil end private attr_reader :products end end end
Возможно вам покажется удобным разделять функциональные объекты по типу. Вы можете добавить свои, например, Builder — создает объект на основе параметров.
Договоренности
- Метод
callявляется единственным обязательным публичным методом. - Метод
initializeявляется единственным опциональным публичным методом. - Остальные методы должны быть приватными.
- Логические ошибки должны наследоваться от класса
LunaPark::Errors::Processing.
Обработка ошибок
Следует разделить 2 типа ошибок, которые могут произойти во время работы того или иного Действия.
Ошибки процесса выполнения
Такие ошибки могут возникать в результате нарушения логики обработки.
Например:
- при создании пользователя email зарезервирован;
- при попытке выпить молоко, оно закончилось;
- другой микросервис отклонил действие (по логической причине, а не потому, что сервис недоступен).
По всей вероятности, об этих ошибках захочет узнать пользователь. Также, вероятно, это те ошибки,
которые мы можем предвидеть.
Такие ошибки должны наследоваться от LunaPark::Errors::Processing
Системный ошибки
Ошибки, которые произошли в результате сбоя системы.
Например:
- не работает БД;
- что-то поделилось на ноль.
По всей вероятности, мы не можем предвидеть эти ошибки и ничего не можем сказать пользователю, кроме того, что все очень плохо, и отправить разработчикам отчет, призывающий к действию. Такие ошибки должны наследоваться от SystemError
Есть еще, ошибки валидации, которые мы рассмотрим подробнее в следующей статье.
Практика использования
1. Используйте переменные, чтобы повысить читаемость
module Fishing # bad - не информативно Serivices::Catch.call(fish, rod) # bad - избыточно Serivices::Catch.call(fish: fish, rod: rod) # good - более выразительно Serivices::Catch.call(fish, with: rod) module Serivices class Catch def initialize(fish, with:) @fish = fish @rod = with # внутри класса мы используем переменную # указывающую на объект. end # ... private attr_reader :fish, :rod end end end
2. Передавайте объекты, а не параметры
Старайтесь делать инициализатор простым, если обработка параметров не является его целью.
Передавайте объекты, а не параметры.
module Service # bad - на сервисном уровне мы работаем только с бизнес-логикой. Преобразование # типов следует вынести на уровень выше, например в форму. class Foo def initialize(foo_params:, bar_params:) @foo = Values::Foo.new(*foo_params) @bar = Values::Bar.new(*bar_params) end # ... end Services::Foo.call(foo: {a: 1, b: 2}, bar: 34) # good - реализуем только бизнес-логику. class Bar def initialize(foo:, bar:) @foo = foo @bar = bar # ... end end foo = Values::Foo.new(a: 1, b: 2) bar = Values::Bar.new(34) Services::Bar.call(foo: foo, bar: bar) # good - логичным исключением является реализация шаблона проектирования - Builder. class BuildFoo def initialize(param_1:, param_2:) @param_1 = param_1 @param_1 = param_1 end def call Foo.new( param_1: param_1.foo, param_2: param_2.bar, param_3: some_magick ) end # ... end end
3. Используйте в название Действия — глагол действия и объект воздействия.
# bad module Services class Milk; end class Work; end class FooBuild; end class PasswordGenerator; end end # good module Services class GetMilk; end class WorkOnTable; end class BuildFoo; end class GeneratePassword; end end
4. По возможности используйте метод класса call
Обычно экземпляр класса Действия, редко используется кроме того, чтобы писать сделать вызов.
# good - Логично использовать сокращенную запись. Services::BuildFoo.call(params) # good - Или еще более сокращенную Services::BuildFoo.(params) # good - Тем не менее, есть возможность создавать экземпляр объекта Действия, # что может быть полезно, когда нам нужно переиспользовать его, с учетом # внутреннего состояния. ring = Services::RingToPhone.new(phone: neighbour) 10.times do ring.call end
5. Обработка ошибок не является задачей сервиса
# bad - оставьте обработку ошибку интеракторам, а сервис легким. def call #... rescue SystemError => e return false end
Модули
До этого момента мы рассматривали имплементацию Сервисного слоя как набора функциональных объектов. Но мы легко можем разместить на этом слое методы:
module Services def sum(a, b) a + b end end
Другая проблема, которая встает перед нами — большое количество сервисных объектов. Вместо, набивших оскомину «rails fat model», мы получим «services fat folder». Есть несколько способов организовать структуру, чтобы уменьшить масштаб трагедии. Эрик Эванс решает это за счет того, что объединяет ряд функций в один класс. Представим, что нам нужно смоделировать бизнес-процессы няни, Арины Родионовны, она может кормить Пушкина и укладывать его спать:
class NoonService def initialize(arina_radionovna, pushkin) # ... end def to_feed # ... end def to_sleep # ... end end
Такой подход более корректный с точки зрения ООП. Но мы предлагаем от него отказаться, по крайней мере, на начальных этапах. Не очень опытные программисты начинают писать много кода в таком классе, что в конечном счете приводит к увеличению связности. Вместо этого можно использовать модуль, представляя деятельность некоторой абстракцией:
module Services module Noon class ToFeed def call! # ... end end class << self # При этом сложные процессы, можно вынести # в функциональные объекты def to_feed(arina_radionovna, pushkin) ToFeed.new(arina_radionovna, pushkin).call end # А простые оставить методами, реализованными в модуле def to_sleep(arina_radionovna, pushkin) arina_radionovna.tell_story pushkin pushkin.state = :sleep end end end end
При делении на модули должна соблюдаться низкая внешняя зависимость (low coupling) при высокой внутренней связности (high cohesion), мы же используем такие модули как Services, или Interactors, это также идет в разрез с идеями чистой архитектуры. Это осознанный выбор, который облегчает восприятие. По имени файла мы понимаем какой шаблон проектирования реализует тот или иной класс, если для опытного программиста это очевидно, то для новичка это не всегда так. После того как ваша команда будет готова, откажитесь от этого излишества.
Процитирую еще небольшой отрывок из большой синей книги:
Выберите такие модули, которые бы рассказывали историю системы и содержали связные наборы понятий. От этого часто сама собой возникает низкая зависимость модулей друг от друга. Но если это не так, найдите способ изменить модель таким образом, чтобы отделить понятия друг от друга, или же поищите пропущенное в модели понятие, которое могло бы стать основой для модуля и тем самым свести элементы модели вместе естественным, осмысленным способом. Добивайтесь низкой зависимости модулей друг от друга в том смысле, чтобы понятия в разных модулях можно было анализировать и воспринимать независимо друг от друга. Доработайте модель до тех пор, пока в ней не возникнут естественные границы в соответствии с высокоуровневыми концепциями предметной области, а соответствующий код не разделится соответствующим образом.
Дайте модулям такие имена, которые войдут в ЕДИНЫЙ ЯЗЫК. Как сами МОДУЛИ, так и их имена должны отражать знание и понимание предметной области.
Тема модулей большая и интересная, но в полном объеме явно выходит за тему данной статьи. В следующий раз мы поговорим с вами о Репозиториях и Адаптерах. Мы открыли уютный телеграмм канал, где хотелось бы делиться материалами на тему DDD. Будем рады вашим вопросам и обратной связи.
- https://www.netsolutions.com/insights/business-and-functional-requirements-what-is-the-difference-and-why-should-you-care/
- http://magazines.russ.ru/nz/2007/54/gi3.html
- https://www.microtool.de/en/knowledge-base/how-use-case-2-0-works/
- https://www.researchgate.net/publication/220059381_Use_cases_-_Yesterday_today_and_tomorrow
- Проблемно-ориентированное проектирование, Эрик Дж. Эванс
