Пару месяцев назад я натолкнулся на концепцию 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.