Relationships are more fun (II)
On the duality between Relationships and Interactions
Trygve Reenskaug and James Coplien have just posted an article questioning the fundamentals of Object Orientation and the Model-View-Controller pattern. It's interesting. I don't agree with all of it (more later). But their characterisation of the relationship between the User's mental model and the Object model represented in software is spot on. Perhaps more interesting is their questioning of one of OO's central tenets: that all behaviour belongs in Objects (or Classes in programme language terms). They assert that, whilst some behaviour does belong within the Object, much doesn't. To use their example: responsibility for increasing or decreasing an Account's balance properly lies with the Account. External parties can ask the Account to perform those operations, but it's up to the Account to do it. That's fully in line with the principles of Abstract Data Types on which OO is founded. But what about performing a Transfer of funds from one Account to another? Where does that belong? Reenskaug and Coplien assert this doesn't belong with the Account Object - I agree. But that's how it would typically be handled in today's OO languages; so the Account class would have a method named something like transferTo(), used as follows:
What's wrong with that? From a mental model perspective, it doesn't work. When I think about transferring funds, I don't think about the responsibility lying with either the source or destination account. Sure they're both involved, but neither is responsible. So where does it belong? In OOP terms we have nowhere else to put it other than in a Class. Reenskaug and Coplien suggest the notion of Interactions as first class entities; however their model still portrays an asymetric pattern (i.e. the responsibility for executing the transfer still lies with either the source or destination objects). I prefer to think about it lying outside of either. But that doesn't mean the OO model is broken. Maybe it's just the way we use it today. Coming back again to the code example above:
Now what happens if we look at this from a relationship perspective? What's the nature of the relationship? It's pretty obvious there's some link between the source and destination accounts - even if it's only transient. Here's an initial view as a UML class diagram:
Both source and destination are instances of Account, so the relationship would have to be recursive on Account. OK. Naming the ends? Easy; one is source, the other destination. Cardinality? Hmm, that's a bit more difficult. A given transfer has exactly one source and one destination account; and any account can be the source and/or target for many transfers. But we can't represent that on our relationship:
- 1:1 would only allow each account to be related to a single other: i.e. at most one transfer.
- many:many would only allow each account to be related to every other; but for every pair there could only be one transfer. Again, not what we need.
Let's revisit naming the relationship ends. I used "source" and "destination" above, which are role-based names. As I've stated previously, I prefer verb-based names because they bring out the "why" of the relationship. So how would we name the relationship ends using verb phrases?
Each Account may be the transfer source of one or more other Accounts
Each Account may be the transfer destination of one or more other Accounts
Kinda works, but it looks clumsy; it smells bad. So the cardinality doesn't work and the relationship naming is ugly. What does that tell us? Maybe there's no relationship? Well that's not true. It may be transient but both accounts are undeniably involved in something - otherwise there's no transfer. Transfer. Hmm. What if, rather than looking at transfer as a verb, we treat it as a noun? What would that mean? Let's rework the model:
Examining the relationships again:
Each Transfer must debit exactly one Account
Each Account may be debited in one or more Transfers
An Account need not be debited in any Transfers
Each Transfer must credit exactly one Account
Each Account may be credited in one or more Transfers
An Account need not be credited in any Transfers
That's much better. The cardinality and phrasing capture exactly what we're trying to express - so the relationships look OK. But what about that "Transfer" class? Is it a proper class? What does it mean to be a "proper" class? It's definitely an abstraction of behaviour - the transfer behaviour. What's more, it's a well encapsulated abstraction of behaviour. We are not loading the Account class with the responsibility for executing transfers; it need only respond to "credit" and "debit" requests: appropriate since it maintains the account balance. What about state? That's OK too. The transfer value again properly belongs with the Transfer. We could also, if required, capture details of the time and success/failure of each transaction. Finally, we could, if required, track the state of each Transfer as it executes ("debiting source account", "crediting destination account", "completed successfully", ...). There are critics of this approach who disagree with the idea of reification - treating behaviour as things. Rather, objects should be tangible things: Accounts, Customers, Chairs, Telephones, whatever - the things that are things in the real world. The trouble is that doing so causes the situation that leads to a.transferTo(b). In fact, most of the interesting behaviour doesn't happen in the tangible objects. Transfer is an example of an Interaction object; one that coordinates a protocol amongst other objects. Interaction objects are useful and interesting precisely because they enable clean abstraction of behaviour amongst a number of other objects.
So is the OO model broken? That's an interesting one. I'd argue the OO model isn't broken, but it's implementation in today's programming languages is too limited. Specifically, the OO model allows for Relationships as first class entities. Programming languages however don't - instead they demote relationships to pointers among classes. That's a key contributor to scenarios like a.transferTo(b).
In that sense I'm with Coplien and Reenskaug. 'Interactions' and 'relationships' are two sides of the same coin. Relationships capture static structure among classes, Interactions focus on the dynamic. With richer modelling constructs, the Transfer class in the example above would be 1st class relation, complete with its own behaviour - which would control the interaction. Adding such a construct with first class status would help increase focus on collaborations. Key to that is learning to love relationships - they're where all the fun happens.