Chris Dzombak

Explicit Programming

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.

An example: a table view

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:

if (app.isAppB) {
    allowDeletionViaSwipeGesture = NO
} else {
    allowDeletionViaSwipeGesture = YES
}

This will work fine, but it’s a poor solution for at least three reasons:

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.)

allowDeletionViaSwipeGesture = YES
UIView superview = tableView.superview
do {
    if ([superview isKindOfClass:[UIScrollView class]]) {
        allowDeletionViaSwipeGesture = NO
    }
} while (superview = superview.superview)

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:

allowDeletionViaSwipeGesture = YES
UIView superview = tableView.superview
do {
    if ([superview respondsToSelector:isScrollEnabled] && superview.isScrollEnabled) {
        allowDeletionViaSwipeGesture = NO
    }
} while (superview = superview.superview)

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.

Another example: iOS version-specific features

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.

The key point

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.

Related best practices

Tell, Don’t Ask

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).

Write verbose code

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.

The table view example from above could be rewritten as such, further clarifying the intent of the conditional:

allowDeletionViaSwipeGesture = YES
UIView superview = tableView.superview
do {
    bool superviewCanScroll = [superview respondsToSelector:isScrollEnabled] && superview.isScrollEnabled
    if (superviewCanScroll) {
        allowDeletionViaSwipeGesture = NO
    }
} while (superview = superview.superview)

Write good documentation

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.

The Single Responsibility Principle

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.

Conclusions

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.