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.
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.
Using @DomainObject#nature attribute (specifying a nature of Nature.MIXIN
), in combination with the above allows for more fine grained control, eg. nominating the mixin’s method name.
When the mixin follows the naming convention SomeType_mixinName
then the method name can be abbreviated, and the name of the member being contributed is inferred from the name of the class itself, being everything after the last '_'.
Contributed Collection
The example below shows 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 /* ... */ {
// ...
public static class UpdateNameActionDomainEvent extends
SimpleModule.ActionDomainEvent<SimpleObject.updateName> {} (1)
@Action(semantics = IDEMPOTENT,
commandPublishing = Publishing.ENABLED,
executionPublishing = Publishing.ENABLED,
associateWith = "name",
domainEvent = UpdateNameActionDomainEvent.class)
public class updateName { (2)
public SimpleObject act(@Name final String name) {
setName(name); (3)
return SimpleObject.this;
}
public String default0Act() { (4)
return getName(); (5)
}
}
// ...
}
1 | Domain event is genericised on the mixin, not on the mixee |
2 | Not static. Can be camelCase or PascalCase, either will work. |
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() );