Pull to refresh

Comments 58

Имхо, по большей части все перечисленное давно реализовывалось в ОО через архитектуру SOA. Среди ее принципов и контракты (то, что автор называет ролями) и loose coupling (с вынесением сервис-логики) и т.п.
Интересно. Но как в такую модель вписывается иерархия объектов? Имеет ли право объект напрямую взаимодействовать с объектами, лежащими в его полях? Или для такого взаимодействия тоже нужно создавать контекст?
Поднимал эту тему в прошлую субботу на devmeetups.ru в Красноярске. Спасибо за интересные ссылки.
Очень прикольно. Как я понимаю, это пока на уровне паттернов реализуется… А прикольно было бы иметь ЯП с конструкциями типа:

class MyClass { ... }
role MyRole extends MyClass { ... }
context MyContext uses MyClass as MyRole, MyClass2 as MyRole2 { ... }
Вы можете создать DSL на руби, например и получить там все эти конструкции.
Я бы поспорил о необходимости такого расширения языка.
Хотя похожие опыты были — тот же SmallTalk
fork, мне кажется, что для ролей можно было вложенные унаследованные классы.

class User #< ActiveRecord::Base class Author < User #... end class Reader < User # ... end end
Вы предлагаете наследовать роль от модели, что неверно, потому что роль может быть дана совершенно разным объектам. Например объекты Человек, Собака, Лошадь, роль — Бегун. Очень общий пример, но думаю мысль ясна.

В руби ваш подход лучше реализовать через модули
class User
  include Author
  ...
end


но и это противоречит DCI, роль назначается в контексте.
Спасибо, почитал по ссылкам о DCI более подробно — понял что к чему. Кстати, спасибо за статью.
Но тогда вам придется писать реализацию роли «Бегун» отдельно для Человека, Собаки и Лошади вместо того чтобы выделить общее поведение в базовом классе. И тогда уже роли обретут базовый класс «Бегун» с наследниками «БегунЧеловек», «БегунСобака», «БегунЛошадь». Видите как разрослась уже иерархия?
Нет, не придется. Отступление: я говорю в рамках руби. Чтобы Быть бегуном у всех должны быть ноги и все. А в роли Бегун у нас просто определена функция реализующая бег через ноги.
Я не силён в Ruby, вы имеете в виду что перечисленные сущности не имеют общего предка, но имеют некий одинаковый признак (метод) ноги? Ну тогда да, аналог в C++ можно будет реализовать шаблонами (template).
Почитайте пути реализации на разных языках. Для плюсов как раз используются шаблоны, вы правы.
на плюсах как-то так:

class MyRole  
{
public:
  MyRole(const MyClass&); // не-explicit конструктор!
...
}

class MyRole2 ....

class Context
{
public:
  Context(const MyRole&, const MyRole2&);
...
}
...
MyClass obj1; MyClass2 obj2;

Context context(obj1, obj2); 


Передаем в конструктор Context ссылки на объекты сущностей (счетов), а по-факту создаются нужные роли.
То же самое в ряде случае можно и нужно сделать на шаблонах.
Хм, а не проще использовать интерфейсы, например в java можно было сделать так:

public class Account implements SourceAccount, DestinationAccount {
...
}

public interface SourceAccount {
    transfer_out(double amoun);
}

public interface DestinationAccount {
    transfer_in(double amount);
}

public class TransferringMoney {
    public TransferringMoney(SourceAccount, DestinationAccount) {
    }
}


И тогда возникает вопрос, а нафига тут DCI? Интерфейсы, вот уже как десятки лет, служат нам для определения ролей класса. В общем, я пока не понял в чем фишка этого DCI…
Штука в том, что определенные роли нужны в определенном контексте и не стоит нагружать модель всеми ролями сразу. В парадигме DCI мы присваиваем роль объекту в контексте прямо перед использованием, мы не расширяем класс, а только конкретный объект. Это возвращает нас к думанию объектами, а не классами.

Плюс такого подхода — чистые доменные модели, которые ничего не знают о ролях и роли, которые не привязаны ни к одной из моделей.
Хм, видимо я не сталкивался с такой сложной бизнес логикой, как по мне, в большинстве случаев, выше приведенного кода с интерфейсами хватает за глаза. Есть модель, роли и классы выполняющие операции над ролями. Дальнейший рефакторинг в DCI парадигму будет оверхедом, в случаях, с которыми я сталкивался.
у вас тут нет доступа из ролей в контекст.
Раза 3 прочитал статью и пытался понять, зачем городить такую сложную структуру классов когда можно написать одну функцию вроде:

def transfer source_account_id, destination_account_id, amount
  def transfer_out account, amount
    raise "Insufficient funds" if account.balance < amount
    account.decrease_balance amount
    account.update_log "Transferred out", amount
  end

  def transfer_in account, amount
    account.increase_balance amount
    account.update_log "Transferred in", amount
  end

  transfer_out Account.find(source_account_id), amount
  transfer_in Account.find(destination_account_id), amount
end

Аккаунт — это данные, трансфер — это некоторое действие. Всё просто и понятно. Вся логика локализована в одном месте.
Я что-то не понимаю, да?
Угу, не понимаете :)
В большом проекте в таких функциях потом легко утонуть. К тому же к чему привязана эта функция, где она лежит? Это не ООП подход.

Разделяя функционал на роли и контексты мы улучшаем читаемость кода. Модели — отдельно, действия (роли) — отдельно, события (контексты) — отдельно.

В идеале нужно разделить даже доменные модели и модели хранения данных (ORM) и абстрагировать свою бизнес логику от фреймворка вплоть до выделения ее в отдельную библиотеку.

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

Сделайте класс AccountOperations с соответствующими методами.

Честно говоря, пример не удачный, не раскрывает преимуществ DCI. Описанное решение получилось громоздким и ни разу не более понятным, чем это
Кажется, что замкнутые функции недоступны снаружи, поэтому их невозможно затестить. Вытаскивание наружу в ооп стиле приведет к появлению сервисного класса. К сожалению я не знаю руби, поэтому могу ошибаться.
self.transfer source_account_id, destination_account_id, amount

Выглядит как процедура. Поэтому кажется, что то ли отсылка к ооп протянула за уши, то ли пример для иллюстрации выбран не удачно.
Интересно, как подобное проворачивать в том же .NET? И несколько смущает факт, что в рамках контекста мы расширяем объект ролью, а затем… Обратно то её никто не забирает :) Понятно, что в контроллерах с кодом вида account = Account.find(account_id) такое прокатит, но в других случаях?
А затем объект вместе с ролью собирается GC. Контексты должны быть лаконичными и односложными, как и контроллеры. Это идеальная ситуация конечно, но к этому нужно стремиться.
В ruby нельзя раз-extend-ить класс =) В этом небольшая загвоздка DCI получается. В .net можно динамически добавлять методы классу?
А зачем класс? можно extend'ить объект:

?> module Talkable; def say; puts "Hi!"; end; end => nil >> u = User.last; u.extend Talkable; u.say Hi! => nil
Для web приложений имхо это не актуально — объекты каждый раз создаются при запросе

а вообще, DCI через этот способ выглядит коряво
Ну я так понял, что роль у объекта должна быть только во время выполнения контекста, значит ее нужно забирать. Но я согласен, что в веб приложении обычно один запрос — одна операция, хотя бывает всякое.
Нет, в .NET нельзя динамически расширять класс\объект. Можно конечно сгенерировать прокси-наследник :))

Вообще, в итоге мне больше нравится способ, где классы-роли не наследуются от объектов, а принимают их в конструкторе и опираются на public-интерфейс переданных объектов. Тогда и забирать ничего не нужно, и в .NET и прочих языках применимо.
module Talkable
  def say
    puts "Hi!"
  end
end

u = User.last
u.extend Talkable
u.say
=> Hi!
Так в том и штука, чтобы думать объектами, а не классами. И расширять конкретный объект.
А я вот не понял, куда девать функциональность, используемую в нескольких контекстах, если она касается взаимодействия объектов, а не их внутренней жизни?
Либо копипаста, либо наследование ролей, видимо.
В Java все уже давно используют для этого Spring и кучу Service, в которые инжектируют DAO.
Это всё же разные вещи
Смысл тот же, только вместо ролей — интерфейсы
Смысл тот же, только вместо ролей — интерфейсы
Смысл тот же, только вместо ролей — интерфейсы
Где-то выше уже обсудили. Куча Service — это куча service. Здесь идея в едином контексте одной бизнес-транзакции. Один запрос — один контекст. Логика всех ролей собрана в одном месте, а не размазана ровным слоем по всем классам модели.

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

А вот группировать роли и операции по сервисам — это нормально.
Например, может быть AccountingService, с методами generateInvoice(), acceptPayment(), DeliveryService с методами deliver() и confirmDelivery(), TaxService с методом sendTaxReport()

Эти сервисы могут использовать одни и те же DAO, но в разных контекстах. Так что — все очень похоже.
Если бы это было похоже на то, как поступают в мире .NET/Java — этой статьи бы не было, всё же.
Не пойму — так это все же не похоже, или вы сомневаетесь в том, что код с сервисами пишут именно так?
Действительно криво выразился. Я про DCI говорил — не похож он, на мой взгляд, на подход к сервисами. Иначе зачем он был придуман?
А мало придумывают велосипедов? :)
Раз всё ещё придумывают — значит пока недостаточно :)

Понятно, что то, как мы раньше делали — это работает. Иначе бы мы так и не делали :) Но всё же лично мне, например, DCI показалось интересным и имеющим право на существование.
Правильно ли я понимаю что весь сыр-бор заключается по факту только в том, что мы на одну операцию заводим по одному классу (называем его SomeContext вместо SomeService) и парочку доп, классов с суффиксом Role?

Помоему «эволюция ОО парадигмы» это громко сказано. Больше тянет на паттерн, либо на дизайн гайдлайн по типу DDD'шных.
Проблема сама по себе интересная и актуальная, стоит напоминания.
По-моему, вы придираетесь.
Блин, DDoS достал, мне 3 раза показало ошибку, а реально — коммент отправлялся.
UFO just landed and posted this here
это всё было в Common Lisp, а конкретнее — в CLOS, уже больше 20 лет назад.
Обобщенные функции плюс комбинаторы методов.
И выглядело приятнее.
Как мне кажется DCI способен проявить себя в приложениях со сложной и проработанной предметной областью.
Есть множество приложений, для которых DCI излишен.
DCI позволяет перекинуть мост между требованиями к создаваемому приложению (в формате USE CASE) и уже имеющейся предметной моделью. Вы реализуете USE CASES как вложенные Contexts и добавляете поведение к предметным моделям, с использованием Roles (без замутнения модели «случайным» поведением).
Sign up to leave a comment.

Articles