I've struggled with this kind of thing for a long time. I still don't really know the "right" way to do it. Sorry if everything I'm about to say sounds like nonsense. There's a good chance I have no idea what I'm talking about.
There's a quote from Sandi Metz's book, Practical Object-Oriented Design in Ruby, that has always stuck with me:
"This transition from class-based design to message-based design is a turning point
in your design career. The message-based perspective yields more flexible applications
than does the class-based perspective. Changing the fundamental design question
from “I know I need this class, what should it do?” to “I need to send this message,
who should respond to it?” is the first step in that direction.
You don’t send messages because you have objects, you have objects because you
send messages."
Still, the question is who can respond to the transferFunds message? I suppose a FundTransferer object will do. It shouldn't matter to the sender. In a dynamic language, the sender is coupled to the message name and arguments. In a static language with interfaces, it might be coupled to an interface and message, e.g.
interface CanPerformFundsTransfer {
void transferFunds(Amount amount, AccountId fromAccountId, AccountId toAccountId);
}
In Grzegorz's example he used the TransferFundsMessage type to represent the message and its data and referred to erlang as an example. I'm not familiar with erlang, but I'm reminded of Smalltalk. In the same way the TransferFundsMessage type statically defines the message (action + data), Smalltalk messages are a combination of action and data, closer to the erlang model. IIRC, erlang objects run on their own processes and implement actor-like behavior which is what makes it different, but I think the message concepts in Smalltalk/Erlang are closer than say Smalltalk/Java.
In a language like Java, the transferFunds method is not really a thing like it is in Smalltalk, so to represent the message itself we could create a typed data clump like Grzegorz's example of TransferFundsMessage type that actually represents the message (unique type name representing the action + data properties).
In Smalltalk, we could have a message selector like #transferAmount:fromAccount:toAccount:. This is a unique symbol representing the message and associated data (arguments), in the same way the TransferFundsMessage type has a unique name and properties.
I think what's more interesting is assuming you've defined the interface as
FundsTransferrer::transferFunds, how is it implemented? If we take the same perspective as messages first, we might practically end up with something like what Justin Searls does with his discovery testing technique...
https://blog.testdouble.com/talks/2015-09-10-how-i-use-test-doubles/ which, for the sake of keeping things simple, might end up looking something like this:
class TransfersFunds {
private ValidatesFundsTransfer validatesTransfer;
private ProvidesTransferId providesTransferId;
private CompletesTransfer completesTransfer;
TransferResult transferFunds(TransferFundsMessage message) {
ValidationResult vr = validatesTransfer.isValid(message.fromAccount(), message.toAccount(), message.amount());
if(!vr.isValid()) {
return TransferResult.failed(vr);
}
TransferId id = providesTransferId.newId();
return completesTransfer.transfer(id, message.fromAccount(), message.toAccount(), message.amount());
}
}
Those other objects would be further broken down into responsibilities. Eventually, they would do some actual work, but mostly they delegate the work to these anonymous agents. You'll notice with his technique, if you read Justin's blog/watch the videos, there are very small objects, each with only one or two methods. There is a pattern of implementing classes with an input, process, and output collaborators. It's an interesting technique, but I look at it and think... The things like ValidatesFundsTransfer, ProvidesTransferId, and CompletesTransfer can be renamed to FundsTransferValidator, TransferIdProvider, and TransferCompeleter. To me, this is reminiscent of procedural or service oriented designs where data clumps are passed along anonymous agent objects that poke at getters and setters. Lots of ThingDoer objects, with doThing() methods, which lots of popular OO literature says to avoid. At the same time, this sure as hell prevents you from having to make any decisions about what the "actual work" looks like, since we're just passing the buck via delegation.
In looking at different ways objects can communicate, I've noticed we typically separate objects into categories of entities and values. Entities having identity and state and values having immutable state and their state == their identity. I think in GOOS, we have the advice "tell objects, ask values", making that distinction between the two categories. But there is another category that I see, which I call "agents". I think this is the "something else" that Nat was referring to, RE: "what that other thing is (if not a value)." Agents don't have identity or state and they tend to represent an anonymous operation or another system that can just "do things". This is the category that the ValidatesFundsTransfer and associated objects from my example fall into.
I think if we turned the funds transfer concept into an entity by giving it identity and state, it might look something like this:
// This is likely a incorrect model without knowing the actual constraints of financial transfers,
// but it works for the purpose of discussion in contrasting the above TransfersFunds agent to an entity
class FundsTransfer {
private TransferId id;
private AccountId fromAccountId;
private AccountId toAccountId;
private Amount amount;
private TransferState state;
FundsTransfer(...) {
this.fromAccountId = fromAccountId;
this.toAccountId = toAccountId;
this.amount = amount;
this.state = TransferState.unexecuted();
}
void execute(TransferSource source, TransferDestination destination) {
source.requestDebit(fromAccountId, amount);
destination.requestCredit(toAccount, amount);
this.state = TransferState.pending();
}
}
Thoughts?
--
Pete