wow.
Thanks :)
Yeah, Qi4j can seem daunting at first, but you get to love it more and more. I'm only about to grasp the powers of the whole composite idea that makes it so cool. I'll continue with more advanced examples, hopefully demonstrating this more clearly. I only got the yellow, maybe orange belt in Qi4j ;)
Cheers,
Marc
> --
> You received this message because you are subscribed to the Google Groups "object-composition" group.
> To post to this group, send email to object-co...@googlegroups.com.
> To unsubscribe from this group, send email to object-composit...@googlegroups.com.
> For more options, visit this group at http://groups.google.com/group/object-composition?hl=en.
>
Awesome start! I have now added two more versions, based on all the
discussions and feedback from this list. I have sent it to you for
review and hopefully you can upload it to replace the one that is there now.
Description of v7:
* TransferMoneyContext is a plain class, no superclass or interfaces
required. In the constructor it gets the objects and then converts them
to roles:
public TransferMoneyContext( BalanceData source, BalanceData
destination )
{
this.source = (SourceAccountRole) source;
this.destination = (DestinationAccountRole) destination;
}
---
* The roles/default implementations are defined WITHIN the
TransferMoneyContext as inner interfaces/classes, so that it is clear
that they are only relevant within that context:
public class TransferMoneyContext
{
... interactions goes here ...
interface SourceAccountRole
{
... role methods goes here ...
class SourceAccountMixin
implements SourceAccountRole
{
... default impl goes here ...
}
}
... more roles goes here ...
}
* The context has two interactions:
public Integer availableFunds()
{
return source.availableFunds();
}
public void transfer( Integer amount )
throws IllegalArgumentException
{
source.transfer( amount );
}
---
One query and one command. I follow CQRS, so an enactment can only do
query OR command, not both.
* The entities (SavingsAccountEntity, CheckingAccountEntity,
CreditorEntity) extend the BalanceData and role interfaces (v8 fixes
this so that the entities in the domain does not have to know about
contexts and roles in which they are used), so that the casting in the
constructor works properly.
* To enact an interaction, the objects are looked up using data
interfaces, and the context is instantiated with these (NOTE: the
environment does NOT have to know about the roles!). Then an explicit
enactment (query or command) is started, which pushes the context onto
the contexts stack:
TransferMoneyContext context = new TransferMoneyContext(source,
destination);
// Query for half the balance
final Integer amountToTransfer = Contexts.withContext( context, new
Contexts.Query<Integer, TransferMoneyContext, RuntimeException>()
{
public Integer query( TransferMoneyContext transferMoneyContext )
throws RuntimeException
{
// Look up available funds from the source account
return transferMoneyContext.availableFunds()/2;
}
});
// Transfer from savings to checking
Contexts.withContext( context, new
Contexts.Command<TransferMoneyContext, IllegalArgumentException>()
{
public void command( TransferMoneyContext transferMoneyContext )
throws IllegalArgumentException
{
// Transfer from source to destination
transferMoneyContext.transfer( amountToTransfer );
}
});
---
The "Contexts.withContext" static method handles the proper
pushing/popping of the context on a stack, so that code within the
query/command methods can access the current context easily and
correctly. Generics is used to maximize static typing of context, return
value of query, and exceptions thrown.
In this case one context is used for two interactions, first a query and
then a command. The reuse of the context shows how there is less to do
for each interaction as they can focus solely on what is done with the
context, rather than how the context is set up.
* Since the context with the bindings is on a stack, it can be accessed
by any role, statically typed. From SourceAccountMixin:
public void transfer( Integer amount )
throws IllegalArgumentException
{
// Validate command
if (!(data.getBalance() >= amount))
throw new IllegalArgumentException("Not enough available funds");
// Command is ok - create events in the data
data.decreasedBalance( amount );
// Look up the destination account from the current transfer context
Contexts.context( TransferMoneyContext.class ).destination.deposit(
amount );
}
---
The context is looked up on the stack, and the first one with the given
type is returned. From the context we can look up the bound object, and
invoke role methods. If this is a stacked context, it is possible to
call Contexts.context(<othercontexttype>) to find previously "pushed"
contexts which this context is executing within.
* Data methods are considered events, and are hence in past tense. They
don't do any validation, apart from the bare input checking (in this
case, if the amount is positive). Any more complex logic is in the role.
This makes it easy to use EventSourcing with this setup.
* Data (as in the above case) is accessed through "@This BalanceData
data" injections, i.e. the role is considered to be a part of the object.
More details, but that's about it. AFAICT this setup complies with all
the rules outlined in articles and this mailing list.
The only remaining issue is that the domain entity explicitly extends
the role interfaces, thus knows about the contexts in which it is used.
v8 fixes this by separating the entity declarations into two layers, one
for the domain and one for the context. The domain one looks like this:
public interface CheckingAccountEntity
extends EntityComposite,
// Data
BalanceData
{}
---
And then the application layer, where the contexts reside, extends this
with:
public interface CheckingAccountATM
extends CheckingAccountEntity,
// Roles
TransferMoneyContext.SourceAccountRole,
TransferMoneyContext.DestinationAccountRole
{}
---
The binding from domain entity to context roles is now properly
separated from the domain, and yet centralized so that this
understanding does not have to be shown anywhere except in the assembly
code of the application. You can now statically analyze what objects can
do what within the application. There is no runtime dynamism going on.
That's pretty much it. This is, AFAICT, the most DCI-compliant way to
implement Qi4j applications. And also brutally simple, and provides easy
to read code that is properly separated in terms of responsibilities.
/Rickard
This was so inspiring that I have replaced the old DCI sample in Qi4j,
and added this instead. You can either get it from the qi4j-samples
module, or look at it on the web through our GitWeb here:
http://bit.ly/bYAENU
Very very nice. The simplicity of the code is very refreshing.
/Rickard
Nice - it looks like this invalidates my comment in another thread
about 'the problem with Qi4j being static class composition'. I take
that back. :)
This is very cool, I think! I have two comments/questions, though,
which may be the same:
1. Semantically, this example is great, I think:
// Query for half the balance
final Integer amountToTransfer = Contexts.withContext( context, new
Contexts.Query<Integer, TransferMoneyContext, RuntimeException>()
{
public Integer query( TransferMoneyContext transferMoneyContext )
throws RuntimeException
{
// Look up available funds from the source account
return transferMoneyContext.availableFunds()/2; //
HERE IT HAPPENS
}
});
However, from a syntactic/readability perspective, it kind of sucks.
There's just so much text there that's in the way of understanding
what is actually going on. This is probably largely or only due to
Java as a language - I love it, but it has weaknesses with regard to
handling generics and anonymous inner classes/closures, and these
weaknesses are highlighted in that example. The only really relevant
line is the one I flagged as 'HERE IT HAPPENS'. Have you done any
experimentation with trying to reduce the amount of noise in the code?
2. I'm not a fan of static methods as they introduce close coupling.
Would it be possible to inject the current context instead of asking
for it using Contexts.context() in mixins? That would reduce the "text
overhead" in a statement like:
Contexts.context( TransferMoneyContext.class ).destination.deposit( amount );
to
transferMoneyContext.destination.deposit( amount );
Maybe that would introduce undesirable state into mixins or just be
bad from some other perspective? It feels like it would make it easier
to unit test them at least, since you can instantiate them directly
with your desired current context rather than having to set up your
static Context class.
Cheers,
Petter
Exactly!
> However, from a syntactic/readability perspective, it kind of sucks.
> There's just so much text there that's in the way of understanding
> what is actually going on. This is probably largely or only due to
> Java as a language - I love it, but it has weaknesses with regard to
> handling generics and anonymous inner classes/closures, and these
> weaknesses are highlighted in that example. The only really relevant
> line is the one I flagged as 'HERE IT HAPPENS'. Have you done any
> experimentation with trying to reduce the amount of noise in the code?
I only wrote it this morning, so no. Better closure support would
definitely help tremendously here.
> 2. I'm not a fan of static methods as they introduce close coupling.
> Would it be possible to inject the current context instead of asking
> for it using Contexts.context() in mixins? That would reduce the "text
> overhead" in a statement like:
>
>
> Contexts.context( TransferMoneyContext.class ).destination.deposit( amount );
>
> to
> transferMoneyContext.destination.deposit( amount );
>
> Maybe that would introduce undesirable state into mixins or just be
> bad from some other perspective? It feels like it would make it easier
> to unit test them at least, since you can instantiate them directly
> with your desired current context rather than having to set up your
> static Context class.
What I *could* do is make a custom dependency injection annotation+provider:
@Context TransferMoneyContext context;
That would be no problem at all actually. Whether it's better... not
sure really.
/Rickard
Ok, to improve this, the easiest solution would be to have a Concern
(aka "Advice" in AOP lingo) on the availableFunds() method that pushes
the context onto the stack and pops it when done. Then the code becomes:
Integer amountToTransfer = context.availableFunds() / 2;
which is better.
Unfortunately Qi4j does not support Concerns on POJO's right now, so
TransferMoneyContext would have to be a composite. We have planned to
add support for concerns on POJO's for next version though, which would
fix it.
/Rickard
Nope, it's an interaction method on the context. Environment code cannot
call role methods, only interactions in the context can.
/Rickard
"it"? You mean the interaction method? It takes the selected objects and
starts off the interaction.
Or what did you refer to? Be specific.
/Rickard
Yes, all of that was just to invoke an interaction in a context, in
order to properly push the context onto the context stack. But if I put
an advice/concern/interceptor (whatever you want to call it) on the
interaction that does it, it simple becomes:
Integer amountToTransfer = context.availableFunds()/2;
/Rickard