Inspired from Andreas Söderlund's DCI tutorial for TypeScript: https://blog.encodeart.dev/series/dci-typescript-tutorial
Please note that given Java's dynamic limitations and the considerations mentioned below, this is not true DCI.
Also, checkout another approach I've tried, which I call "Entity - UseCase - Wrapper": https://github.com/alexbalmus/euw
If you are new to Data-Context-Interaction, then it's recommended you read the following article first: https://fulloo.info/Documents/ArtimaDCI.html
DCI is a valuable (but not very well known) use case oriented design & architecture approach and OOP paradigm shift. Due to its particular characteristics, it's rather difficult to implement in a strongly typed programming language like Java. Two reference examples have been provided by DCI's authors, one using a library called Qi4J and the other using Java's reflection API: https://fulloo.info/Examples/JavaExamples/
In my case, I'm going for some tradeoffs: this is not "pure" DCI, but still aiming to be as close as possible to the valuable features DCI brings.
Prior considerations:
- Pure Java for roles/role-injection - no third party libraries / frameworks, as they might not be accepted in certain projects
- No reflection - this also might not be accepted in some projects, and Java's reflection API is a pain to work with
- Able to integrate in a mature/legacy code base, i.e., not requiring any changes to existing entities.
Approach taken for role methods in Java: variables of type functional interface with a naming convention.
We start by defining a custom functional interface that represents a role method:
com.alexbalmus.dcibankaccounts.common.RoleMethod:
/**
* Represents a DCI role method that contributes new contextual behavior to an object
* @param <T> method argument
*/
public interface RoleMethod<T>
{
void call(T t);
}
An actual role method might look something like this:
com/alexbalmus/dcibankaccounts/usecases/moneytransfer/MoneyTransferService.java:
// Source account:
RoleMethod<Double> SOURCE_transferToDestination = (amount) ->
{
if (SOURCE.getBalance() < amount)
{
throw new BalanceException(INSUFFICIENT_FUNDS); // Rollback.
}
SOURCE.decreaseBalanceBy(amount);
// equivalent of: DESTINATION.receive(amount):
DESTINATION_receive.call(amount);
};
When calling this, i.e. DESTINATION_receive.call(amount), it would be the equivalent of doing: SOURCE.transferToDestination(amount)
The context would select the objects participating in the use case and call the necessary role methods:
com.alexbalmus.dcibankaccounts.usecases.moneytransfer.MoneyTransferService.executeSourceToDestinationTransfer:
/**
* DCI context (use case): transfer amount from source account to destination account
*/
public void executeSourceToDestinationTransfer(
final Double amountToTransfer,
final A sourceAccount,
final A destinationAccount)
{
//----- Roles:
final A SOURCE = sourceAccount;
final A DESTINATION = destinationAccount;
//----- Role methods:
// Destination account:
RoleMethod<Double> DESTINATION_receive = (amount) ->
{
DESTINATION.increaseBalanceBy(amount);
};
// Source account:
RoleMethod<Double> SOURCE_transferToDestination = (amount) ->
{
if (SOURCE.getBalance() < amount)
{
throw new BalanceException(INSUFFICIENT_FUNDS); // Rollback.
}
SOURCE.decreaseBalanceBy(amount);
// equivalent of: DESTINATION.receive(amount):
DESTINATION_receive.call(amount);
};
//----- Interaction:
// equivalent of: SOURCE.transferToDestination(amount)
SOURCE_transferToDestination.call(amountToTransfer);
}
More info:
https://fulloo.info/Documents/ArtimaDCI.html