A perennial debate that I enjoy wading into is the classic “Should it be kick(ball) or ball.kick()?”
This seems to reveal a fundamental dichotomy in our shared understanding of Object-Oriented Programming.
It’s a trick question, of course. If the effect is the same – the displacement of the ball – then kick(ball) and ball.kick() mean exactly the same thing. But the debate rages around who is doing the kicking and who is being kicked.
Many programmers quite naturally assign agency to objects, and object (pun intended) to the ball kicking itself. Balls don’t kick themselves! They will often counter with “It should be player.kick(ball)“.
But this can lead us down the rabbit hole to distinctly non-OO code. Taking an example from a Codemanship training course about an online CD warehouse, the same question comes about whether it should be cd.buy() or warehouse.buy(cd).
Again, the protestation is that “CDs don’t buy themselves!” I can completely understand why students might think this, having had it drummed into us that objects do work. (Although why nobody ever objects that “Warehouses don’t buy CDs!” is one of life’s little mysteries.)
I’m the first person to say that object design should start with the work. Then we figure out what data is required to do that work. Put the data with the work. And, hey presto, you got an object. Assign one job to each object, and get them talking to each other to coordinate bigger jobs, and – hey presto! – you got OOP.
(The art of OOP is really in deciding where to put the work, and that’s what this debate is essentially all about.)
But warehouse.buy(cd) – in the training exercise we do – can lead us into deep water regarding encapsulation. The are told that the effect of buying a CD is that the stock count of that CD goes down, and that the customer’s credit card is charged the price of that CD.
So our test looks a bit like this:
The implementation that passes this test suffers from a distinct case of Feature Envy between Warehouse and CD, because buying a CD requires access to a CD’s stock and price.
When we refactor this code to eliminate the Feature Envy (i.e., to encapsulate the work)…
…we end up with a CD that – shock, horror! – buys itself!
This refactoring is typically followed by “But… but….”. Placing this behaviour inside the CD class conflicts with our mental model of the world. CD’s don’t buy themselves!
And yet we encounter objects apparently doing things to themselves in OO libraries all the time: lists that filter themselves, database connections that open themselves, files that read themselves.
And that’s what’s meant by “object-oriented”. The CD is the thing being bought. It’s the object of the buy action. In OOP, we put the object first, followed by the action. Read cd.buy() not as “the CD buys” but as “buy this CD”.
Millions of people around the world read OO code the wrong way around. The ones who tend to grock that it’s object-oriented are those of us who’ve had to approximate OOP in non-OO languages – particularly C. (Check out previous posts about encapsulating in C and applying SOLID principles to C code.)
Without the benefit of an OO syntax, we resort to defining all the functions that apply to a type of data structure in one place, and the first parameter to every function is a pointer to an instance of that structure, usually named this.
Then we might hide the data definition of the structure – just declaring its type in our .h file – in the same .c implementation file, so only those functions can access the data. Then we might define a table of virtual functions – a “v-table” – that can be applied to that data structure, and attach the data structure to them so that clients can invoke functions on instances of the data structure. Is this all starting to sound familiar?
The set of operations defined by a class are the operations that can be applied to that object.
In reality, objects don’t do work. The CPU does. The object identifies the thing – the record in memory – to or with which the work is to be done. That’s literally how object-oriented programming works. cd.buy() means “apply the buy() function to this CD”. list.filter() means “filter this list”. file.read() means “read this file”.
The idea of objects doing work, and passing messages to each other to coordinate larger pieces of work – “collaborations” – is a metaphor. And it works just fine once you let go of the idea that balls don’t kick themselves.
But words are powerful things, and in programming especially, they can get tangled in our mental models of how the problem domain works in the real world. In the real world, only life has agency (well, maybe). Most things are acted upon. So we have a natural tendency to separate agency from data, and this leads us to oodles and oodles of Feature Envy.
I learned to read object-oriented code as “do that to this” a long time ago, and it therefore has no conflict with my mental model of the world. The CD isn’t buying. The CD is bought.
I’ve been very much enjoying the ensuing furore that suggesting ball.kick() means “kick the ball” inevitably starts. The fun part is reading the “better” designs folk come up with to avoid accepting that.
player.kick(ball) is one of the most popular. Note now that we have two classes instead of one to achieve the same outcome.
Likewise, cd.buy() seems to have offended the design senses of some. It should be cart.add(cd), they say. Again, we now have two classes involved, and also the CD didn’t actually get bought yet. And it also kind of proves my point, because the CD is being add to the cart.
On a more general note, when students go down the warehouse.buy(cd) route, I ask them why the warehouse needs to be involved if we know which CD we’re buying.
object.action() tends to simplify things.