Статьи

DCI в AspectJ

Пару месяцев назад я натолкнулся на концепцию DCI (Data, Context, Interaction), которая показалась мне довольно интересной. Поскольку Java не поддерживает черты, реализация DCI в Java немного неловкая. Мне удалось найти несколько реализаций: простая реализация Java DCI, основанная на прямом использовании динамических прокси, реализация, основанная на платформе Qi4J, а затем недавно другая, использующая DCI Tools для библиотеки Java . Инструменты Qi4J и DCI для Java, в свою очередь, полагаются на рефлексию и динамические прокси, в частности. Когда вы посмотрите на код, вы обнаружите, что каждый подход включает в себя много кода «scaffolding», который помогает преодолеть недостаток Java. Например, давайте посмотрим на образец Qi4J:

SingletonAssembler assembler = new SingletonAssembler() {
public void assemble(ModuleAssembly module) throws AssemblyException {
module.addEntities(TaskEntity.class, UserEntity.class);
module.addServices(MemoryEntityStoreService.class, UuidIdentityGeneratorService.class);
}
};

UnitOfWork uow = assembler.unitOfWorkFactory().newUnitOfWork();

TaskEntity task = uow.newEntity(TaskEntity.class);

EntityBuilder<UserEntity> builder = uow.newEntityBuilder(UserEntity.class);
builder.instanceFor(AssigneeData.class).name().set("Rickard");
UserEntity user = builder.newInstance();

{
InboxContext inbox = new InboxContext(user, user);
inbox.assign(task);
}

uow.complete();

На первый взгляд не очень понятно, что здесь происходит. По сути, Задача назначается Пользователю с именем Rickard в контексте Inbox. Хотя читаемость кода можно улучшить, используя фабрики для создания экземпляров сущностей, вы, вероятно, в конечном итоге напишите много шаблонного кода внутри фабрики. Также, как упоминалось ранее, все подходы в значительной степени зависят от отражения, которое может повлиять на производительность приложения. И наконец, сущности не являются обычными объектами Java, поэтому, например, сериализация сущностей или интеграция со средой Spring не так просты. Учитывая это, я начал искать различные способы реализации DCI и нашел идею реализации ролей как привлекательных аспектов.

Реализация AspectJ

В следующем разделе я покажу пример, как реализовать DCI в AspectJ. Пример представляет собой переписанный пример реализации DCI в Qi4J, который можно скачать отсюда и который хорошо описан в презентации Рикарда Эберга, В этом примере показан пользователь и проект, у которых есть задача, назначенная в контексте папки «Входящие». Пользователь, Проект и Задача — это объекты, для которых Пользователю может быть назначено несколько Задач. Назначенные задачи также могут быть частью проекта. Теперь вы, наверное, поняли, что сущности играют роли. В этом случае у пользователя есть роль Assignee, которая представляет кого-то, кто будет работать над задачей (или заданием в целом). Он также имеет роль назначений, т.е. пользователю может быть назначено несколько задач. С другой стороны, задача имеет только назначаемую роль, поэтому ее можно назначить объекту, в данном случае — пользователю или проекту. Как и объект «Пользователь», проект также выполняет роль «Назначения», поэтому он может содержать назначения — в данном случае «Задачи».

Давайте посмотрим на код. Роли представлены интерфейсами.

public interface Assignments {

void assign(Assignable assignable, Assignee assignee);

List<Assignable> assignments();
}

public interface Assignable {

void assignTo(Assignee assignee);
}

public interface Assignee {

String assigneeName();
}

Объекты представлены аннотированными классами.

@AssignmentsRole
public class Project {
}

@AssignableRole
public class Task {
}

@AssignmentsRole
@AssigneeRole
public class User {

public User(String name) {
setName(name);
}
}

Код выше такой же, как

public class User implements Assignments, Assignee {
}

где интерфейсы реализуются не классом User, а аспектами. (Я лично предпочитаю, чтобы аннотации были подсказкой для компилятора AspectJ, а не полагались на реализацию интерфейса. Это дает программисту, читающему код, лучшую визуальную подсказку о том, что интерфейсы реализуются аспектами.) Аннотации служат маркерами, поэтому AspectJ Компилятор знает, какие аспекты он должен применять к классу. Сами аннотации являются простыми аннотациями с политикой хранения по умолчанию (RetentionPolicy.CLASS) и могут применяться к объявлениям классов.

@Target(ElementType.TYPE)
public @interface AssigneeRole {
}

Теперь давайте перейдем к реализации ролей. Как упоминалось ранее, они реализованы как аспекты.

aspect AssignableImpl {

declare parents: (@AssignableRole *) implements Assignable;

private Assignee Assignable.assignee;

public void Assignable.assignTo(Assignee assignee) {
// No object schizophrenia - this reference is to the entity itself.
this.assignee = assignee;
System.out.println(assignee.assigneeName());
}
}

В части объявления родителей говорится, что интерфейс Assignable будет реализован любым классом, помеченным аннотацией @AssignableRole. Ниже объявить родителей является фактической реализацией интерфейса. Переменная assignee и метод assignTo станут членами объекта (ов), помеченных аннотацией @AssignableRole.

Реализация других ролей происходит по той же схеме.

aspect AssignmentsImpl {

declare parents: (@AssignmentsRole *) implements Assignments;

private List<Assignable> Assignments.assignments = new ArrayList<Assignable>();

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

public List<Assignable> Assignments.assignments() {
return assignments;
}
}

Реализация контекста «Входящие», где выполняется назначение, приведена ниже.

public class InboxContext {

private Assignments assignments;
private Assignee assignee;

public InboxContext(Assignments assignments, Assignee assignee) {
this.assignments = assignments;
this.assignee = assignee;
}

public void assign(Assignable assignable) {
assignments.assign(assignable, assignee);
}
}

А теперь давайте все вместе.

Task task = new Task();
User user = new User("Rickard");

InboxContext inbox = new InboxContext(user, user);
inbox.assign(task);

System.out.println("Nr of assignments: " + user.assignments().size());

Project project = new Project();
Task task2 = new Task();

inbox = new InboxContext(project, user);
inbox.assign(task2);

System.out.println("Nr of assignments: " + project.assignments().size());

Я считаю, что это намного чище, чем код Qi4J. Кроме того, сущности в основном являются гражданами Java первого класса, поэтому их можно, например, сериализовать так же, как обычные классы (см. Пример сериализации в исходном коде), или использовать в качестве бинов Spring. Ниже приведен переписанный код с использованием среды Spring.

Файл конфигурации Spring spring.xml:

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="userRickard" class="net.sourceforge.dciaspectj.example1.entity.User">
<constructor-arg index="0" value="Rickard" />
</bean>

<bean id="project" class="net.sourceforge.dciaspectj.example1.entity.Project" />

<bean id="task1" class="net.sourceforge.dciaspectj.example1.entity.Task" />

<bean id="task2" class="net.sourceforge.dciaspectj.example1.entity.Task" />

<bean id="userInbox" class="net.sourceforge.dciaspectj.example1.context.InboxContext">
<constructor-arg index="0" ref="userRickard" />
<constructor-arg index="1" ref="userRickard" />
</bean>

<bean id="projectInbox" class="net.sourceforge.dciaspectj.example1.context.InboxContext">
<constructor-arg index="0" ref="project" />
<constructor-arg index="1" ref="userRickard" />
</bean>
</beans>

Код загружает контекст приложения, определенный в spring.xml. Затем сущности и InboxContext ищутся и получаются из этого контекста приложения Spring.

ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:spring.xml");

Task task = (Task) ctx.getBean("task1");
InboxContext inbox = (InboxContext) ctx.getBean("userInbox");

inbox.assign(task);

User user = (User) ctx.getBean("userRickard");
System.out.println("Nr of assignments: " + user.assignments().size());

Task task2 = (Task) ctx.getBean("task2");
inbox = (InboxContext) ctx.getBean("projectInbox");

inbox.assign(task2);

Project project = (Project) ctx.getBean("project");
System.out.println("Nr of assignments: " + project.assignments().size());

 

Роль Полиморфизм

Я не уверен, что подтип ролей все еще входит в сферу оригинальной идеи DCI, но вы определенно можете поэкспериментировать с этим. Например, может быть полезно иметь роль CompositeAssignee, которая будет представлять группу Asignees, и в то же время это будет сам Assignee. Хорошим кандидатом на эту роль будет группа пользователей, образующая команду.

CompositeAssignee расширяет интерфейс Assignee.

public interface CompositeAssignee extends Assignee {

List<Assignee> assignees();
}

Ролевая аннотация и реализация роли ниже.

@Target(ElementType.TYPE)
public @interface CompositeAssigneeRole {
}

aspect CompositeAssigneeImpl {
declare parents: (@CompositeAssigneeRole *) implements CompositeAssignee;

private List<Assignee> CompositeAssignee.assignees = new ArrayList<Assignee>();

// You can also override a method if needed.
public String CompositeAssignee.assigneeName() {
StringBuilder name = new StringBuilder(super.assigneeName());
name.append(":");
for (Assignee assignee : assignees) {
name.append(" ");
name.append(assignee.assigneeName());
}
return name.toString();
}

public List<Assignee> CompositeAssignee.assignees() {
return new ArrayList<Assignee>(assignees);
}

public void CompositeAssignee.addAssignee(Assignee assignee) {
assignees.add(assignee);
}
}

В этом случае объект группы имеет роль CompositeAssignee.

 @AssignmentsRole
@CompositeAssigneeRole
public class Group {

public Group(String name) {
setAssigneeName(name);
}
}

 И вот использование:

Task task = new Task();
Group team = new Group("DCI Team");
team.addAssignee(new User("Rickard"));
team.addAssignee(new User("Jim"));
team.addAssignee(new User("Trygve"));

InboxContext inbox = new InboxContext(team, team);
inbox.assign(task);

System.out.println("Nr of assignments: " + team.assignments().size());

Вы можете скачать исходный код здесь . Пример, описанный в этой статье, находится в папке example1. Переписанный пример, описанный в Простая реализация Java DCI, можно найти в папке example2.

Ресурсы