Hi guys,
On the DCI mailing list, there has been a lot of discussion about how to
actually implement roles, and what is the "proper way" and what is the
"practical way", and so on. Lately I came up with a variant that seems
to fulfill all the semantical and syntactical criteria, with the only
problem being that the roles are not actually part of the object, but
instead are implemented as wrappers. So, I wanted to run this by you
guys and get your views on this.
Here's the old way: an Entity would have Data, i.e. Properties and
Associations, and with methods to modify these formulated as imperative
Commands (i.e. "changeFoo"). These can internally be handled by just
doing it (i.e. check if command is ok, and if yes, apply to state), or
it can be done by using event sourcing (i.e. do command check and then
call event method that in turn does the state change). Here's an example
from Streamflow:
@Mixins(Groups.Mixin.class)
public interface Groups
{
Group createGroup( String name );
... methods removed for brevity ...
interface Data
{
@Aggregated
ManyAssociation<Group> groups();
}
interface Events
{
Group createdGroup( @Optional DomainEvent event, String id );
void addedGroup( @Optional DomainEvent event, Group group );
void removedGroup( @Optional DomainEvent event, Group group );
}
class Mixin
implements Groups, Events
{
@This Data data;
@Service
IdentityGenerator idGen;
@Structure
Module module;
public Group createGroup( String name )
{
Group group = createdGroup(null,
idGen.generate(GroupEntity.class));
group.changeDescription( name );
addGroup(group);
return group;
}
public Group createdGroup(@Optional DomainEvent event, String id)
{
return
module.unitOfWorkFactory().currentUnitOfWork().newEntity(Group.class, id);
}
.. the rest of the methods removed for brevity ...
}
}
---
The above is then extended by an entity interface that needs "Group
management", in my case an OrganizationalUnitEntity.
So once you have a "Groups" entity, you can invoke the commands
directly. With DCI, the "createGroup" method would only be called from a
GroupsContext where the interaction exist.
The new way: since GroupsContext is basically the only place from which
you would do this, and it represents a particular usecase, the
"createGroup" method actually belongs in a role in the context, which is
only valid *from within that context*. So, the way to do this is to
implement GroupsContext as a class and then do the Groups commands thing
as an inner class that wraps a Groups object that has Events+Data:
public class GroupsContext
{
// Injection
@Structure
Module module;
GroupsAdmin groups;
public GroupsContext()
{
groups = new GroupsAdmin(); // Create wrapper
}
public GroupsContext rebind(Groups groups)
{
this.groups.bind(groups); // Bind wrapper
return this;
}
public Iterable<Group> index()
{
return groups.index();
}
public Group creategroup( StringValue name )
{
return groups.createGroup( name.string().get() );
}
class GroupsAdmin
extends Role<Groups>
{
Iterable<Group> index()
{
return self.groups();
}
Group createGroup(String name)
{
Group group = self.createdGroup(null,
module.serviceFinder().<IdentityGenerator>findService(IdentityGenerator.class).get().generate(GroupEntity.class));
group.changeDescription(name);
self().addedGroup(null, group);
return group;
}
}
}
---
The way to use this in client code would be:
new GroupsContext().rebind(groups).createGroup(newName);
The benefits with this solution are:
* You can create any number of contexts and roles that uses the basic
data/event methods, and don't have to worry with method clashing (major
issue without this solution).
* Role code can access members of the Context that wraps it, e.g.
"module". If there were more bound objects, those could be accessed
directly instead of having them be passed as method arguments to role
methods.
* Easier to see the algorithms as they are directly in the context,
rather than having to go one step down into some external mixin.
* Roles are only visible/usable from within the context that defines them.
* Very clear what the dependencies of the context are.
* Invisible from outside that DCI is being used.
The drawbacks I can see are:
* Since the roles are wrappers you have to think about what to pass
around: wrapper or self() (will almost always be self() since wrapper
class is unknown outside of context)
* Right now cannot use concerns/sideeffects/constraints on inner
classes. Can still use on context interaction if implemented as
transient though.
I'm sure there are more benefits and drawbacks, but this is what I can
see right now. If anyone has other ideas, that would be great. IF this
solution is acceptable, then that changes quite a bit what the typical
usage pattern of Qi4j in a DCI setting would be, so it is kind
important. It also makes it easier to use DCI outside of Qi4j, since
there's nothing really in the above that requires Qi4j, from a pattern
point of view.
WDYT?
/Rickard
_______________________________________________
qi4j-dev mailing list
[email protected]
http://lists.ops4j.org/mailman/listinfo/qi4j-dev