Concretely, this post is about a guideline for object-oriented and imperative programming, but the underlying principles and conclusions should be equally applicable to some other paradigms. One example here uses Cocoa Touch and Objective-C pseudocode, but again the principles described apply equally to any framework.
It’s very easy to make mental inferences — logical jumps — while coding. As a result, programmers tend to use one condition as a proxy for some other meaning without even thinking about it.
But this is dangerous; it makes your code less readable, and even in simple cases hurts maintainability. This should sound obvious, but avoiding the pitfalls which can arise takes some conscious thought.
The problem isn’t strictly limited to conditionals, but it’s easiest to demonstrate in that context, so let’s try.
Consider a hypothetical
UITableView which is itself contained, in a few different apps, within another container view. That container view might be something that scrolls, because one of these apps (“App B”) allows the user to switch tabs with a pan gesture. When that’s the case, we need to disable swipe-to-delete for this table view, otherwise the gesture recognizers in this view hierarchy won’t work well together.
A simple test might be something like:
This will work fine, but it’s a poor solution for at least three reasons:
- The test is too specific; it will break when this view controller is reused inside a scroll view in a different app. Similarly, it will also seem to break swipe-to-delete if App B’s navigation architecture is redesigned.
- It relies on the programmer understanding the implication that App B has a specific navigation architecture. This is not obvious to developers new to the codebase. (And even programmers who know the codebase well may not immediately see the connection between “swipe-to-delete being disabled” and “this view being embedded in App B”.)
- It makes the view controller aware of more context than necessary; at a minimum, this adds unnecessary a priori knowledge to the view controller, and at worst it adds some other dependency.
A better solution might walk up the view hierarchy and check for scroll views.
(A gentle reminder: this is Objective-C pseudocode; I’ve adopted it slightly from formal ObjC to make it more accessible.)
This is better; it removes any additional explicit dependencies, any programmer reading it should immediately understand why this code exists, and there’s very little a priori knowledge left in this logic. This would be a fine stopping point.
Still, it can be improved. We don’t really care whether the superview is a scroll view, we care whether it scrolls:
This is pretty good. Here, instead of making the assumption that “
UIScrollViews can scroll”, we check whether scrolling is enabled for each view in the hierarchy.
One can imagine a further improvement wherein we actually check each view for pan gesture recognizers which might conflict with ours, but I’ll leave this as an exercise for the reader.
Here’s a simpler, less-concrete example. Each version of iOS adds new features, fixes bugs, adds new APIs, and deprecates older ones.
To deal with this at runtime, a naïve approach could check the device’s iOS version and take a different action based on that check. A smarter approach could check for the availability of a new API immediately before trying to use it, then fall back on the old API if the newer one isn’t available.
There are just two examples, but I’ve seen the same problem, had a resulting discussion, and implemented similar solutions in hundreds of cases.
Prefer writing explicit code vs. code that has implicit, a priori knowledge. Write code that does what you want explicitly, not code that does so through an assumption or logical leap.
Think, “what am I actually trying to accomplish?”, and write code that does that and will always do it, not code that does the right thing now but might fail down the road.
Implicit code lends itself to unmaintainability. It’s hard to reason about, and developers working with it must make the same mental leaps while maintaining a codebase as the original programmer did. Assumptions are missed, and bugs are introduced.
In some cases, a compiler may help enforce conditions in explicit code which are impossible to check in implicit code.
There should be a clear tie between a conditional and the code in each branch; if you have to think about this relationship, the conditional should be clarified. (Sometimes extracting a complex boolean statement into a well-named variable is all that’s required.)
Note that “explicit” code actually rises to a higher level of abstraction than “implicit” code. The terminology here is confusing; one might think that implicit code would be abstract, but (as in the table view example, above) implicit code is often tied to specific implementation details. Implicit code usually uses a concrete implementation detail as a proxy for some more abstract concept which is really at issue.
The Tell, Don’t Ask principle can really help you here.
Using TDA within a well-designed class hierarchy often helps you avoid writing any conditional, explicit or otherwise. It encapsulates conditional behavior inside the program’s class hierarchy, and you can simply write clean OO code that declares behavior and is a bit less imperative (ie. involves fewer conditional statements).
Objective-C/Cocoa developers are used to this already, but it bears repeating: verbose code tends to be more readable and maintainable.
Explicit code should read nicely, with clear logical relationships among its branches and tokens.
- Choose good names for things; describe what they are, don’t abbreviate unnecessarily, and in lieu of optionals don’t hesitate to add
OrNilto variable names.
- Extract the boolean statement from a complex conditional into its own, well-named variable.
The table view example from above could be rewritten as such, further clarifying the intent of the conditional:
Obsessively writing good documentation for your classes will help you realize where pain points in your API are. It’ll help you and others maintain your code in the long run.
Both of these factors make it much easier to write good, explicit code.
I feel like the Single Responsibility Principle plays into this as well, but I haven’t had time to really think through this point in detail.
At a high level, maintaining unnecessary a priori knowledge (in the form of implicit assumptions) is an additional responsibility, and thus should be avoided.
Incidentally, obsessively writing good documentation will naturally help guide your classes toward SRP adherence.
- Write code that does what you want explicitly, not code that does so through an assumption or logical leap. Avoid writing a priori knowledge and logical assumptions into your program.
- Use Tell, Don’t Ask rather than writing conditionals when possible.
- Explicit code should read nicely, with clear logical relationships among its branches and tokens.
- Choose good names to make your code read well.
- Extract boolean statements from complex conditionals to further enhance readability.
- Obsessively write good documentation.
- Adhere to the Single Responsibility Principle.
The resulting code will be clearer, less fragile, more readable, more maintainable, more portable, have fewer dependencies, and will better adhere to best practices.
If these practices feel heavyweight in your application, I submit you likely have larger architectural problems which might be addressed by better adhering to OOP best practices.