Continuing my chapter-by-chapter review of A Philosophy of Software Design By John Ousterhout, today I’m going to review Chapter 8.
Chapter 8: Pull Complexity Downward
This chapter has two sentences in the introductory paragraph that I find particularly interesting. The first one is my favorite, but the second one was bolded by the author:
Most modules have more users than developers, so it is better for the developers to suffer than the users.
it is more important for a module to have a simple interface than a simple implementation.
The first sentence more closely aligns with my own personal mantra, and I would extend it a few more levels: Developers of shared services and shared libraries should “suffer” (do more high-complexity encapsulation and quality control) than developers of the downstream application. Your senior developers and subject-matter experts should be “suffering” (encapsulating their own complexity) so that junior developers and non-experts don’t have to.s
Pass-through methods
A method which passes its parameters down to the next layer without making significant changes or adding value is a “pass-through” method, a problem when different layers should have different abstractions.
This typically indicates that there is not a clean division of responsibility between the classes.
Yes, your Single Responsibility Principle senses should be tingling. A single method in a class which passes arguments down without modification might not be a problem. After all, an abstraction means that the underlying implementation is None Of Your Business, and the actual implementation might be a simple pass-through. If you have a class with several of these methods, however, it’s an indication that you’ve probably drawn your abstraction boundaries in the wrong place.
Adaptors and Facades seem like they should be immune from this criticism, by design. For the most part I agree that passthrough methods are usually a code smell.
Aside: Parrot’s C API
When I was still working on the Parrot project, one of the features I worked on extensively was the public API for C embedding. The goal was to be able to embed a Parrot interpreter into other applications and be able to make calls from C code into Parrot and vice-versa. The problem is that there is an immediate mismatch between the inside of Parrot (which uses it’s own Exception-handling mechanism) and the outside of Parrot where C doesn’t have exceptions and C++ has it’s own Exception-handling mechanism.
The API layer often passed request parameters more-or-less unmolested into the underlying functions of the interpreter. What changed was the ambient error-handling strategy: The API layer would set up an exception handler and return error information as a false
flag to the caller. Outside the API layer, code would operate with the normal C idioms of status flags, but inside the API layer code would operate with Exceptions. While the parameters were technically “pass through”, the abstraction was significantly different: One one side you could safely throw exceptions, and on the other side you couldn’t.
Decorators
As promised, John does discuss the Decorator pattern directly (though he doesn’t mention other similarly-afflicted patterns like Facade, Bridge or Adaptor).
decorator classes tend to be shallow: they introduce a large amount of boilerplate for a small amount of new functionality. It’s easy to overuse the Decorator class…
Call it a bit of a heuristic, but I would try never to use the Decorator pattern for classes which have a large public interface. Interface Segregation is your friend here. Smaller public interfaces mean less boilerplate for your Decorators. I’d hate to draw too deep a line in the sand, but I probably wouldn’t use decorators if the interface
I was wrapping had more than 4 or 5 public methods exposed. Your mileage may vary.
Acquaintance makes heavy use of Decorators, but that system is all about composing pipelines. Decorators make that mechanism much cleaner and easier to modify. But, because it makes such common use of Decorators, the interfaces which are decorated are intentionally kept small.
Pass-through variables
This next section I think best deserves to be given in John’s own words:
Another form of API duplication across layers is a pass-through variable, which is a variable that is passed down through a long chain of methods.
The solution I use most often is to introduce a context object… A context stores all of the application’s global state…
Contexts are far from an ideal solution
Unfortunately, I haven’t found a better solution than contexts
I’m in the same boat, I’ve never found a better solution to the problem than a context object, and I don’t feel like context objects are that great a solution. Context objects do offer a few advantages:
- They hoard all the mutable state, allowing many of your interactor and utility classes to be immutable
- They provide a common “language” for multiple interactor and utility classes on the same layer to speak
- They allow the easy extension of an operation without needing to modify existing steps
A pattern that I’ve used pretty frequently is for a Service Layer method to create a Context for an operation and pass that context into an ordered list of steps in lower-level interactor objects. At the end of the service method we can .Commit()
any pending operations or Units of Work and return our results. I have found this technique to be powerful, at least from the point of view of the service. The Context object, on the other hand, often becomes a dumping ground for unrelated bits of mutable state, and I haven’t quite figured out a better way to organize that state. In the end, just having enough discipline to not pass around things you don’t really need is the best piece of advice I can offer here.
I’ve experimented with callback lambdas to replace contexts and seen some interesting results. Though, ultimately, I never thought they were superior to a context object in terms of either code simplicity or system performance.
Conclusion
Pass-through is generally bad, and every new method should add some value to it’s parameters before just passing them on to the next method. These seem like good recommendations to me.