Message Obsession
I’ve noticed a “code smell” in object-oriented code that I call “Message Obsession”. I find Message Obsession causes similar difficulties to Primitive Obsession. However, Message Obsession appears to be the complete opposite of Primitive Obsession. Refactoring to address the difficulties caused by either Primitive Obsession or Message Obsession leads to the same design.
To demonstrate the Message Obsession, and to compare it to Primitive Obsession, imagine a game in which a robot moves around a grid of tiles.
Primitive Obsession
The code below, which moves the robot east and then north, suffers from Primitive Obsession. Domain concepts – direction of movement, in this case – are held as multiple primitive data types instead of being modelled explicitly.
robot.move(0,1)
robot.move(-1,0)
It’s awkward to work with this interface because you have to pass the deltaX and deltaY coordinates around as a pair of values instead of as a single Direction value. This can lead to code duplication. Any code that has to store moves will have to define a structure to do so. Code that has to perform calculations on directions – to add, invert or rotate directions, for example – will do so via helper functions or inline calculations. This logic ends up duplicated all over the code because programmers working in one area do not know that programmers working in other areas are writing the the same calculations.
Primitive Obsession can also lead to errors. Are the coordinates passed to the move function as x and y or row and column? The type system won’t catch the error because both coordinates are integers. Named parameters help, of course, but not all languages support them.
Message Obsession
Programmers know about the drawbacks of Primitive Obsession, but sometimes create designs that go too far the other way, to the smell I call “Message Obsession”. A design that suffers from Message Obsession does not pass around primitive values – it does not pass around values at all. Instead, objects implement several similar methods, each of which manipulates the same fields used to store primitive values inside the object.
Message Obsession leads to code like:
robot.moveEast()
robot.moveNorth()
It’s awkward to work with this kind of interface because you can’t pass around, store or perform calculations on the direction at all.
Symptoms
Symptoms caused by this smell include…
…class hierarchies that mirror the direction methods. For example, a hierarchy of Command classes with a concrete class for each direction the robot can move:
interface MoveCommand {
fun apply(r: robot)
}
object MoveNorth : MoveCommand {
fun applyTo(r: robot) = r.moveNorth()
}
object MoveSouth : MoveCommand {
fun applyTo(r: robot) = r.moveSouth()
}
... etc. ...
…dispatching with conditional statements:
when (keyCode) {
UP_ARROW -> robot.moveNorth()
DOWN_ARROW -> robot.moveSouth()
... etc. ...
}
…using anonymous functions to treat directions as first-class values.
val movesByKeyCode : Map<KeyCode,(Robot)->Unit> = mapOf(
UP_ARROW to {robot -> robot.moveNorth()},
DOWN_ARROW to {robot -> robot.moveSouth()},
... etc. ...
)
Cure
The duplication in the method names shows that, as with primitive obsession, there’s a value type to be factored out. Let’s call it “Direction”. The names of the methods show that we’ll need some constant Direction values and what those constants should be called.
We really want code that moves the robot to look like:
robot.move(east)
robot.move(north)
We could define the Direction type as:
data class Direction(val deltaX: Int, val deltaY: Int)
And direction constants as:
val north = Direction( 0, -1)
val east = Direction( 1, 0)
val south = Direction( 0, 1)
val west = Direction(-1, 0)
Now directions are first class values, they can be stored in collections, mapped to and from user input events, rotated, inverted, etc. For example:
val movementKeyBindings: Map<KeyCode, Direction>
The value type acts as an “attractor” for behaviour. We can add operations to it to rotate or invert directions, for example.
fun Direction.inverse() = Direction(-deltaX, -deltaY)
fun Direction.rotatedClockwise() = Direction(deltaY, -deltaX)
fun Direction.rotatedAnticlockwise() = Direction(-deltaY, deltaX)
... etc. ...
Those rotatedClockwise and rotatedAnticlockwise methods also exhibit Message Obsession! Maybe we will need to introduce a RightAngleTurn
type as the system evolves…
Implicit Duplication
Sometimes the Message Obsession smell is not obvious from the names. The duplication in naming is missing or implicit.
For example, in the term “move” is not used in following code, but is implied by the behaviour of the east
and north
methods.
robot.east()
robot.north()
The missing concept of “move” has to be named before the duplication stands out.
Refactoring too Far, and Back Again
I’m struck by how Message Obsession acts as an opposite of primitive obsession. I have often seen Message Obsession appearing as a reaction to Primitive Obsession, pushing the design too far in the opposite direction. The design eventually settles on a compositional style as the need for first-class values arises and the drawbacks of Message Obsession become clear.