- PVSM.RU - https://www.pvsm.ru -
Слишком часто стала мелькать в западных блогах и твиттере аббревиатура “DCI”. Меня удивил тот факт, что на хабре по данной тематике почти нету информации, лишь в Ruby NoName Podcast S04E09 [1] упоминалось об этом. Любопытство взяло вверх, и я решил узнать об этом загадочном слове побольше. В процессе поиска я наткнулся на хорошую статью, написанную на английском моим земляком, Виктором Савкиным. Данная статья без обильной теории, на практических примерах показывает, что из себя представляет DCI. Далее повествование будет идти от лица Виктора.
Давайте начнем с обзора тех проблем, где традиционное объектно-ориентированное программирование зарекомендовало себя с лучшей стороны.
Объекто-ориентированное программирование показало себя хорошо в управлении состоянием объекта. Классы, поля и свойства — это мощные инструменты, позволяющие вам определить состояние и оперировать им. Так как мы имеем эти средства отображения состояния, встроенные в языки программирования, то мы можем сделать вывод о нем, как во время компиляции, так и во время райнтайма. Во время компиляции мы можем взглянуть на определение объекта класса, а в райнтайме мы можем опросить объект о его полях.
Другая проблема, которая решается достаточно хорошо всеми объекто-ориентированными языками, состоит в отображении операций связанных с состоянием объекта. Такие операции не вовлечены в какую либо совместную работу. Они локальны для владеющего объекта. Мы выражаем локальные операции определением методов класса. Когда мы определяем метод у класса и создаем его экземпляр, мы знаем что он будет иметь этот метод. Это достаточно очевидно.
В качестве хорошего примера объекта, имеющего только локальные операции, можно привести Ruby класс String. Каждый String имеет массив байтов или символов. Все его операции работают с этим массивом. Этот объект независим — ему нет необходимости взаимодействовать с другими объектами. Я сомневаюсь, что кто либо может не понять как использовать String. Для решения такого рода задач и были построены ОО языки.
ОО проигрывает в отображении взаимодействия объектов. Для того чтобы показать что я имею ввиду давайте взглянем на две системные операции(два сценария использования) объектов из одной группы, взаимодействующих друг с другом.
На картинке показана системная операция четырех объектов, общающихся между собой.
А здесь уже представлен другой сценарий использования, другая системная операция, использующая ту же самую группу объектов. Но как вы видите, модель взаимодействия объектов и сообщения уже другие.
Мы видели две системные операции выраженные в Use Case 1 и Use Case 2. Как мы их отображаем в коде? В идеальном случае, я бы хотел иметь возможность открыть один файл и понять модель взаимодействия объектов в сценарии использования, над которым я работаю. Если я работаю над Use Case 1, я ничего не хочу знать про Use Case 2. Вот что я считаю успешным отображением сценариев использования в коде.
К сожалению, традиционное объектно-ориентированное программирование не дает нам какой-либо возможности это сделать. Оно дает нам некоторые инструменты для отображения состояния объектов и назначения им локального поведения. Но у нас нету каких-либо хороших способов описать взаимодействие объектов между собой в рантайме для выполнения сценария использования. Следовательно, системные операции не отображены в коде.
В конечном счете, мы все равно пишем код и программируем системные операции каким-то образом. Как мы это делаем? Мы делим их на множество мелких методов и назначаем их куче разных объектов.
Здесь мы видим, что все методы, необходимые для всех сценариев использования, зажаты в этих объектах. Методы для выполнения первого сценария выделены зеленым, для второго — красным. Кроме того, у этих объектов имеются некоторые локальные методы. Они используются синими и красными методами.
Проблема, которая проиллюстрирована на картинке, в том, что исходный код не отображает того, что происходит в рантайме. Исходный код говорит нам о четырех обособленных объектах, с кучей методов в каждом из них. Рантайм говорит нам о наличии четырех, общающихся между собой, объектов, и только малое подмножество этих методов имеет отношение к данному сценарию использования. Эта нестыковка осложняет понимание программ. Исходный код показывает нам одну историю, а рантайм — совершенно другую.
Кроме того, нету никакого способа определить системные операции (сценарии использования) в явном виде, поэтому нам приходится отслеживать все вызовы методов, чтобы получить представление о том, что происходит в программе. У нас нету файла, который мы могли бы открыть, чтобы понять данный сценарий использования. Даже хуже, так как все классы содержат методы для уймы сценариев использования, нам приходится тратить много времени на отфильтровывание нужных нам методов.
DCI (Data, context, interaction) — это парадигма, созданная Trygve Reenskaug (автор MVC шаблона), для разрешения этих проблем.
Давайте взглянем на первый сценарий использования выраженный в DCI стиле.
Здесь мы видим отделение неизменной части системы, содержащее только данные и локальные методы, от данного сценария использования. Все традиционные объектно-ориентированные техники могут быть использованы для моделирования этой неизменной части. В частности, я бы рекомендовал использовать техники проблемно-ориентированного проектирования (Domain-driven design), такие как агрегаты (aggregates) и репозитории (repositories), но здесь отсутствуют контексто-зависимое поведение и взаимодействие, есть только локальные методы.
У нас в наличии есть новая абстракция для описания взаимодействия — контекст (context). Контекст — это класс, содержащий в себе все роли (roles) для данного сценария использования. Каждая роль представляет собой сослуживца во взаимодействии и обыгрывается неким объектом. Как вы видите, контекстно-зависимое поведение сосредоточено в ролях. Контекст только лишь назначает роли объектам и после этого инициирует взаимодействие.
Я бы хотел отметить, что наши объекты (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 состоит в явно выраженных ролях. Достаточно много проектировщиков сходятся во мнении, что объекты сами собой не имеют обязанностей, это удел ролей. Например, возьмите меня как пример объекта. Я обладаю следующими свойствами: я родился в России, мое имя Виктор, мой вес в районе 65кг. Могут ли эти свойства действительно выражать некие высокоуровневые обязательства? Они не могут. Но когда я возвращаюсь домой и начинаю играть роль мужа, я становлюсь ответственен за все супружеские дела. Итак, объекты играют роли. Тот факт, что роли не стоят во главе угла традиционного объекто-ориентированного программирования, неправилен.
Собственно сама статья:
Если вас заинтересовала данная идея, советую вам посмотреть следующие ресурсы:
Если вы предпочитаете чтение книг, то могу вам порекомендовать эти три книги:
Автор: Timrael
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/ruby/14794
Ссылки в тексте:
[1] Ruby NoName Podcast S04E09: http://ruby.rpod.ru/275445.html
[2] Data Context Interaction: The Evolution of the Object Oriented Paradigm: http://rubysource.com/dci-the-evolution-of-the-object-oriented-paradigm/
[3] DCI in Ruby: http://dci-in-ruby.info
[4] DCI — Data Context Interaction (A New Role-Based Paradigm for specifying collaborating objects): http://fulloo.info
[5] Clean Ruby от Jim Gay.: http://clean-ruby.com/
[6] Lean Architecture: for Agile Software Development от James O. Coplien and Gertrud Bjørnvig.: http://www.leansoftwarearchitecture.com/
[7] Object Design: Roles, Responsibilities, and Collaborations от Rebecca Wirfs-Brock and Alan McKean.: http://www.amazon.com/Object-Design-Roles-Responsibilities-Collaborations/dp/0201379430
Нажмите здесь для печати.