This post expands on a section in my Writing Code That Lasts Forever talk.
When I was learning object oriented programming I struggled to define boundaries between classes. Should a Chess game’s Bishop class have a
move() method to reposition itself on the board? Should there even be a
BishopMove? Which of these types should have interfaces? What changes if the game is online? Has undo?
So I learned to do it by feel. Follow patterns from the codebase, do what the JDK does, and refactor when I get it wrong. But it was still unsatisfying. Certain classes were difficult to test because of how they were structured. Other classes were easy to test but their tests were mostly ceremony. Does
Bishop.setLocation() work? Whoop-de-doo.
It was only after I got serious about immutability and dependency injection that I found a reliable strategy for dividing responsibilities among classes. All code serves one of 3 purposes.
These model data. It's natural to represent these using JSON, protocol buffers, or SQL.
They should be immutable. For stateful entities like
ChessPlayer, the value object holds a snapshot of the whole entity or some aspect of it.
They mustn’t do anything fancy: no I/O, no RxJava, no coroutines, and no threading.
By accepting these limitations, value objects are easy to define and use. Kotlin has built-in support via data classes! Most value objects don’t need tests; there’s nothing interesting to test!
These make decisions and have side-effects. They send pixels to the screen, files to the disk, or packets to the network.
Service objects are composable: they may depend on other service objects. For example, the
MoveRecommender might need a
BoardRanker and a
PredictionPruner to do its work.
I test these. For the most expressive tests I like to define a service interface, a real implementation, and a fake. Doing so lets me test
MoveRecommender independently of the services it composes. Fakes also mean my tests run really fast and that makes coding fun.
The previous two categories are well known and well understood. But they aren’t enough for a full application! I need a
main() method or an
Activity or something to serve as my application’s entry point.
Glue is what I call my application’s plumbing. It’s the Jetty Servlets, Android activities, Dagger modules, RxJava endpoints, and Hibernate entities.
The problem with glue code is that it’s awkward to test. This is not to say that many heroes haven’t tried. Espresso attempts to test Android activities. In Misk, we launch databases and web servers so we can test arbitrary APIs. We worked very hard to make testing easy but even so the tests are neither fast nor simple.
Avoid Sticky Situations
I avoid mixing glue (difficult to test) with behavior (necessary to test). If the glue is simple enough, I don’t have to test it!
And I use interfaces so that my service objects don’t interact directly with glue.