Actions have four parts to them (responsibliities, I suppose):
- execution
- supporting methods for perform the validation etc
- target object that they act upon
- event publishing
Our programming model could provide several syntaxes to put these responsibilities in different places. It already supports two (standard actions, and mixins).
To compare these syntaxes, we'll use a concrete example:
public class Customer { // target ... public static class PlaceOderEvent extends ActionDomainEvent<Customer> {} @Action(domainEvent = PlaceOrderEvent.class) // event publishing public Customer placeOrder(Product p, int quantity) { ... } // execution public boolean hidePlaceOrder() { return this.isBlackListed(); } // supporting methods public String disablePlaceOrder() { return clockService.outsideShoppingHours(); } public String disable1PlaceOrder(Product p) { return p.isOutOfStock() ? "Out of stock": null; } public Collection<Product> choices0PlaceOrder() { ... } public Product default0PlaceOrder() { return orderService.findLastProductPurchasedBy(this); } public int default1PlaceOrder() { return 1; } public String validate1PlaceOrder(int quantity) { return quantity <= 0 ? "Can only order +ve amounts": null; } public String validatePlaceOrder(Product p, int quantity) { return catalogService.runningLowOn(p) && quantity > 4 ? "We're running low on that product, no more than 4" : null; } ... @Inject CatalogService catalogService; @Inject ClockService clockService; @Inject OrderService orderService; }
Notes:
- For disable1PlaceOrder(), we could also hide individual parameters similarly
- Instead of choices0PlaceOrder(), we could have used autoComplete0PlaceOrder(String)
Standard syntax
The standard syntax uses regular methods on the target object. Naming conventions are used to associate the action with supporting methods (default, choices, hide, disable and validate).
Here's the same example as before, but with implementation stripped out so that it is easier to compare with the alternate syntaxes that follow:
public class Customer { // target public static class PlaceOderEvent extends ActionDomainEvent<Customer> {} @Action(domainEvent = PlaceOrderEvent.class) // event publishing public Customer placeOrder(Product p, int quantity) { ... } // action execution public boolean hidePlaceOrder() { ... } // supporting methods public String disablePlaceOrder() { ... } public String disable1PlaceOrder(Product p) { ... } public Collection<Product> choices0PlaceOrder() { ... } public Product default0PlaceOrder() { ... } public int default1PlaceOrder() { ... } public String validate1PlaceOrder(int quantity) { ... } public String validatePlaceOrder(Product p, int quantity) { ... } }
Mixins syntax
Mixins change the target, by allowing this set of methods to be moved to a different object. In other words, the target responsibility changes.
So the real target is simply:
public class Customer {}
and the action itself moves onto the mixin:
@Action public class Customer_placeOrder() { private final Customer target; // target public Customer_placeOrder(Customer target) { ... } public static class PlaceOrderEvent extends ActionDomainEvent<Customer> {} @Action(domainEvent = PlaceOrderEvent.class) // event publishing public Customer act(Product p, int quantity) { ... } // action execution public boolean hideAct() { ... } // supporting methods public String disableAct() { ... } public String disable1Act(Product p) { ... } public Collection<Product> choices0Act() { ... } public Product default0Act() { ... } public int default1Act() { ...} public String validate1Act(int quantity) { ... } public String validateAct(Product p, int quantity) { ... } }
Notes:
- Instead of
@Action
, the@Mixin(method="act")
could also be used, with additional annotations on theact(...)
method. I've chosen the version with the least boilerplate here. - Mixins are also used for derived properties or collections (ie query-only actions with no side-effects). These are specified using
@Property
or@Collection
- We now have two classes here: the mixin, and the domain event within.
Parameters syntax (proposed)
Per this thread on slack, we could introduce a Parameters object (in Java 14+, this might be a record) to bring together all of the parameters into a single object. This would make it easier to avoid issues with numbering etc.
This syntax changes the way in which supporting methods are associated back to the main execution method.
public class Customer { // target @Value @Accessors(fluent = true) public class PlaceOrderParameters { // to assist supporting methods Product product; int quantity; } public static class PlacerOrderEvent extends ActionDomainEvent<Customer> {} @Action(domainEvent = PlaceOrderEvent.class) // event publishing public Customer placeOrder(Product p, int quantity) { ... } // execution public boolean hidePlaceOrder() { ... } // supporting methods use PlaceOrderParameters public String disablePlaceOrder() { ... } public String disable1PlaceOrder(PlaceOrderParameters params) { ... } public Collection<Product> choices0PlaceOrder() { ... } public Product default0PlaceOrder() { ... } public int default1PlaceOrder() { ... } public String validate1PlaceOrder(PlaceOrderParameters params) { ... } public String validatePlaceOrder(PlaceOrderParameters params) { ... } }
Notes:
- The
@Value
@Accessors(fluent=true)
allows us to use a syntax that is very similar to Java 14 records. - There is some duplication here: the list of the parameter types appears both in the
placeOrder(...)
method, as well as in thePlacerOrdersParameters
class.
The above would also be supported with mixins:
@Action public class Customer_placeOrder { private final Customer target; // target public Customer_placeOrder(Customer target) { ... } @Value @Accessors(fluent = true) public static class PlaceOrderParameters { // to assist supporting methods Product product; int quantity; } public static class PlaceOrderEvent extends ActionDomainEvent<Customer> {} @Action(domainEvent = PlaceOrderEvent.class) // event publishing public Customer act(Product p, int quantity) { ... } // execution public boolean hideAct() { ... } // supporting methods public String disableAct() { ... } public String disable1Act(PlaceOrderParameters params) { ... } public Collection<Product> choices0Act() { ... } public Product default0Act() { ... } public int default1Act() { ... } public String validate1Act(PlaceOrderParameters params) { ... } public String validateAct(PlaceOrderParameters params) { ... } }
Notes:
- we now have three classes here: the mixin, the domain event, and the parameters object.
Parameters on Act syntax (proposed)
This is a variant of the previous, but uses the parameters class in the action as well:
public class Customer { // target @Value @Accessors(fluent = true) public class PlaceOrderParameters { // to assist supporting methods @Parameter() @MemberOrder(1) Product product; @Parameter() @MemberOrder(2) int quantity; } public static class PlacerOrderEvent extends ActionDomainEvent<Customer> {} @Action(domainEvent = PlaceOrderEvent.class) // event publishing public Customer placeOrder(PlaceOrderParameters params) { ... } // execution public boolean hidePlaceOrder() { ... } // supporting methods use PlaceOrderParameters public String disablePlaceOrder() { ... } public String disable1PlaceOrder(PlaceOrderParameters params) { ... } public Collection<Product> choices0PlaceOrder() { ... } public Product default0PlaceOrder() { ... } public int default1PlaceOrder() { ... } public String validate1PlaceOrder(PlaceOrderParameters params) { ... } public String validatePlaceOrder(PlaceOrderParameters params) { ... } }
Notes:
- this removes the duplication between the
placeOrder(...)
parameter list and the list of members inPlaceOrderParameters
class. - the
@Parameter
and@MemberOrder
syntax would be required by the framework to identify PlaceOrderParameters as a container of parameters (as opposed to a reference object or custom value type)
As a mixin, this becomes:
@Action public class Customer_placeOrder { private final Customer target; // target public Customer_placeOrder(Customer target) { ... } @Value @Accessors(fluent = true) public static class PlaceOrderParameters { // to assist supporting methods @Parameter() @MemberOrder(1) Product product; @Parameter() @MemberOrder(2) int quantity; } public static class PlaceOrderEvent extends ActionDomainEvent<Customer> {} @Action(domainEvent = PlaceOrderEvent.class) // event publishing public Customer act(PlaceOrderParameters params) { ... } // execution public boolean hideAct() { ... } // supporting methods public String disableAct() { ... } public String disable1Act(PlaceOrderParameters params) { ... } public Collection<Product> choices0Act() { ... } public Product default0Act() { ... } public int default1Act() { ... } public String validate1Act(PlaceOrderParameters params) { ... } public String validateAct(PlaceOrderParameters params) { ... } }
Notes:
- we still have three classes here (mixin, parameters and domain event), but we have removed the duplication between the
act(...)
parameter list and the list of members ofPlaceOrderParameters
class
Parameters everywhere syntax (proposed)
The previous syntax only passes in parameters to some of the supporting methods. For consistency, we could imagine it being passed in always.
Just focusing on the mixin syntax, this would become:
@Action public class Customer_placeOrder { private final Customer target; // target public Customer_placeOrder(Customer target) { ... } @Value @Accessors(fluent = true) public static class PlaceOrderParameters { // to assist supporting methods @Parameter() @MemberOrder(1) Product product; @Parameter() @MemberOrder(2) int quantity; } public static class PlaceOrderEvent extends ActionDomainEvent<Customer> {} @Action(domainEvent = PlaceOrderEvent.class) // event publishing public Customer act(PlaceOrderParameters params) { ... } // execution public boolean hideAct(PlaceOrderParameters params) { ... } // supporting methods public String disableAct(PlaceOrderParameters params) { ... } public String disable1Act(PlaceOrderParameters params) { ... } public Collection<Product> choices0Act(PlaceOrderParameters params) { ... } public Product default0Act(PlaceOrderParameters params) { ... } public int default1Act(PlaceOrderParameters params) { ... } public String validate1Act(PlaceOrderParameters params) { ... } public String validateAct(PlaceOrderParameters params) { ... } }
Discussion
With the parameters object passed in everywhere, I could see myself starting to move functionality onto that object. So as an idiom, we might see the following sort of code (in a mixin):
@Action public class Customer_placeOrder { private final Customer target; public Customer_placeOrder(Customer target) { ... } @Value @Accessors(fluent = true) public static class PlaceOrderParameters { ... } // see below. public static class PlaceOrderEvent extends ActionDomainEvent<Customer> {} @Action(domainEvent = PlaceOrderEvent.class) public Customer act(PlaceOrderParameters params) { return params.act(this); } public boolean hideAct(PlaceOrderParameters params) { return params.hide(this); } public String disableAct(PlaceOrderParameters params) { return params.disable(this); } public String disable1Act(PlaceOrderParameters params) { return params.disable1(this); } public Collection<Product> choices0Act(PlaceOrderParameters params) { return params.choices0(this); } public Product default0Act(PlaceOrderParameters params) { return params.default0(this); } public int default1Act(PlaceOrderParameters params) { return params.default1(this); } public String validate1Act(PlaceOrderParameters params) { return params.validate1(this); } public String validateAct(PlaceOrderParameters params) { params.validate(this); } }
which would then beef up the parameters object:
@Action public class Customer_placeOrder { private final Customer target; // target ... @Value @Accessors(fluent = true) public static class PlaceOrderParameters { @Parameter() @MemberOrder(1) Product product; @Parameter() @MemberOrder(2) int quantity; public Customer act(Customer customer) { ... } // execution public boolean hideAct(Customer customer) { ... } // supporting methods public String disableAct(Customer customer) { ... } public String disable1Act(Customer customer) { ... } public Collection<Product> choices0Act(Customer customer) { ... } public Product default0Act(Customer customer) { ... } public int default1Act(Customer customer) { ... } public String validate1Act(Customer customer) { ... } public String validateAct(Customer customer) { ...} } ... public static class PlaceOrderEvent extends ActionDomainEvent<Customer> {} @Action(domainEvent = PlaceOrderEvent.class) // event publishing public Customer act(PlaceOrderParameters params) { return params.act(this); } // remainder is just boilerplate public boolean hideAct(PlaceOrderParameters params) { return params.hide(this); } public String disableAct(PlaceOrderParameters params) { return params.disable(this); } public String disable1Act(PlaceOrderParameters params) { return params.disable1(this); } public Collection<Product> choices0Act(PlaceOrderParameters params) { return params.choices0(this); } public Product default0Act(PlaceOrderParameters params) { return params.default0(this); } public int default1Act(PlaceOrderParameters params) { return params.default1(this); } public String validate1Act(PlaceOrderParameters params) { return params.validate1(this); } public String validateAct(PlaceOrderParameters params) { params.validate(this); } }
Notes:
- the target is still outside of the parameters object
- Event publishing also outside
- Everything else has moved inside the parameters object
- This implies that we would need dependency injection for the parameters object
- The rest of the code in the mixin is just boilerplate. It's possible that the Lombok @Delegate annotation might be used to remove some of this boilerplate, didn't investigate further.
Mixins and Parameters combined (proposed)
yada yada