Слишком часто стала мелькать в западных блогах и твиттере аббревиатура “DCI”. Меня удивил тот факт, что на хабре по данной тематике почти нету информации, лишь в Ruby NoName Podcast S04E09 упоминалось об этом. Любопытство взяло вверх, и я решил узнать об этом загадочном слове побольше. В процессе поиска я наткнулся на хорошую статью, написанную на английском моим земляком, Виктором Савкиным. Данная статья без обильной теории, на практических примерах показывает, что из себя представляет DCI. Далее повествование будет идти от лица Виктора.
Давайте начнем с обзора тех проблем, где традиционное объектно-ориентированное программирование зарекомендовало себя с лучшей стороны.
Объекто-ориентированное программирование показало себя хорошо в управлении состоянием объекта. Классы, поля и свойства — это мощные инструменты, позволяющие вам определить состояние и оперировать им. Так как мы имеем эти средства отображения состояния, встроенные в языки программирования, то мы можем сделать вывод о нем, как во время компиляции, так и во время рантайма. Во время компиляции мы можем взглянуть на определение объекта класса, а в рантайме мы можем опросить объект о его полях.
Другая проблема, которая решается достаточно хорошо всеми объекто-ориентированными языками, состоит в отображении операций связанных с состоянием объекта. Такие операции не вовлечены в какую либо совместную работу. Они локальны для владеющего объекта. Мы выражаем локальные операции определением методов класса. Когда мы определяем метод у класса и создаем его экземпляр, мы знаем что он будет иметь этот метод. Это достаточно очевидно.
В качестве хорошего примера объекта, имеющего только локальные операции, можно привести Ruby класс String. Каждый String имеет массив байтов или символов. Все его операции работают с этим массивом. Этот объект независим — ему нет необходимости взаимодействовать с другими объектами. Я сомневаюсь, что кто либо может не понять как использовать String. Для решения такого рода задач и были построены ОО языки.
ОО проигрывает в отображении взаимодействия объектов. Для того чтобы показать что я имею ввиду давайте взглянем на две системные операции(два сценария использования) объектов из одной группы, взаимодействующих друг с другом.
![](https://habrastorage.org/r/w1560/storage2/6d8/1d3/f73/6d81d3f73cde451190bce86b70e7cef7.png)
На картинке показана системная операция четырех объектов, общающихся между собой.
![](https://habrastorage.org/r/w1560/storage2/dbd/b79/bcb/dbdb79bcbd7ca70945f827091ab9937d.png)
А здесь уже представлен другой сценарий использования, другая системная операция, использующая ту же самую группу объектов. Но как вы видите, модель взаимодействия объектов и сообщения уже другие.
Мы видели две системные операции выраженные в Use Case 1 и Use Case 2. Как мы их отображаем в коде? В идеальном случае, я бы хотел иметь возможность открыть один файл и понять модель взаимодействия объектов в сценарии использования, над которым я работаю. Если я работаю над Use Case 1, я ничего не хочу знать про Use Case 2. Вот что я считаю успешным отображением сценариев использования в коде.
К сожалению, традиционное объектно-ориентированное программирование не дает нам какой-либо возможности это сделать. Оно дает нам некоторые инструменты для отображения состояния объектов и назначения им локального поведения. Но у нас нету каких-либо хороших способов описать взаимодействие объектов между собой в рантайме для выполнения сценария использования. Следовательно, системные операции не отображены в коде.
В конечном счете, мы все равно пишем код и программируем системные операции каким-то образом. Как мы это делаем? Мы делим их на множество мелких методов и назначаем их куче разных объектов.
![](https://habrastorage.org/r/w1560/storage2/b0c/dc6/b9b/b0cdc6b9b552ee4d36a830e73842776b.png)
Здесь мы видим, что все методы, необходимые для всех сценариев использования, зажаты в этих объектах. Методы для выполнения первого сценария выделены зеленым, для второго — красным. Кроме того, у этих объектов имеются некоторые локальные методы. Они используются синими и красными методами.
Проблема, которая проиллюстрирована на картинке, в том, что исходный код не отображает того, что происходит в рантайме. Исходный код говорит нам о четырех обособленных объектах, с кучей методов в каждом из них. Рантайм говорит нам о наличии четырех, общающихся между собой, объектов, и только малое подмножество этих методов имеет отношение к данному сценарию использования. Эта нестыковка осложняет понимание программ. Исходный код показывает нам одну историю, а рантайм — совершенно другую.
Кроме того, нету никакого способа определить системные операции (сценарии использования) в явном виде, поэтому нам приходится отслеживать все вызовы методов, чтобы получить представление о том, что происходит в программе. У нас нету файла, который мы могли бы открыть, чтобы понять данный сценарий использования. Даже хуже, так как все классы содержат методы для уймы сценариев использования, нам приходится тратить много времени на отфильтровывание нужных нам методов.
DCI (Data, context, interaction) — это парадигма, созданная Trygve Reenskaug (автор MVC шаблона), для разрешения этих проблем.
Давайте взглянем на первый сценарий использования выраженный в DCI стиле.
![](https://habrastorage.org/r/w1560/storage2/57a/442/367/57a442367d93986a33d2af0e9c7c4d25.png)
Здесь мы видим отделение неизменной части системы, содержащее только данные и локальные методы, от данного сценария использования. Все традиционные объектно-ориентированные техники могут быть использованы для моделирования этой неизменной части. В частности, я бы рекомендовал использовать техники проблемно-ориентированного проектирования (Domain-driven design), такие как агрегаты (aggregates) и репозитории (repositories), но здесь отсутствуют контексто-зависимое поведение и взаимодействие, есть только локальные методы.
У нас в наличии есть новая абстракция для описания взаимодействия — контекст (context). Контекст — это класс, содержащий в себе все роли (roles) для данного сценария использования. Каждая роль представляет собой сослуживца во взаимодействии и обыгрывается неким объектом. Как вы видите, контекстно-зависимое поведение сосредоточено в ролях. Контекст только лишь назначает роли объектам и после этого инициирует взаимодействие.
![](https://habrastorage.org/r/w1560/storage2/110/4e9/109/1104e91095586758b1e2c07807b1d4fc.png)
Я бы хотел отметить, что наши объекты (Object A-D) остаются прежними. Нам не пришлось добавлять каких либо методов для второго сценария использования. Все представленные методы являются фундаментальными, самодостаточными и локальными. Все сценарии использования отдельного поведения были выделены в контекст и роли.
Стоит также отметить, что мы не видим красные и зеленые методы в одно и тоже время. Каждый контекст содержит только необходимые для своего выполнения методы.
Возможно это звучит слишком абстрактно, так что давайте взглянем на пример кода, выполненный на языке Ruby.
Здесь представлен пример Hello World только для DCI. Все заинтересованные в DCI начинают с перевода денег, с одного аккаунта на другой.
Я понимаю, что этот пример слишком упрощен, и так как он настолько прост, он может успешно выражен через сервисы (services), регулярные объекты (regular entities) или функции. Так что взгляните на этот пример, как на иллюстрацию того, как вы должны структурировать ваш код.
Так как мы говорим о переводе денег с одного аккаунта на другой, то нам понадобится хранить информацию об аккаунтах каким-нибудь способом. За это отвечает класс Account. Он хранит информацию о балансе и списке транзакций.
Как вы видите, все методы здесь локальны и контекстно-независимы. Аккаунт ничего не знает о переводе денег. Он только ответственен за пополнение баланса и считывания денег с него. Логика перевода денег находится в контексте:
Я запрашиваю два аккаунта из базы данных, затем создаю экземпляр контекста и вызываю метод transfer. Вы наверно заметили, что я передаю два аккаунта в конструктор, а сумму перевода — в метод transfer. Этим я хочу показать какие объекты являются актерами в данном взаимодействии, а какие только данными. Аккаунты — это актеры, у них есть поведение, а сумма перевода — это уже данные.
Далее я назначаю роли объектам аккаунтов в моем конструкторе. Я обучаю эти объекты данных быть аккаунтом-источником и аккаунтом-получателем.
В конце я инициирую взаимодействие вызовом метода transef_out у аккаунта-источника. В этом примере контекст только инициирует взаимодействие, но в некоторых сложных случаях он может так же координировать актеров.
А теперь давайте взглянем на реализацию ролей:
Сначала я проверяю, что аккаунт-источник имеет достаточно денег, затем я вычитаю из баланса сумму перевода. После этого я вызываю аккаунт-получатель через переменную контекста, для того чтобы сообщить ему, чтобы тот получил денег.
Обратите внимание на несколько интересных вещей:
Контекст и обе роли:
DCI решает проблему традиционного объекто-ориентированного программирования, когда алгоритм размазан среди множества разных файлов. Если вы захотите узнать, как реализован какой-либо сценарий использования, вам нужно будет открыть всего лишь один файл.
Контекст содержит только методы являющихся частью сценария выполнения, которую он представляет. Так что вам не придется сканировать десятки, а то и сотни методов, не имеющих ничего общего с проблемой над которой вы работаете.
Все объекты данных и их локальные методы выражают собой “Что из себя представляет система”. Обычно, данная часть системы неизменна. Контекстно-зависимое, быстро изменяющееся поведение представляет из себя “Что система делает”. Разделение неизменных частей от быстро изменяющихся — одно из необходимых условий для построения стабильного ПО. И DCI обеспечивает это разделение:
Также исходный код совпадает с рантаймом. Рантайм показывает нам, что у нас в наличии два аккаунта и сумма перевода. Вот что вы увидите если откроете контекст.
Основное преимущество DCI состоит в явно выраженных ролях. Достаточно много проектировщиков сходятся во мнении, что объекты сами собой не имеют обязанностей, это удел ролей. Например, возьмите меня как пример объекта. Я обладаю следующими свойствами: я родился в России, мое имя Виктор, мой вес в районе 65кг. Могут ли эти свойства действительно выражать некие высокоуровневые обязательства? Они не могут. Но когда я возвращаюсь домой и начинаю играть роль мужа, я становлюсь ответственен за все супружеские дела. Итак, объекты играют роли. Тот факт, что роли не стоят во главе угла традиционного объекто-ориентированного программирования, неправилен.
Собственно сама статья:
Если вас заинтересовала данная идея, советую вам посмотреть следующие ресурсы:
Если вы предпочитаете чтение книг, то могу вам порекомендовать эти три книги:
Достоинства ОО
Давайте начнем с обзора тех проблем, где традиционное объектно-ориентированное программирование зарекомендовало себя с лучшей стороны.
Объекто-ориентированное программирование показало себя хорошо в управлении состоянием объекта. Классы, поля и свойства — это мощные инструменты, позволяющие вам определить состояние и оперировать им. Так как мы имеем эти средства отображения состояния, встроенные в языки программирования, то мы можем сделать вывод о нем, как во время компиляции, так и во время рантайма. Во время компиляции мы можем взглянуть на определение объекта класса, а в рантайме мы можем опросить объект о его полях.
Другая проблема, которая решается достаточно хорошо всеми объекто-ориентированными языками, состоит в отображении операций связанных с состоянием объекта. Такие операции не вовлечены в какую либо совместную работу. Они локальны для владеющего объекта. Мы выражаем локальные операции определением методов класса. Когда мы определяем метод у класса и создаем его экземпляр, мы знаем что он будет иметь этот метод. Это достаточно очевидно.
В качестве хорошего примера объекта, имеющего только локальные операции, можно привести Ruby класс String. Каждый String имеет массив байтов или символов. Все его операции работают с этим массивом. Этот объект независим — ему нет необходимости взаимодействовать с другими объектами. Я сомневаюсь, что кто либо может не понять как использовать String. Для решения такого рода задач и были построены ОО языки.
Недостатки ОО
ОО проигрывает в отображении взаимодействия объектов. Для того чтобы показать что я имею ввиду давайте взглянем на две системные операции(два сценария использования) объектов из одной группы, взаимодействующих друг с другом.
Первый сценарий использования
![](https://habrastorage.org/storage2/6d8/1d3/f73/6d81d3f73cde451190bce86b70e7cef7.png)
На картинке показана системная операция четырех объектов, общающихся между собой.
Второй сценарий использования
![](https://habrastorage.org/storage2/dbd/b79/bcb/dbdb79bcbd7ca70945f827091ab9937d.png)
А здесь уже представлен другой сценарий использования, другая системная операция, использующая ту же самую группу объектов. Но как вы видите, модель взаимодействия объектов и сообщения уже другие.
Код не отображает системные операции
Мы видели две системные операции выраженные в Use Case 1 и Use Case 2. Как мы их отображаем в коде? В идеальном случае, я бы хотел иметь возможность открыть один файл и понять модель взаимодействия объектов в сценарии использования, над которым я работаю. Если я работаю над Use Case 1, я ничего не хочу знать про Use Case 2. Вот что я считаю успешным отображением сценариев использования в коде.
К сожалению, традиционное объектно-ориентированное программирование не дает нам какой-либо возможности это сделать. Оно дает нам некоторые инструменты для отображения состояния объектов и назначения им локального поведения. Но у нас нету каких-либо хороших способов описать взаимодействие объектов между собой в рантайме для выполнения сценария использования. Следовательно, системные операции не отображены в коде.
Исходный код != рантайм
В конечном счете, мы все равно пишем код и программируем системные операции каким-то образом. Как мы это делаем? Мы делим их на множество мелких методов и назначаем их куче разных объектов.
![](https://habrastorage.org/storage2/b0c/dc6/b9b/b0cdc6b9b552ee4d36a830e73842776b.png)
Здесь мы видим, что все методы, необходимые для всех сценариев использования, зажаты в этих объектах. Методы для выполнения первого сценария выделены зеленым, для второго — красным. Кроме того, у этих объектов имеются некоторые локальные методы. Они используются синими и красными методами.
Проблема, которая проиллюстрирована на картинке, в том, что исходный код не отображает того, что происходит в рантайме. Исходный код говорит нам о четырех обособленных объектах, с кучей методов в каждом из них. Рантайм говорит нам о наличии четырех, общающихся между собой, объектов, и только малое подмножество этих методов имеет отношение к данному сценарию использования. Эта нестыковка осложняет понимание программ. Исходный код показывает нам одну историю, а рантайм — совершенно другую.
Кроме того, нету никакого способа определить системные операции (сценарии использования) в явном виде, поэтому нам приходится отслеживать все вызовы методов, чтобы получить представление о том, что происходит в программе. У нас нету файла, который мы могли бы открыть, чтобы понять данный сценарий использования. Даже хуже, так как все классы содержат методы для уймы сценариев использования, нам приходится тратить много времени на отфильтровывание нужных нам методов.
DCI спешит на помощь!
DCI (Data, context, interaction) — это парадигма, созданная Trygve Reenskaug (автор MVC шаблона), для разрешения этих проблем.
Первый сценарий использования (DCI)
Давайте взглянем на первый сценарий использования выраженный в DCI стиле.
![](https://habrastorage.org/storage2/57a/442/367/57a442367d93986a33d2af0e9c7c4d25.png)
Здесь мы видим отделение неизменной части системы, содержащее только данные и локальные методы, от данного сценария использования. Все традиционные объектно-ориентированные техники могут быть использованы для моделирования этой неизменной части. В частности, я бы рекомендовал использовать техники проблемно-ориентированного проектирования (Domain-driven design), такие как агрегаты (aggregates) и репозитории (repositories), но здесь отсутствуют контексто-зависимое поведение и взаимодействие, есть только локальные методы.
Как мы моделируем взаимодействие?
У нас в наличии есть новая абстракция для описания взаимодействия — контекст (context). Контекст — это класс, содержащий в себе все роли (roles) для данного сценария использования. Каждая роль представляет собой сослуживца во взаимодействии и обыгрывается неким объектом. Как вы видите, контекстно-зависимое поведение сосредоточено в ролях. Контекст только лишь назначает роли объектам и после этого инициирует взаимодействие.
Второй сценарий использования (DCI)
![](https://habrastorage.org/storage2/110/4e9/109/1104e91095586758b1e2c07807b1d4fc.png)
Я бы хотел отметить, что наши объекты (Object A-D) остаются прежними. Нам не пришлось добавлять каких либо методов для второго сценария использования. Все представленные методы являются фундаментальными, самодостаточными и локальными. Все сценарии использования отдельного поведения были выделены в контекст и роли.
Стоит также отметить, что мы не видим красные и зеленые методы в одно и тоже время. Каждый контекст содержит только необходимые для своего выполнения методы.
Возможно это звучит слишком абстрактно, так что давайте взглянем на пример кода, выполненный на языке Ruby.
Пример кода
Здесь представлен пример Hello World только для DCI. Все заинтересованные в DCI начинают с перевода денег, с одного аккаунта на другой.
Я понимаю, что этот пример слишком упрощен, и так как он настолько прост, он может успешно выражен через сервисы (services), регулярные объекты (regular entities) или функции. Так что взгляните на этот пример, как на иллюстрацию того, как вы должны структурировать ваш код.
Так как мы говорим о переводе денег с одного аккаунта на другой, то нам понадобится хранить информацию об аккаунтах каким-нибудь способом. За это отвечает класс Account. Он хранит информацию о балансе и списке транзакций.
class Account
def decrease_balance(amount); end
def increase_balance(amount); end
def balance; end
def update_log(message, amount); end
def self.find(id); end
end
Как вы видите, все методы здесь локальны и контекстно-независимы. Аккаунт ничего не знает о переводе денег. Он только ответственен за пополнение баланса и считывания денег с него. Логика перевода денег находится в контексте:
class TransferringMoney
include Context
def self.transfer source_account_id, destination_account_id, amount
source = Account.find(source_account_id)
destination = Account.find(destination_account_id)
TransferringMoney.new(source, destination).transfer amount
end
attr_reader :source_account, :destination_account
def initialize source_account, destination_account
@source_account = source_account.extend SourceAccount
@destination_account = destination_account.extend DestinationAccount
end
def transfer amount
in_context do
source_account.transfer_out amount
end
end
...
end
Я запрашиваю два аккаунта из базы данных, затем создаю экземпляр контекста и вызываю метод transfer. Вы наверно заметили, что я передаю два аккаунта в конструктор, а сумму перевода — в метод transfer. Этим я хочу показать какие объекты являются актерами в данном взаимодействии, а какие только данными. Аккаунты — это актеры, у них есть поведение, а сумма перевода — это уже данные.
Далее я назначаю роли объектам аккаунтов в моем конструкторе. Я обучаю эти объекты данных быть аккаунтом-источником и аккаунтом-получателем.
В конце я инициирую взаимодействие вызовом метода transef_out у аккаунта-источника. В этом примере контекст только инициирует взаимодействие, но в некоторых сложных случаях он может так же координировать актеров.
А теперь давайте взглянем на реализацию ролей:
class TransferringMoney
include Context
...
def transfer amount
...
end
module SourceAccount
include ContextAccssor
def transfer_out amount
raise "Insufficient funds" if balance < amount
decrease_balance amount
context.destination_account.transfer_in amount
update_log "Transferred out", amount
end
end
module DestinationAccount
include ContextAccssor
def transfer_in amount
increase_balance amount
update_log "Transferred in", amount
end
end
end
Сначала я проверяю, что аккаунт-источник имеет достаточно денег, затем я вычитаю из баланса сумму перевода. После этого я вызываю аккаунт-получатель через переменную контекста, для того чтобы сообщить ему, чтобы тот получил денег.
Обратите внимание на несколько интересных вещей:
- Разделение неизменного поведения и контекстно-зависимого. Класс аккаунта знает только как манипулировать своими данными. Все проверки, вся бизнес-логика в контексте.
- Роли обращаются к другим сослуживцам через переменную контекста. Еще раз, это делается для отделения актеров от данных. Если бы я передавал всё в качестве аргументов, откуда я узнал бы кто актер, а кто нет? По этой причине обращаться к актерам следует через переменную контекста, а все объекты данных передавать в качестве аргументов.
Контекст и обе роли:
class TransferringMoney
include Context
def self.transfer source_account_id, destination_account_id, amount
source = Account.find(source_account_id)
destination = Account.find(destination_account_id)
TransferringMoney.new(source, destination).transfer amount
end
attr_reader :source_account, :destination_account
def initialize source_account, destination_account
@source_account = source_account.extend SourceAccount
@destination_account = destination_account.extend DestinationAccount
end
def transfer amount
in_context do
source_account.transfer_out amount
end
end
module SourceAccount
include ContextAccssor
def transfer_out amount
raise "Insufficient funds" if balance < amount
decrease_balance amount
context.destination_account.transfer_in amount
update_log "Transferred out", amount
end
end
module DestinationAccount
include ContextAccssor
def transfer_in amount
increase_balance amount
update_log "Transferred in", amount
end
end
end
Что мы имеем
Локальность
DCI решает проблему традиционного объекто-ориентированного программирования, когда алгоритм размазан среди множества разных файлов. Если вы захотите узнать, как реализован какой-либо сценарий использования, вам нужно будет открыть всего лишь один файл.
Фокусировка
Контекст содержит только методы являющихся частью сценария выполнения, которую он представляет. Так что вам не придется сканировать десятки, а то и сотни методов, не имеющих ничего общего с проблемой над которой вы работаете.
“Что из себя представляет система” и “Что система делает”
Все объекты данных и их локальные методы выражают собой “Что из себя представляет система”. Обычно, данная часть системы неизменна. Контекстно-зависимое, быстро изменяющееся поведение представляет из себя “Что система делает”. Разделение неизменных частей от быстро изменяющихся — одно из необходимых условий для построения стабильного ПО. И DCI обеспечивает это разделение:
- Данные в DCI показывают нам все о содержимом объекта и ничего о его соседях (“Что из себя представляет система”)
- Контекст в DCI показывает нам все о сети взаимодействующих объектов и ничего об их содержимом (“Что система делает”)
Исходный код == Рантайм
Также исходный код совпадает с рантаймом. Рантайм показывает нам, что у нас в наличии два аккаунта и сумма перевода. Вот что вы увидите если откроете контекст.
Явно выраженные роли
Основное преимущество DCI состоит в явно выраженных ролях. Достаточно много проектировщиков сходятся во мнении, что объекты сами собой не имеют обязанностей, это удел ролей. Например, возьмите меня как пример объекта. Я обладаю следующими свойствами: я родился в России, мое имя Виктор, мой вес в районе 65кг. Могут ли эти свойства действительно выражать некие высокоуровневые обязательства? Они не могут. Но когда я возвращаюсь домой и начинаю играть роль мужа, я становлюсь ответственен за все супружеские дела. Итак, объекты играют роли. Тот факт, что роли не стоят во главе угла традиционного объекто-ориентированного программирования, неправилен.
Источники
Собственно сама статья:
Если вас заинтересовала данная идея, советую вам посмотреть следующие ресурсы:
- DCI in Ruby
- DCI — Data Context Interaction (A New Role-Based Paradigm for specifying collaborating objects)
Если вы предпочитаете чтение книг, то могу вам порекомендовать эти три книги:
- Clean Ruby от Jim Gay. Эта книга удовлетворяет потребность в практическом представлении DCI для рубистов. Она еще в разработке, но уже выглядит многообещающе.
- Lean Architecture: for Agile Software Development от James O. Coplien and Gertrud Bjørnvig. Эта не только первая книга на рынке по стройной архитектуре и гибкой разработке, но она так же разъясняет разницу между этими двумя мощными подходами и показывает как они могут быть объединены. Это также первая книга содержащая новую архитектуру ПО от Trygve Reenskaug, названную DCI: Data, Context, Interaction.
- Object Design: Roles, Responsibilities, and Collaborations от Rebecca Wirfs-Brock and Alan McKean. Так как книга была выпущена в 2002 году, то она не содержит информации по DCI. Но в ней присутствует хороший материал по проектированию объектов, использованию ролей, и моделированию сослуживцев. Эти темы очень близко стоят к центральными идеям DCI.