Views and The Single Responsibility Principle (very wonkish)
In object-oriented design, the single responsibility principle states that a class should do one thing, or more specifically that a class should have one reason to change. This has been much discussed lately, for example in this discussion of controllers, or in the premiere issue of the estimable objc.io. It’s a touchstone of refactoring and a central rubric of clean code.
And it’s a true headache.
Take Tinderbox Six and its map view.
The framework expects that the view do several things:
- draw itself, which primarily means drawing the links and the focus indicator.
- manage its subviews, creating them as they scroll into view, recycling them when they are removed from sight, and handling the timer and view controller for the hover expression.
- receive mouse clicks and other events.
- handle things dragged and pasted from other windows or applications.
If you let TbxMap do all those things, it becomes a monster class. Refactoring should be capable of fixing things. But can it?
Simplifying a View
In Tinderbox Six, I began by moving all the event-handling logic from TbxMap to its controller. That’s a win, though a bunch of small methods need to remain in TbxMap to forward events it receives to the controller. (The controller is quickly bloated by this refactoring, of course, and moves all those command handlers to dedicated helper objects — each requiring a tiny stub of its own.)
Then, I moved lots of drawing logic to dedicated classes. The drag highlights that appear when dragging a note over a container move to DragHighlighter. All the layout and drawing of links moves to LinkAnimator. All the logic for figuring out what the user is dragging into Tinderbox and what to do with the stuff moves to a class cluster behind TbxDrag. This slims TbxMap down considerably. Again, tiny stub methods must be left behind because the framework, or other classes in Tinderbox, expect the view to receive them, but all the work is done by small dedicated classes.
More recently, I pulled just about all the remaining logic from TbxMap into a collection of helper classes known only to the map view.
Remember, this is just the part of Tinderbox that draws maps and outlines and charts.
The helper classes are the blue lozenges; I've suppressed lots of detail for (comparative) clarity. The Layout helper knows where things go; the Valet knows how to put them away when they’re off-screen and how to fetch them again when needed, the Locator knows how to locate them for the Find Bar, the Selector knows how to select them. They each talk mostly to the TbxMap (and the underlying data, of course). TbxMap is now, essentially, a hollow shell, with almost no logic beyond relaying incoming methods to the appropriate helper. It’s a collection of stubs, plus some initialization and teardown logic.
To be precise, it’s a collection of 182 stubs.
182 stubs!
This is madness, of course, but it’s the natural tendency of the natural refactorings. Sprouted classes need to use the original class as a Facade so other classes can use them. So each sprout removes a pile of logic and replaces it with a few simple methods. Those methods pile up. The helpers often need to interact with other helpers; if you don’t want a terrible tangle of interdependent helper classes, you’re going to need to use the TbxMap as a Facade between the helpers.
The alternative is to provide other classes with direct access to the helper class. This means we only need one or two accessors for the helper, but now the clients need to depend on the helper class. And they still need TbxMap, because that’s where they get the helper. And every time they use the helper, they smell of feature envy and indecent exposure and they violate the Law of Demeter.
It’s not just Cocoa. Every window system that ever lived suffers from something like this affliction. Either the system routes everything through the view (or the controller) , which leaves a complex view looking like an old-fashioned switchboard operator, or instantiating a simple pane makes you create a whole slew of of handlers and delegates and thumgummies, and that drives programmers bats. Remember OpenDoc?
A Path Forward?
I can envision some ways to make this better. TbxMap is acting as a Facade or Mediator between lots of disparate classes; we might split out the role of coordinating the helpers into one object and the role of serving the Controller and the rest of the system into another. That might help — or might just entangle all the helpers with each other.
I can envision that I’ve simply hit the wrong set of abstractions, and that some other collection of helpers would suggest a natural way for the helpers to work together without knowing too much about each other. But I’m skeptical.
It’s possible that the Tinderbox Map is just too complicated. Again, though, I’m skeptical: it hardly seems that complex; it’s just shapes and links and annotations.
I’m really surprised that there’s not more literature on this conflict between proliferating sprouted classes and the Law of Demeter. I’ve read the obvious sources: Fowler, Beck, Feathers, Kerievsky, Bob Martin, the Pragmatics. Surely, someone has a more extensive discussion?