Implementing DCI in Qi4j
Join the DZone community and get the full member experience.
Join For FreeLast year me, Trygve Reenskaug and Jim Coplien presented a track on Oredev on DCI: Data, Context, Interaction. DCI is a new way of looking at how to construct object oriented applications, and which focuses on creating code that matches the users mental model. For an introduction to DCI I would recommend reading this article at Artima, and also that you watch the three presentations from Oredev. That should give you enough background on what it is that DCI is trying to achieve.
But the question is: how should this be implemented in practice? Are there any guidelines on how to actually do it, in a way that makes the code truly easier to read.
I've been investigating this within the context of my StreamFlow project, which intends to build a product for human workflow based on GTD and Systems Thinking principles. The need I had was to create a self-describing REST API for my application, and by accident it turned out that DCI was the perfect fit for it. This blog post will outline the DCI side of things, and a later blog post will describe how to use that in to construct a REST API.
Roles
One of the core ideas of DCI is that objects are not made up of single classes, i.e. POJO's, but instead use roles to compose functionality. To be specific, when I say "object" here it relates to Entities in DDD terms, rather than Values or anything else. In an enterprise architecture setting, with persistent data, it's mainly the entities that we want to split up into roles, where each role has a distinct purpose. In Qi4j, these roles are implemented by Mixins. In Scala they would be termed Traits. In plain Java there is no equivalent, hence making it rather difficult to implement DCI properly in Java, unless you use Qi4j.
Data
The data part of DCI would simply be a Mixin that primarily holds data for the Entity. The main point is that data mixins are declared as private, meaning, they are not accessible from outside of the entity. Private mixins are an important concept, which allows the developer to keep state private within the object, without resorting to using the "private" Java keyword, which would make state inaccessible from other roles. Instead, all methods are marked as public, but the mixin itself can only be accessed by other mixins, hence hiding it for outside users.
Context
In DCI, a Context holds the mappings from roles in an interaction to specific object instances, and also have the interactions that can be performed on this mapping. Contexts can be implemented using either POJO's or TransientComposites in Qi4j. POJO's are simpler, but TransientComposites allow you to use composable contexts, which is more powerful. For simplicity I will stick to the POJO version here.
The implementation of contexts that I have created separates between the context itself and the context map, which holds the actual object to role mappings. Because of this you can create a context map, send it in to a context, and then let that context add to the map and create new contexts. I will demonstrate why that can be useful.
Interactions
Finally, interactions are implemented as simple methods in the context. They may take arguments, and then look up mapped roles in the context map, and fire off a domain method. The important point here is that the interaction methods should match whatever actions you have in your user interface, so that the code matches the users mental model. This is one of the key goals here. If you change your UI to something different, then you need to change your contexts and interactions accordingly, so that it becomes easy to trace what in the UI maps to what code in the DCI setup.
The domain model
For the purposes of this blog post I will use a simple domain model. We have three entities, Project, User, and Task. Tasks implement the Assignable role. Project and User implement the Assignments role. Users can be Assignees. We then have a main context with interactions called InboxContext, which has one method(/interaction): "assign(Assignable)". Both Users and Projects have an Inbox with tasks, and it is these that we want to assign to Users. But they can be "owned" either by the User itself, or by a particular Project, hence the need for a separate role Assignments so that our notion of "assignment" is not tied to "given a users list of tasks, one of them can be assigned to the user" but rather "given a Assignments collection of Assignables, pick one and assign to an Assignee". This way the interaction and context is entirely separated from the actual classes, and the only thing we need are the appropriate roles.
What's the point of this? Well, for starters it allows the developer to only focus on the role classes while trying to understand how assignment works. The User might have username/password, a user profile, email delivery preferences, and all sorts of other things, but if you had to know about this when just looking into the assignment handling it would be quite messy. And this is exactly how POJO development works today.
Additionally, if I later want to implement the exact same interaction but with "Calculation" instead of Task and "ClusterNode" instead of User, then I could reuse the whole assignments subsystem without changing a thing. The only thing that changes is the mapping from objects to roles.
Executing an interaction
Let's see what happens as we walk through an execution of the "assign" interaction. The first thing we need to do is set up the context:
InteractionContext map = new InteractionContext();
RootContext context = assembler.objectBuilderFactory().newObjectBuilder( RootContext.class ).use(stack ).newInstance();
InboxContext inboxContext = context.user( userId ).inbox();
I create a new context map for this interaction, and instantiate a new RootContext. This symbolizes the root of all contexts in my application, and will mainly hold methods for getting to subcontexts. I pass in the map so that the RootContext can pass it on during the user() call, which will create the subcontext UserContext that has all the interactions and subcontexts for working with a selected user. I pass in the userId so that the user() method can do the lookup. If this DCI implementation is used in a REST API setting that context lookup will basically map to the URL, so the "userId" will be one part of the URL being referenced. You can imagine the above being mapped to "/administrator/inbox" in a URL.
The user() method will add the given user to the context map, and then create a new subcontext with the extended map. Here's what it looks like:
public class RootContext"Context" is a baseclass that has the InteractionContext in a variable "context", and a "subContext" method for easily instantiating new contexts with that context map. What I do here is to look up the UserEntity with the given id from the Qi4j UnitOfWork, and then register it in the map with the given roles. The object already has those roles, so the only thing that happens here is that the map knows that if someone asks for the object playing the "Assignee" role, it knows what to return.
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 );
}
}
Once the context has been looked up it is time to invoke the interaction, with arguments:
inboxContext.assignTo( task );
Since the context has access to the context map the above method doesn't need to know who to assign it to. That is given by the context map! The implementation of assignTo() is as follows:
public class InboxContext
extends Context
{
public void assignTo( Assignable assignable )
{
context.role( Assignments.class).assignTo( assignable, context.role( Assignee.class ));
}
}
The assignTo() interaction uses the context map to look up the objects bound to the roles Assignments and Assignee, and invokes the assignTo method given these objects. In the above you therefore see exactly how the context, interaction and roles interact to implement a given usecase.
If we follow the assignTo() method and see what it does, it looks like this:
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();
}
}
The AssignmentsMixin implementation assigns the Assignable to the Assignee, and then adds it to the list of assignments. Nowhere in this code do we see that we are talking about Users, Tasks or Projects. It is all related to the roles related to handling assignment. This allows us to focus on one thing at a time, and makes it clear what the boundaries are between various algorithms and roles that interact in our system as a whole.
The @This injection is what provides the private mixin support. The field will be injected with a reference to "this object", cast to the "AssignmentsData", which is a mixin that holds the data for managing assignments. This cannot be reached from the outside of the entity, however. What we want is to ensure that all access to data of entities are accessed through our roles. This helps keep our state encapsulated.
If another algorithm also needs to use the same state, then all it has to do is perform the same injection. This way that other functionality, for some other usecase, can be kept separate from this AssignmentsMixin, so that each mixin deals with one thing, and one thing only.
Now that you know how to assign a task to a user, let's try switching things around: a task will be assigned to a user, but within the context of a specific project. The code to do this looks like this:
InteractionContext stack = new InteractionContext();In this case, instead of letting the user play both the Assignee and Assignments roles, the user will only be used for the Assignee. The project which is looked up in the project() call will be bound to Assignments, so that once we get to assignTo() in the InboxContext, the algorithm will essentially say: "Assign the Task to the User within the Projects assignment". And to do this we did not have to change any code in the assignments handling. The only thing that changed was what objects were bound to what roles! Let me hear you say: "SWEET!"
RootContext context = assembler.objectBuilderFactory().newObjectBuilder( RootContext.class ).use( stack ).newInstance();
context.user( user.identity().get() ).project( project.identity().get() ).inbox().assignTo( task2 );
What we have seen here is a simple example of how DCI can be implemented in Qi4j, and which provides all the key ingredients needed: Roles, Data, Contexts and Interactions.
What's the point?
You might be wondering: what is the point of all this? One thing I'm not trying to do here is to keep the amount of code to a minimum. Instead I'm purposefully creating mixins which collaborate within an entity, and the amount of code compared to a POJO approach grows quite a bit, at least initially. So LOC is obviously not my main concern.
So what then? The key thing I want to achieve is readability of code, maintainability of code, and ease of change. If you instead work with a POJO approach, putting all the code for these roles into one class, that complicates all of the above. Once you get to a certain size of your project, having all code in one place will make it less readable, less maintainable, and harder to change. Much harder.
Another key benefit of doing things this way is that it really enables reuse on a whole new level. You can create roles, contexts and interactions that implement a particular usecase, and then reuse that over and over again in various parts of your domain model. Whether this is useful or not depends on the size and complexity of your project. If you're doing a small consultant gig, with one developer, then it probably won't pay off. However, if you're doing a system that is medium sized or above, with several developers, then this approach will make it significantly easier to build and maintain, and it will be much better at handling new requirements as there is less need to consider all the code that is already there. An entity can easily have 20+ mixins without much problems, since all the code is cleanly separated into specialized roles. Do that with a POJO-approach, and you're screwed. You will get a whole lot of methods and state within the same class, and it will be almost impossible to figure out what belongs code belongs to what usecase. Needless to say, this also screws up reusability, since there is no way to take a part of that POJO and reuse it elsewhere.
Another benefit here is that we have avoided the anemic domain model, which is another "solution" to this problem. In a POJO-approach, to avoid having all the logic in the actual class, you could make the objects only contain the data and then put all the logic into the application services. This obviously breaks encapsulation, with all the problems that this gives, but that is how most people seem to "solve" it these days. The DCI approach gives us a clean way to get back to putting the logic into our objects, without messing things up.
The full code for the above is available in the qi4j-samples Git repo, which you can find on the web here. Go here for instruction on how to check it out. The code includes 3 versions of how to do this using POJO's, and 2 versions in Qi4j using DCI.
Opinions expressed by DZone contributors are their own.
Comments