Статьи

Взаимодействие контекста данных: эволюция объектно-ориентированной парадигмы

Эта статья представляет собой практическое введение в DCI (взаимодействие с контекстом данных). Я не собираюсь объяснять всю теорию, стоящую за этим. Вместо этого я собираюсь показать вам, какие проблемы DCI пытается решить и как это можно реализовать в Ruby.

Что ОО делает хорошо

Давайте начнем с рассмотрения некоторых проблем, где традиционное объектно-ориентированное программирование хорошо работает.

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

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

Ruby String — хороший пример объекта, который имеет только локальные операции. Каждая строка имеет массив байтов или символов, и все операции работают с этим массивом. Этот объект самодостаточен; Сотрудничество с другими объектами не требуется. Я не думаю, что у кого-то возникли проблемы с пониманием того, как использовать строку. Это та проблема, для решения которой были созданы ОО-языки.

Что ОО не может сделать

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

Вариант использования 1

Use Case 1

Здесь у нас есть системная операция с четырьмя объектами, разговаривающими друг с другом.

Вариант использования 2

Use Case 2

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

Системные операции не представлены в коде

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

К сожалению, традиционное объектно-ориентированное программирование не дает нам никаких средств сделать это. Это дает нам некоторые инструменты для выражения состояния объектов и привязки локального поведения к этим объектам. Но у нас нет хороших способов описать, как объекты взаимодействуют во время выполнения, чтобы выполнить сценарий использования. Следовательно, системные операции не представлены в коде.

Исходный код! = Время выполнения

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

All Methods

То, что мы видим здесь, это то, что все методы, требуемые всеми вариантами использования, защемлены в этих объектах. Методы, необходимые для выполнения первого варианта использования, обозначены зеленым, а второй вариант — красным. Кроме того, эти объекты имеют несколько локальных методов. Эти локальные методы используются зелеными и красными методами.

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

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

DCI на помощь

DCI — это парадигма, изобретенная Trygve Reenskaug (изобретателем шаблона MVC) для решения этих проблем.

Вариант использования 1 (DCI)

Давайте рассмотрим первый вариант использования, реализованный в стиле DCI.

Use Case 1 (DCI)

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

Как мы моделируем взаимодействия?

У нас есть новая абстракция для описания взаимодействий: контекст. Это класс, включающий все роли для данного варианта использования. Каждая роль является сотрудником во взаимодействии, и он играет объект. Как видите, контекстное поведение сосредоточено в ролях. Контекст просто назначает роли объектам и после этого запускает взаимодействие.

Вариант использования 2 (DCI)

Второй вариант использования реализован в стиле DCI:

Use Case 2 (DCI)

Я хотел бы отметить, что наши объекты (Object AD) остаются прежними. Нам не нужно было добавлять какие-либо методы для поддержки второго варианта использования. Все методы, которые у нас есть, являются фундаментальными, автономными и локальными. Во всех случаях использования специфическое поведение было выделено в контексты и роли.

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

Это может звучать слишком абстрактно, поэтому давайте посмотрим на пример кода, чтобы увидеть, как он может быть реализован в Ruby.

Пример кода

Это привет мир пример для DCI. Все заинтересованные в DCI начинают с перевода денег с одного счета на другой.

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

Поскольку мы говорим о переводе денег с одного счета на другой, нам нужно как-то хранить информацию о счетах. Класс 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

view raw
account.rb
hosted with ❤ by GitHub

Как видите, все методы здесь локальны и не зависят от контекста. Аккаунт ничего не знает о переводе денег. Он отвечает только за увеличение и уменьшение своего баланса. Логика перевода денег в контексте:

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

view raw
context.rb
hosted with ❤ by GitHub

Я извлекаю две учетные записи из базы данных, затем создаю экземпляр контекста и затем вызываю «перевод». Возможно, вы заметили, что я передаю эти два счета конструктору, а сумму — методу transfer . Делая это, я пытаюсь понять, какие объекты являются действующими лицами в этом взаимодействии, а какие — просто данными. Счета — актеры, у них есть поведение. Количество данных.

Затем я назначаю роли объектам учетной записи в моем конструкторе. Я учу эти объекты данных, как быть исходной учетной записью и целевой учетной записью.

Наконец, я запускаю это взаимодействие, вызывая «Transfer_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

view raw
roles.rb
hosted with ❤ by GitHub

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

Здесь есть несколько интересных вещей:
* Разделение между стабильным поведением и контекстным поведением. Account — это класс дампа, который знает только, как манипулировать данными. Все проверки, вся бизнес-логика в контексте.
* Роли получают доступ к другим соавторам через переменную контекста. Еще раз, это сделано, чтобы отделить актеров от данных. Если я передам все как аргумент, как я узнаю, что такое актер, а что нет? Следовательно, все акторы доступны через контекст, а все объекты данных передаются в качестве аргументов.

Контекст и обе роли:

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 говорит все о сети взаимодействующих объектов и ничего об их внутренностях («Что делает система»).

Исходный код == Runtime

Другое дело, что исходный код соответствует среде выполнения. Среда выполнения сообщает нам, что есть два аккаунта и сумма. Это то, что вы видите, когда открываете контекст.

Роли явные

Самое большое, что приносит DCI — это явные роли. Многие дизайнеры сходятся во мнении, что объекты сами по себе не имеют обязанностей — роли есть. Например, возьмите меня в качестве примера объекта. У меня есть следующие свойства: я родился на русском языке; меня зовут Виктор; мой вес около 65 кг. Эти свойства действительно подразумевают некоторые обязанности высокого уровня? Они не Но когда я прихожу домой и начинаю играть роль мужа, я становлюсь ответственным за все вещи этого мужа. Так что объекты играют роли. Тот факт, что роли не являются первоклассными гражданами в традиционной объектной ориентации, просто ошибочен.

Ресурсы

Если вы считаете эту идею интересной, вам следует проверить следующие ресурсы:

Если вы предпочитаете читать книги, я могу порекомендовать вам три книги: