Mixins
As described in the overview, a mixin acts like a trait or extension method, allowing one module to contribute behaviour or derived state to another object. This is a very powerful feature, but at the same time is easy to use.
Under the covers, all mixins are basically actions that use the mixee as one of their arguments.
-
if the mixin action takes some arguments (apart from the mixee), then it will only ever be rendered as an action
-
if the mixin action is not query-only, then again it will only ever be rendered as an action
-
if the mixin action is query-only and takes no arguments, then it can either be rendered as an action or as some derived state
-
if as an action, then it’s basically a query to find and return some related info (eg "most recent order", "last 5 orders")
-
if as derived state, then it will be rendered as a property (if it returns a scalar), or as a collection (if it returns a vector).
-
Accordingly, a mixin can be defined using the @Action, @Property or @Collection annotations, but defined at the domain class level rather than at the method level.
The mixin is expected to follow the naming convention SomeType_mixinName
, with the name of the contributed member being contributed is inferred from the name of the class itself, being everything after the last '_'.
For example, if the mixin is called DocumentHolder_documents
, then the mixee is DocumentHolder
interface, and documents
is the name of the contributed collection.
This example also hints at why mixins are so powerful: the mixee’s type is usually an interface, not a concrete class.
Thus, any class that implements DocumentHolder
, eg Customer
or Order
, will be contributed to.
Contributed Collection
The example below shows in more detail how to contribute a collection, using @Collection. The method is expected to be called "coll":
@Collection (1)
@RequiredArgsConstructor (2)
public class DocumentHolder_documents {
private final DocumentHolder holder;
public List<Document> coll() { /* ... */ } (3)
...
}
public boolean hideColl() { /* ... */ } (4)
}
1 | indicates this is a collection mixin (that contributes its method as an association) with method name 'coll' |
2 | mixee is injected into the mixin |
3 | method (which must be called "coll") must accept no arguments, be query-only, and return a collection |
4 | supporting methods (discussed in business rules) follow the usual naming conventions
(Because the collection is derived/read-only, the only supporting method that is relevant is hideColl() ). |
The above will result in a contributed collection "documents" for all types that implement/extend from DocumentHolder
.
Contributed Property
Contributed properties can likewise be defined using @Property; this implies a method called "prop":
@Property (1)
@RequiredArgsConstructor (2)
public class DocumentHolder_mostRecentDocument {
private final DocumentHolder holder;
public Document prop() { (3)
...
}
public boolean hiderProp() { /* ... */ } (4)
}
1 | indicates this is a property mixin (that contributes its method as an association) with method name 'prop' |
2 | mixee is injected into the mixin |
3 | method (which must be called "prop") must accept no arguments, be query-only, and return a scalar value |
4 | supporting methods (discussed in business rules) follow the usual naming conventions
(Because the property is derived/read-only, the only supporting method that is relevant is hideProp() ). |
This contributes a property called "mostRecentDocument".
Contributed Action
Contributed actions are defined similarly, using @Action; this implies a method called "act". For example:
@Action (1)
@RequiredArgsConstructor (2)
public class DocumentHolder_addDocument {
private final DocumentHolder holder;
public Document> act(Document doc) { (3)
...
}
public boolean hideAct() { /* ... */ } (4)
}
1 | indicates this is an action mixin (that contributes its method as an action) with method name 'act' |
2 | mixee is injected into the mixin |
3 | method must be called "act" Unlike contributed properties and collections, contributed actions can accept parameters, and have any semantics. |
4 | supporting methods follow the usual naming conventions |
This contributes an action called "addDocument".
As Nested Classes
While mixins primary use case is as a means of allowing contributions from one module to the types of another module, they are also a convenient mechanism for grouping functionality/behaviour against a concrete type. All the methods and supporting methods end up in a single construct, and the dependency between that functionality and the rest of the object is made more explicit.
We might therefore want to use a mixin within the same module as the mixee; indeed even within the same package or class as the mixee. In other words, we could define the mixin as nested static class of the mixee it contributes to.
In the previous examples the "_" is used as a separator between the mixin type and mixin name. However, to support mixins as nested classes, the character "$" is also recognized as a separator.
For example, the following refactors the "updateName" action — of the SimpleObject
class in SimpleApp start app — into a nested mixin:
public class SimpleObject /* ... */ {
// ...
@Action(semantics = IDEMPOTENT,
commandPublishing = Publishing.ENABLED,
executionPublishing = Publishing.ENABLED,
associateWith = "name",
domainEvent = updateName.DomainEvent.class) (2)
public class updateName { (1)
public class DomainEvent extends
SimpleModule.ActionDomainEvent<SimpleObject.updateName> {} (2)
public SimpleObject act(@Name final String name) {
setName(name); (3)
return SimpleObject.this;
}
public String default0Act() { (4)
return getName(); (5)
}
}
// ...
}
1 | Mixin class is not static , so that the containing object is implicitly available.
Its name can be either "camelCase" or "PascalCase", either will work. |
2 | Domain event can be declared within the mixin, again, not static .
Note that it is genericised on the mixin, not on the mixee |
3 | Acts on the owning instance. |
4 | Supporting methods follow the same naming convention. |
5 | Acts on the owning instance. |
Programmatic usage
When a domain object is rendered, the framework will automatically instantiate all required mixins and delegate to them dynamically. If writing integration tests or fixtures, or (sometimes) just regular domain logic, then you may need to instantiate mixins directly.
For this you can use the FactoryService#mixin(…) method.
For example:
DocumentHolder_documents mixin =
factoryService.mixin(DocumentHolder_documents.class, customer);
Alternatively, you can use ServiceInjector to inject domain services after the mixin has been instantiated.
You’ll need to use this method if using nested non-static
mixins:
SimpleObject.updateName mixin =
serviceInjector.injectServicesInto( simpleObject.new updateName() );