Статьи

Реализация DCI в Qi4j

В прошлом году я, Трюгве Реенскауг и Джим Коплиен представили трек Оредева о DCI: данные, контекст, взаимодействие. DCI — это новый способ взглянуть на то, как конструировать объектно-ориентированные приложения, и который фокусируется на создании кода, соответствующего ментальной модели пользователя. Для введения в DCI я бы рекомендовал чтение этой статьи на Artima, а также , что вы смотрите на три презентации от Oredev. Это должно дать вам достаточное представление о том, что именно DCI пытается достичь.

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

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

Роли

Одна из основных идей DCI состоит в том, что объекты не состоят из отдельных классов, то есть POJO, но вместо этого используют роли для создания функциональности. Точнее говоря, когда я говорю «объект», это относится к сущностям в терминах DDD, а не к значениям или чему-либо еще. В условиях корпоративной архитектуры с постоянными данными в основном это объекты, которые мы хотим разделить на роли, где каждая роль имеет свое назначение. В Qi4j эти роли выполняются Mixins. В Scala их можно назвать чертами. В простой Java нет аналога, что затрудняет правильную реализацию DCI в Java, если вы не используете Qi4j.

Данные

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

контекст

В DCI контекст содержит сопоставления ролей в взаимодействии с конкретными экземплярами объекта, а также имеет взаимодействия, которые могут быть выполнены в этом сопоставлении. Контексты могут быть реализованы с использованием POJO или TransientComposites в Qi4j. POJO проще, но TransientComposites позволяют вам использовать составные контексты, что является более мощным. Для простоты я буду придерживаться версии POJO здесь.

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

взаимодействия

Наконец, взаимодействия реализуются как простые методы в контексте. Они могут принимать аргументы, а затем искать сопоставленные роли в контекстной карте и запускать метод домена. Важным моментом здесь является то, что методы взаимодействия должны соответствовать любым действиям, которые вы выполняете в своем пользовательском интерфейсе, чтобы код соответствовал ментальной модели пользователей. Это одна из ключевых целей здесь. Если вы измените свой пользовательский интерфейс на что-то другое, вам необходимо соответствующим образом изменить контексты и взаимодействия, чтобы было легко отследить, что в пользовательском интерфейсе отображается с каким кодом в настройке DCI.

Модель предметной области

Для целей этого поста в блоге я буду использовать простую модель домена. У нас есть три объекта: проект, пользователь и задача. Задачи реализуют Назначаемую роль. Проект и Пользователь реализуют роль Назначений. Пользователи могут быть назначенными. Затем у нас есть основной контекст с взаимодействиями, называемый InboxContext, у которого есть один метод (/ взаимодействие): «назначить (назначаемое)». И пользователи, и проекты имеют папку «Входящие» с задачами, и именно они мы хотим назначить пользователям. Но они могут «принадлежать» либо самому пользователю, либо конкретному проекту, поэтому возникает необходимость в отдельной роли назначений, чтобы наше понятие «назначение» не было привязано к «заданному списку задач пользователей, одна из них» может быть назначен пользователю «а точнее» с учетом коллекции назначений назначаемых лиц, выбрать один и назначить назначенному лицу «.Таким образом, взаимодействие и контекст полностью отделены от реальных классов, и нам нужны только соответствующие роли.

какой в ​​этом смысл? Ну, во-первых, это позволяет разработчику сосредоточиться только на классах ролей, пытаясь понять, как работает назначение. Пользователь может иметь имя пользователя / пароль, профиль пользователя, настройки доставки электронной почты и многое другое, но если бы вам пришлось знать об этом, просто рассматривая обработку назначений, это было бы довольно грязно. И именно так сегодня работает разработка POJO.

Кроме того, если позже я захочу реализовать то же самое взаимодействие, но с «Вычислением» вместо «Задачи» и «ClusterNode» вместо «Пользователя», я мог бы повторно использовать всю подсистему назначений, не меняя ничего. Единственное, что меняется, — это сопоставление объектов с ролями.

Выполнение взаимодействия

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

InteractionContext map = new InteractionContext();
RootContext context = assembler.objectBuilderFactory().newObjectBuilder( RootContext.class ).use(stack ).newInstance();
InboxContext inboxContext = context.user( userId ).inbox();

Я создаю новую контекстную карту для этого взаимодействия и создаю новый RootContext. Это символизирует корень всех контекстов в моем приложении и в основном содержит методы для перехода к подконтекстам. Я передаю карту, чтобы RootContext мог передать ее во время вызова user (), который создаст подконтекст UserContext, который имеет все взаимодействия и подконтексты для работы с выбранным пользователем. Я передаю userId, чтобы метод user () мог выполнять поиск. Если эта реализация DCI используется в настройке REST API, то поиск по контексту будет в основном отображаться на URL, поэтому «userId» будет одной частью URL-адреса, на который ссылается. Вы можете представить вышеприведенное сопоставление «/ administrator / inbox» в URL.

Метод user () добавит данного пользователя к контекстной карте, а затем создаст новый подконтекст с расширенной картой. Вот как это выглядит:

public class RootContext
extends Context
{
public UserContext user(String id)
{
UserEntity user = module.unitOfWorkFactory().currentUnitOfWork().get( UserEntity.class, id );
context.playRoles( user, Assignee.class, Assignments.class);
return subContext( UserContext.class );
}
}

«Context» — это базовый класс, который имеет InteractionContext в переменной «context» и метод «subContext» для простого создания новых контекстов с этой картой контекста. То, что я делаю здесь, — это поиск UserEntity с заданным идентификатором из Qi4j UnitOfWork, а затем регистрация его на карте с заданными ролями. У объекта уже есть эти роли, поэтому единственное, что здесь происходит, это то, что карта знает, что если кто-то запрашивает объект, играющий роль «Назначенный», он знает, что возвращать.

Как только контекст был найден, настало время вызвать взаимодействие с аргументами:

inboxContext.assignTo( task );

Поскольку у контекста есть доступ к контекстной карте, вышеуказанному методу не нужно знать, кому его назначить. Это дается контекстной картой! Реализация assignTo () заключается в следующем:

public class InboxContext
extends Context
{
public void assignTo( Assignable assignable )
{
context.role( Assignments.class).assignTo( assignable, context.role( Assignee.class ));
}
}

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

Если мы следуем методу assignTo () и видим, что он делает, это выглядит так:

class AssignmentsMixin
implements Assignments
{
@This
AssignmentsData data;

public void assignTo( Assignable assignable, Assignee assignee )
{
assignable.assignTo( assignee );
data.assignments().add( assignable );
}

public Iterable<Assignable> assignments()
{
return data.assignments();
}
}

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

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

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

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

InteractionContext stack = new InteractionContext();
RootContext context = assembler.objectBuilderFactory().newObjectBuilder( RootContext.class ).use( stack ).newInstance();
context.user( user.identity().get() ).project( project.identity().get() ).inbox().assignTo( task2 );

В этом случае вместо того, чтобы позволить пользователю играть роли как Назначителя, так и Назначения, пользователь будет использоваться только для Получателя. Проект, который ищется в вызове project (), будет привязан к Assignments, поэтому, как только мы получим метод assignTo () в InboxContext, алгоритм по существу скажет: «Назначить задачу пользователю в назначении Projects». И для этого нам не нужно было менять код при обработке назначений. Единственное, что изменилось, это то, какие объекты были связаны с какими ролями! Позвольте мне услышать, как вы говорите: «СЛАДКИЙ!»

То, что мы видели здесь, — это простой пример того, как DCI может быть реализован в Qi4j, и который предоставляет все необходимые ключевые компоненты: роли, данные, контексты и взаимодействия.

В чем смысл?

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

И что тогда? Главное, чего я хочу достичь — это удобочитаемость кода, удобство сопровождения кода и простота изменений. Если вы вместо этого работаете с подходом POJO, помещая весь код для этих ролей в один класс, это усложняет все вышеперечисленное. Как только вы достигнете определенного размера вашего проекта, наличие всего кода в одном месте сделает его менее читабельным, менее поддерживаемым и сложным для изменения. Гораздо сложнее.

Еще одним ключевым преимуществом такой работы является то, что она действительно позволяет использовать ее на совершенно новом уровне. Вы можете создавать роли, контексты и взаимодействия, которые реализуют конкретный сценарий использования, а затем снова и снова использовать это в различных частях вашей доменной модели. Полезно это или нет, зависит от размера и сложности вашего проекта. Если вы проводите небольшой консультативный концерт с одним разработчиком, то это, вероятно, не окупится. Однако, если вы работаете с системой среднего размера или выше, с несколькими разработчиками, тогда этот подход значительно упростит ее сборку и обслуживание, и он будет намного лучше справляться с новыми требованиями, поскольку меньше нужно учитывать весь код, который уже есть. У сущности легко может быть 20+ миксинов без особых проблем,поскольку весь код четко разделен на специализированные роли. Сделайте это с подходом POJO, и вы облажались. В одном и том же классе вы получите множество методов и состояний, и будет практически невозможно определить, какой код принадлежит к какому варианту использования. Излишне говорить, что это также портит возможность повторного использования, поскольку нет возможности взять часть этого POJO и использовать его в другом месте.

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

Полный код вышеперечисленного доступен в репозитории qi4j-samples Git, который вы можете найти в Интернете здесь . Зайдите сюда, чтобы узнать, как это проверить. Код включает в себя 3 версии того, как это сделать с помощью POJO, и 2 версии в Qi4j с использованием DCI.

С http://www.jroller.com/rickard