DDD & TDD. Часть III

Итак, у нас есть домен с необходимыми классами и сервисами. В целом можно генерировать и обрабатывать данные в любых количествах согласно написанным сервисам. Но есть проблема, даже проблемы:

  • Как пользователь будет вводить данные;
  • Как данные будут сохраняться/загружаться.

Домен по природе и задумке своей должен быть полностью независимым и работать независимо от того, как построено взаимодействие с пользователем (WPF, WinForms, Web) и как реализован слой данных (MSSQL, MySQL, Oracle и т.д.).

Слоеные пироги

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

  • Домен – слой бизнес-логики. В котором описано КАК работать с данными.
  • Слой данных. В этом слое осуществляется сохранение объектов во внешние ресурсы и восстановление из внешних ресурсов в доменные объекты. Выделением этих операций в отдельный слой мы делаем приложение более гибким, т.к. можно осуществлять работу с несколькими БД, меняя их, а приложение этого даже не заметит.
  • Слой представления. В этом слое мы говорим приложению ЧТО делать с данными, т.е. вызываем в нужном порядке доменные сервисы и отдаем результат вычислений на прорисовку. Таким образом, этот слой связывает Домен и Front End.
  • Пользовательское отображение (Front End). Это пользовательский интерфейс, может быть чем угодно: консолью, win forms, web, да хоть сразу в мозг! =) Этот слой должен быть максимально «тупой», тут недолжно быть никаких изменений данных, никакой логики, как и что показывать. Слой просто делает все что ему скажут без капли самодеятельности.

После того, как определились из чего состоит приложение, надо понять как эти слои связаны между собой. Еще раз скажу, что домен ни от чего не зависит, доменная сборка не должна содержать ссылок на другие слои. Слой данных ссылается на домен, т.к. требуется сохранять и восстанавливать данные из внешних источников. Слой представления ссылается на домен и слой данных. Пользовательское отображение ссылается на  представление. Более подробно все детали соотношений будут далее по тексту, пока же можно связи представить следующим образом: Для того чтобы эта схема заработала надо вкратце рассказать о паттерне Inversion Of Control /Dependency Injection

.

Inversion of Control/Dependency Injection

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

В дата фасаде

Стало: В домене код

В фасаде

Получается, что фасад осуществляет реализацию интерфейсов из домена. Такая же история и с пользовательским интерфейсом. В слое презентации определяются интерфейсы для вьюх (view. По-другому уже не сказать), которые будут реализованы в слое GUI.

Тестирование

Представим что у нас все реализовано без интерфейсов. Придется подготавливать очень много данных, для того чтобы проходили тесты. Это будет адом, да таким что вы перестанете писать тесты вообще. С помощью интерфейсов можно сделать поддельные (Fake) классы. Например, мы тестируем поведение некоторого сервиса, который вызывает другой сервис. В обоих могут быть достаточно объемные вычисления.

Тест на сервис В будет выглядеть так:

И как вы думаете, сколько будет тест ходить? И будет желание каждый раз его запускать? У меня точно не будет! Сейчас будем творить самую обыкновенную уличную магию по ускорению прохождения тестов. Для того чтобы тесты забегали быстро-быстро необходимо выделить интерфейсы для сервисов.

После этого в тестовом проекте можно создать фальшивый класс сервиса А. Для удобства так же создадим и свойство, которое будет отвечать за возвращаемый результат.

Теперь тест на сервис В будет выглядеть так:

И ходить он будет примерно в 1000000 раз быстрее!!! Это невероятно! ;)  Но тесты на сервис А все равно будут долгими, от этого никуда не деться. Но хотя бы использование его в других тестах будет быстрым, а это немало уже. Я надеюсь, что основной принцип тестирования вы уловили.

Запуск приложения

После того как сделаете в порыве творческого запала некоторую работу, захочется проверить первые результаты в деле. И тут возникает вопрос, как все это хозяйство запустить, ведь из сборки с GUI мы не можем создать ни одного класса, который смог бы стать главным пультом управления. Что делать? Создать еще одну специальную управляющую сборку, которая будет доступна для GUI и будет запускать требуемые сценарии работы программы. Можно сборку назвать что-то в духе AppRunner (Application). И в этой сборке в самом простом случае может быть достаточно всего одного класса Runner, который будет содержать все возможные варианты сценариев работы из слоя представления и запускать их по запросу с пользовательского интерфейса. Runner может содержать всего один публичный метод, которые принимает параметр указывающий на необходимый сейчас сценарий работы. Из принципа работы понятно, что Application зависит от типа GUI. Порядок работы:

  1. Запуск приложения. Одновременно запускается Runner с переданным заранее значением, указывающим на сценарий приветствия.
  2. Пользователь жмет на пункт меню.
  3. В обработчике события вызывается Runner с значением сценария в виде параметра.
  4. Сценарий из слоя представления запускается.
  5. Все действия не связанные с переходами между сценариями обрабатывает запущенный сценарий.

Да, согласен, в этом месте немного сумбурно, но это потому что тут уже многое зависит от самой структуры программы, от того каким образом запускаются основные сценарии. Причем сборка Application вполне может быть связана и с дата фасадом. Вариантов миллион. Вопросы, пожелания, замечания приветствуются!

7 комментариев на “DDD & TDD. Часть III

  1. Спасибо!!! Очень хорошее объяснение!!! Возник вопрос относительно разделения ответственности между Сущностями и Доменными Сервисами!

    Приведу пример: ( не судите строго ))) )
    Делаю игру. В игре имеются игровые объекты (шахты и заводы)! Определяю их как сущности! и создаю методы для создания и получения игровых ресурсов, а так же методы запуска и остановки этих объектов. Все выше перечисленные методы размещаю в игровом объекте! Но существует условие, которое не дает мне покоя : Если игровой объект разрушен до определенного состояния он останавливается.
    Подскажите пожалуйста, где лучше определить это условие в классе игрового объекта, или создать сервис, обслуживающий данные объекты и возложить на него ответственность за проверку состояния игрового объекта и остановку его в случае необходимости???

    • Привет! (Извините что поздно отвечаю)

      тут зависит от того, где и как хранится состояние игрового объекта (шахты, завода). Хотя логично предположить, что это свойство шахты, так как она должна знать сколько у нее «здоровья». Тогда получается, что игровой объект сам знает когда ему остановиться. Но при этом расчет здоровья может зависеть от нескольких факторов, которые не содержаться и не зависят от самой шахты, тогда нужен сервис, который будет обновлять значение здоровья и при его критическом значении объект шахты сам себя остановит.

      В качестве примера тут можно привести выработку ресурса. Скажем в шахту может бегать от 1 до 20 юнитов, которые еще могут и с различной эффективностью копать =), и чем больше их будет, тем быстрее в шахте закончатся ресурсы. Понятое дело, что шахта не знает сколько народу в нее бегает и каким образом отсчитывать остаток — это будет делать сервис. Тот же сервис перед отсчетом будет просто проверять «живость» шахты, спросив у объекта «ты жива?».

      Я бы сделал так

  2. Отлично!
    Вопрос по п.5: Сценарий из слоя представления передаёт GUI задачу на отрисовку новых данных (обновление), или это должен делать Runner из п.3?

    • Смотря что за обновление. Я так сразу сказать не могу.
      Насколько я помню, я там имел ввиду, что Runner грубо говоря управляет сменой экранов и все. Любые обновления данных внутри одного экрана лежат на конкретном сценарии.

  3. Ещё созрел вопрос: у меня есть 4 слоя: данные, бизнес-логика, хранилка и отображалка
    В качестве данных используется объект, к которому у пользователя может быть 4 типа доступа (или не быть вовсе). Типы доступа: чтение, создание, изменение, удаление.
    Для чтения бизнес-логика вызывает у хранилки метод «Загрузить». Для создания, изменения и удаления объекта метод «Сохранить». При сохранении хранилка смотрит состояние объекта и определяет — новый это объект (создаёт) или он уже есть в базе (тогда изменяет существующий). При определённых условиях хранилка удаляет объект из базы.
    Структура программы не верная? Потому что я не могу придумать, куда лучше поместить проверку на доступ пользователя к действию: Бизнес-логика у меня не знает, какое именно действие с объектом в базе совершится при его сохранении. А проверять права в слое-хранилке — вроде как не правомерно. Что вы посоветуете? Выносить из хранилки логику определения типа запроса в слой бизнес-логики? Или есть более подходящее решение?

    • Тут может быть полезен шаблон UnitOfWork как я вижу.
      Если кратко, то он отвечает за то, как и когда сохранять данные в базу. Т.е. бизнес-логика работает с UnitOfWork и для нее это словно бы база. Но на самом деле UoW абстрагирован от способа сохранения, т.е. нарушения логики нет.
      UoW создается на каждого пользователя и на каждый «логический экран». Поэтому я не вижу сильных противоречий, если UoW будет знать о роли пользователя. В зависимости от роли, можно реализовать стратегии, которые будут находиться в бизнес-логике и использоваться при CRUD операциях.

Оставить комментарий