Continuing my chapter-by-chapter review of A Philosophy of Software Design By John Ousterhout, today I’m going to review Chapters 5 and 6.
Chapter 5: Information Hiding
This chapter is all about abstraction, and is quite nicely summed up by the quote:
The basic idea is that each module should encapsulate a few pieces of knowledge, which represent design decisions. The knowledge is embedded in the module’s implementation but does not appear in its interface, so it is not visible to other modules.
This is a great, concise statement of the principle of abstraction and I don’t think I can elaborate on it any further.
The problem with information hiding (and it’s converse, information leakage), is that all abstractions leak. My ability to design abstractions improved dramatically (at least, by my own self-assessment) when I learned this. The faster you realize that all abstractions leak, the faster you come to your first fork in the road:
- Do I make the abstraction larger and more complicated, to try and reduce the leakage as close to zero as possible, or
- Do I abstract the most common case only, and allow the advanced user to bypass my abstraction in the rare cases?
Acquaintance often takes the first approach, trying to hide most of the implementation details because of the underlying complexity of threads, Task<T>
and synchronization primitives. These things are so difficult to get right, even in the common case, that it’s worth trying to hide them.
Conversely, CastIron takes the second approach, giving quick and easy access to the underlying bits the like IDbConnection
and the IDataReader
for people who want that. These underlying bits are much easier to understand than concurrency primitives, people have more experience with them, and there are fewer opportunities to shoot yourself in the foot.
Anyway, my point is that leak-free abstractions are a nice ideal but the reality is that all abstractions leak, so picking a mitigation strategy is important.
Temporal Decomposition
This is a term I haven’t heard too much before and I’m glad John discusses it here:
In temporal decomposition, the structure of a system corresponds to the time order in which operations will occur.
Temporal decomposition is a problem because often the order of operations doesn’t correspond to the single responsibilities that classes are supposed to encapsulate. Reading a data format and writing that same data format often happen at different times, so it’s tempting to have two classes: one to read and one to write. But this involves separating out the knowledge of the file format into two places, which violates the Single Responsibility Principle and causes all sorts of problems.
I know I’ve failed on this measure before, and I’ll be trying to do a better job with it in the future.
Example: ElasticSearch
I’ve done some work on a CRM application which had an SQL primary database but used ElasticSearch as a secondary for fast full-text searching. At the time we were planning for some possible microservice decompositions, so the system was designed with some CQRS concepts in mind: For each indexed type we had a class to perform the queries, and a separate class to do the data updating (mostly simple upserts and deletes). For each type we also had a utility class, in a separate maintenance program we did not deploy to the server, for creating the mappings during rebuild operations. Later we added a fourth set of classes to handle bulk seeding operations for maintenance purposes, so that we could have specially optimized DB queries and fast bulk index inserts (these, again, were in a maintenance program and not deployed to the server). This worked out pretty well for a while, we were able to make necessary modifications to specific classes depending on need, and everybody knew exactly where to go to make which kinds of changes.
But then somebody came to me and said, “We want to add a new type to the ElasticSearch index, can you tell me how to do it?”. When I started to list out all the classes that needed to be added (and those that needed to be modified to wire up the additions), I realized immediately how complicated the system had become and how hard it would be for anybody but the core development team to add a new type to the search index. What we should have had instead was a single class per indexed type, which would provide (or delegate) all the logic for that type. Sure we would have some logic living on the production server which was never called from there, but that’s an issue which would have been easy to mitigate.
Information hiding within a class
Try to design the private methods within a class so that each method encapsulates some information or capability and hides it from the rest of the class.
I disagree with this statement. A class represents a single idea, and it makes no sense to me that we would try to hide one aspect of an idea from the rest of that idea. If you need separating and hiding, delegate to another class.
The recommendation to “delegate to another class” is, of course, impossible if your thesis is that classes should be larger and deeper.
The role of private methods within a class is to organize the implementation. These can be used for code sharing (where, for counterexample, class inheritance should absolutely not be used for code sharing), or simply for giving a descriptive name to a block of code. If you really need a separate idea with separate implementation details which require hiding, use a separate class. This is what classes are for.
As an aside, this is why I find testing tools which allow unit testing of private methods so abhorent. Private methods are the quintessential “implementation detail” and private methods should be allowed to change and evolve quickly and easily to allow maximum flexibility to refactor and improve code. Anything that decreases this fast flexibility is a nightmare.
As a second aside, I wish that “private methods” and “public methods” weren’t both considered “methods”. It’s not just different visibility on the same idea, the two constructs have radically different purposes: Public methods are the public interface of the class and define it’s interface. They rarely change because changes to public methods imply ripple changes downstream. Private methods, on the other hand, are an implementation detail for organizing code within the class; they can and should be changed often during clean up and refactoring operations. If you need to encapsulate separate, new ideas, you should delegate to a new class. Classes are the mechanism for encapsulation and information hiding, private methods are not.
Taking it too far
If the information is needed outside the module, then you must not hide it.
This part seems like obvious advice. But maybe it raises a small nit pick that we might not be using suitable vocabulary? If the word “information” refers to all sorts of stuff which may be internal or external, which may be needed or may not be, which may be business logic or may be about implementation and structure, and if that overloaded term creates confusion by obscuring what parts need to be “hidden” and which parts need to be “exposed”, we might want to consider words that are more precise.
There’s a difference between information which belongs to the caller and information which is private to the module, and understanding which is which is a non-trivial exercise, made more difficult in this case by tricky vocabulary. Getting the two types of information mixed up can cause headaches.
Chapter 6: General-Purpose Modules are Deeper
Chapter 6 is short, straight-forward, and uncontroversial. He starts with a clear statement of a decision which affects almost all designs:
The general-purpose approach seems consistent with the investment mindset … some might argue that it’s better to focus on today’s needs, building just what you know you need and specializing it for the way you plan to use it today.
This is a pretty serious dilemma when it comes to software design, and it rarely seems like there are wins to be had. Damned if you do, damned if you don’t. There’s always a strong “You aren’t gonna need it” (YAGNI) mindset to fight against, but then there’s always that nagging voice saying “yeah, but you might actually need it”. When you choose the general purpose approach and never end up reusing it, people will say “what a stupid waste of time” but when you choose the specific approach people will complain “if you had just done this right the first time, it would save me a lot of work today.
John does provide some guidance:
The module’s functionality should reflect your current needs, but … the interface should be general enough to support multiple uses.
I might also add some guidance of my own: If you are exposing the functionality in a shared library, you might consider being more general where if you have embedding functionality in a single application you might err more on the YAGNI side and be more specific.
Conclusion
The rest of chapter 6 is mostly devoted to an example of a text editor, so I won’t delve into it any further here. Instead, I do recommend you pick up your own copy of the book and get all the delicious details for yourself.
I find that a lot of people know what Abstraction and Encapsulation are, in theory, but few programmers are able to really understand and apply these ideas to their code in an effective way. John’s discussion of these topics, especially in these two chapters, provide a very practical foundation for using these principles. Despite a few of my own little caveats and disagreements, I’m very grateful that he’s covering these topics in such a thoughtful and reasoned way.