Domain, Context & Integration (DCI)

Относительно недавно на хабрахабре попалась статья Data Context Interaction (DCI) — эволюция объектно-ориентированной парадигмы, которая меня весьма заинтересовала описанным подходом, но примеры в ней были тупо скопированы с оригинальных публикаций 3х годичной давности или около того. Вообще все статьи по теме оперируют одним и тем же примером, который, на мой взгляд, не до конца раскрывает тему и все плюсы использования DCI. Но это общая проблема интернетов, даже уже говорить про это не хочется.

В исходной статье хорошо дана теория, проблемы которые возникают при традиционном подходе и как они решаются с помощью DCI, но в примере не приводится сравнения. Собственно этот недостаток я и хочу исправить в силу того, как я сам понял этот подход после прочтения дополнительной документации, представленной в конце статьи и просмотра видеодокладов по теме.

Для начала все же стоит привести немного теории, для тех, кому лень читать статью с хабра.

Проблема

Объектно-ориентированный подход проигрывает в отображении взаимодействия объектов. Для того чтобы показать что я имею ввиду, давайте взглянем на две системные операции (два
сценария использования) объектов из одной группы, взаимодействующих друг с другом.

На картинке показана системная операция четырех объектов, общающихся между собой. В данном случае Message это вызов метода или функции.

Рассмотрим второй возможный сценарий работы с теми же объектами:

Мы видим совсем другой сценарий использования, другая системная операция, использующая ту же самую группу объектов. Но модель взаимодействия объектов и сообщения уже другие.

Выше были представлены две системные операции выраженные в Use Case 1 и Use Case 2. Как мы их отображаем в коде? В идеальном случае, я бы хотел иметь возможность открыть один файл и понять модель взаимодействия объектов в сценарии использования, над которым я работаю. Если я работаю над Use Case 1, я ничего не хочу знать про Use Case 2. Вот что я считаю успешным отображением сценариев использования в коде.

На данный момент в гипотетическом коде, по которому построены UseCase может быть такое распределение методов.

Проблема, которая проиллюстрирована на картинке, в том, что исходный код не отображает того, что происходит в рантайме. Исходный код говорит нам о четырех обособленных объектах, с кучей методов в каждом из них. Рантайм говорит нам о наличии четырех, общающихся между собой, объектов, и только малое подмножество этих методов имеет отношение к данному сценарию использования. Эта нестыковка осложняет понимание программ. Исходный код показывает нам одну историю, а рантайм — совершенно другую.

DCI спешит на помощь

При использовании DCI основной будет новая абстракция контекста – описание взаимодействия. Контекст — это класс, содержащий в себе все роли (roles) для данного сценария использования. Каждая роль представляет собой сослуживца во взаимодействии и обыгрывается неким объектом. Как вы видите, контекстно-зависимое поведение сосредоточено в ролях. Контекст только лишь назначает роли объектам и после этого инициирует взаимодействие.


Здесь мы видим отделение неизменной части системы, содержащее только данные и локальные методы, от данного сценария использования. Все традиционные объектно-ориентированные техники могут быть использованы для моделирования этой неизменной части. В частности, я бы рекомендовал использовать техники проблемно-ориентированного проектирования (Domain-driven design), такие как агрегаты (aggregates) и репозитории (repositories), но здесь отсутствуют контексто-зависимое поведение и взаимодействие, есть только локальные методы.

А вот как будет выглядеть второй контекст

При этом объекты A-D остаются прежними.

Практика

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

В качестве примера возьмем пример, который уже наверно набил оскомину всем разработчиком, но который так хорошо подходит для описания многих технологий и подходов. Пусть у нас есть некоторый HR отдел и можно производить разные кадровые действия – найм, отпуск и так далее.

Начнем c того, как это выглядит обычно.

Традиционный подход

При традиционном подходе, я бы создал класс Person, в котором будут описаны методы работы с классом.

Тел у методов специально нет, так как в данном контексте это совершенно не важно. В целом уже можно догадаться о том, что может делать класс. В классе описаны возможности в зависимости от того, кто будет представлен этим классом: рабочий Василий, менеджер Иван или же директор Петр.

Класс Offer является пустым, Position и State являются перечислимыми типами (enum), что в целом не будет влиять на основную концепцию.

Рассмотрим данный класс в контексте принятия нового работника. Пусть мы регистрируем кандидатов и с ними в системе тоже ведется разная аналитическая работа. Итак, сразу полный класс:

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

Глядя на код в статике, проблем не увидеть, так как мы используем только нужные методы класса Person, получаем нужные объекты из репозитория – всё хорошо.

Но если привести скриншоты из рабочего процесса, то можно увидеть, что intelliSence предлагает все доступные методы класса Person и можно нанять, к примеру менеджера, а не кандидата, так
как для кода они идентичны.


 И еще пример

В работе это может запутывать. К тому же методы не дают подсказок по типам.

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

DCI подход

Учитывая мои знания по возможностям C#, то же самое я представил в виде следующего кода:

Кода получилось несколько больше, но тем не менее. В качестве роли выступают инерфейсы, в данном случае я выделил интерфейсы босса, работника, стороннего человека – в соответствии с типичными задачами =) Ну или около того, в соответствии с выполняемыми задачами в роли.

При внимательном рассмотрении реализации интерфейсов можно видеть, что в качестве параметров методов указаны интерфейсы и, например, не получится «по незнанию» нанять уже работающего сотрудника еще раз.

Только что подумал, что в целом это хорошо, потому что босс снизу видится как человек нанимающий и увольняющий людей, а так же принимает или переносит отпуск. В то же время этот босс для своего босса видится как простой работник, который вджобывает и иногда просит отпуск.

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

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

Т.е. теперь у нас в качестве кандидатов только люди с улицы и мы видим только их специфичный набор методов. При найме, все ровно то же самое: менеджер не может нанять повторно
сотрудика и компилятор нам в этом помогает, форсируя применение логики приложения.

Так же компилятор поможет отсеять невозможные действия в данном контексте, как например
отправка кандидата в отпуск:

На мой взгляд отличный функционал получается!

Получается действительно оперирование не классом, а объектом и контекстом его в определенной ситуации. Так как даже в представленном примере объект «человек» может выполнять множество разных ролей и действия\возможности его при этом будут очень сильно различаться.

Спорные моменты

Конечно остается большой вопрос о том, как реализовывать поведение роли, как ее навешивать на объект, может все совсем не так как есть на самом деле. В C# динамически добавлять функционал объекту, как в примере на руби или еще в каких-либо языках, не получится, но это наверно и не сильно большая проблема.

Честно сказать, еще не знаю насколько много людей самостоятельно создает partial классы и использует их в реальной работе. На мой взгляд это очень хороший инструмент, и в данном случае позволяет хорошо отделить функциональные части одного объекта.

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

В общем надо читать, пробовать, думать. По мне так весьма интересный подход.

Небольшое обсуждение с коллегами показало, что ничего нового в таком подходе они не видят, в том смысле что интерфейсы пользуются давно и успешно и в чем отличие. Отличие же, на мой взгляд в том, как именно строятся интерфейсы, для чего они служат. В данном случае они отображают возможные методы действий в зависимости от роли. В C# только интерфейсы для меня выглядят подходящим инструментом для динамического расширения\сужения возможностей конкретного объекта в процессе выполнения приложения.

Далее было возражение насчет того, что код в статике точно так же не отображает процесса работы. Возможно, тут я еще не уверен как оно на практике пойдет. Однако в статике в данном случае видны роли\интерфейсы, перейдя к их реализации мы имеем дело только с теми методами которые применимы к данному сценарию, особенно если разделять класс по файлам. Так что с некоторой натяжкой возможно как раз будет как на картинке. К тому же у сценария может быть не одна точка входа и разные триггеры срабатывания.

Вообще чисто с технической точки зрения в данном подходе нет ничего экстраординарного и волнительного. Это организационный подход к написанию кода, и главное здесь соблюдение дисциплины кодирования. Естественно, что можно все сломать и переделать и кастовать объекты произвольно в контекстах, система все позволит технически. Данный подход скорее должен заставлять задумываться о том, что вы делаете и как, если возникают проблемы в использовании. Может неправильно определены роли, или пошла тенденция к God-object.

UPD:

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

 

Дополнительное чтение

В качестве дополнительного чтения можно взять следующий ресурс http://fulloo.info/. Там есть ссылка на статьи изобретателя, видео. Пояснения, примеры (2 и один из них разошелся по всему интернету), документы, FAQ (достаточно примитивный).

Годная длинная подробная статья http://www.artima.com/articles/dci_vision.html

Видео с NDC 2012 http://csharptube.com/watch/37255/trygve-reenskaug-object-orientation-revisited-simplicity-and-power-with-dci

 

Hard’n’Heavy!

 

7 комментариев на “Domain, Context & Integration (DCI)

  1. У меня такое ощущение, что Вы уже все возможные сценарии использования включили в класс Person, что несколько не совпадает с парадигмой DCI (как я ее понял).

    Думаю, было бы верным в классе Person оставить только два метода ChangeStatus & ChangePosition, а вот остальные методы вынести в соответствующие контексты — НанятьСотрудника, ОтправитьСотрудникаВОтпуск, СделатьВыговор и т.д., внутри которых и определять роли.

  2. Даже не так… Скорее всего Вам потребуется контекст Фирма, в котором будут описаны различные сценарии использования класса Person (НанятьСотрудника, ОтправитьВОтпуск,…).

    В любом случае, спасибо за статью! Заставила задуматься над текущим проектом…

    • Хм, да, скорее всего для найма потребуется другой класс — Фирма, тут я соглашусь. И тогда получается, что в контексте Найма будет описан способ найма.
      А вот насчет отпусков, премий, наказаний и вджобываний — не соглашусь. Решение принимает не абстрактная Фирма, а определенный человек (начальник) и тогда ведь должны быть методы у объектов, а не контекст выполняет всю работу. Если контекст выполняет всю работу, то получаем анемичный домен.

      Надо еще подумать на эту тему.

        • Пообщался с автором оригинальной статьи с хабра и вот что он говорит:
          Общая идея поймана правильно, но… в контексте должен расширятся новыми свойствами сам объект в зависимости от роли, и роль не привязана к конкретным классам и объектам. По большому счету должна быть возможность наделить нужной ролью любой подходящий по смыслу объект в конкретном контексте. В таком разрезе я вообще затрудняюсь сказать, как это может быть реализовано средствами C#.

          • Вероятно, все объекты модели должны быть наследником одного класса — Родителя. А в контексте Фирма переменные Boss, Manager должны быть именно этого типа. Тогда неважно, что ты передашь в конструктор Person или ТелеграфнныйСтолб все равно они получат все присущие этим ролям возможности.

            Правда, мне кажется уж очень широких возможностей оставлять все-таки не следует… должны быть классы-потомки Человек, Столб и в контексте надо использовать именно их, а не Родитель. Ни к чему телеграфным столбам возможность нанимать сотрудников в Фирму.

  3. Со всем полностью согласен. Но ключевая фраза «Это не ООП подход.» Я именно это и хотел сказать, что не всё так гладко в ООП. В данной статье, по сути, описывается метод реализации на ООП читабильности функционального подхода, когда выделяются отдельно стуктуры данных (в данном случае аккаунт), и отдельно действия (в данном случае транзакция и её контекст). Из чего следует вопрос, а зачем тогда так следовать ООП? Это напоминает ситуацию когда для вместо того, чтобы написать простую функцию с аргументами делают класс, в конструктор которого передают аргументы, и пишут отдельный метод вроде «run» для вызова. Спрашивается, зачем?

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