Monday, December 24, 2007

The Value of Consistency

Consistency. Doing similar things in a similar manner. Consistency in programming can be applied to any layer of your work. At the lowest level, consistency of idioms means you're adhering to an accepted coding standard and known implementation patterns, such as Composed Method (Beck) or Explaining Variables (Fowler). For a real world example, consider getting the current thread in Java using the Thread.currentThread() method. This is one of a handful of getter methods in the JDK that is not prefixed with "get". Inconsistent, confusing, and hard to remember.

At the highest level, consistency of architecture means that your broader decisions are respected. If you have 99 reports running against a data warehouse, it would be inconsistent to have the 100th create its own Access database. This inconsistency would make deployment harder, to say the least.

And somewhere in the middle is a nebulous area I'll call consistency of design, which might include things like structure, object design, and patterns. Developing 99 Object Oriented libraries using C++ and one in procedural C is not consistent. As a real world example, have you ever looked at JVM permissions at the class level? Initially, Java managed permissions using the SecurityManager object. The design was less than extensible, so they added AccessController. Read that last sentence carefully... they did not "replace", they "added". The two objects now split responsibilities for permissions in a unique way. The objects don't have orthogonal concerns and this overlap can make them, at times, frustrating to use.

These examples all contain value judgments. I personally find that the inconsistency in Java permissions objects makes them difficult to use. Consistency is clearly important, I'm not unique in believing this, and the belief is far from a new development:

Uniform rules are invariably simpler. - Brian Kernighan and P. J. Plauger, The Elements of Programming Style, 1978

Consistency doesn't just lead to simplicity, it is simplicity. And with simplicity comes qualities we as engineers should always be striving towards:

Consistency is Simplicity: A consistent approach to style and solutions can make code easier to maintain. - Ken Pugh, Prefactoring, 2005

Maintainability results from a simple, consistent design. Readability results from following simple, consistent programming idioms. As for a consistent and simple high level architecture... what -ility can we possibly claim true for a consistent architecture? Scalability... eh, a simple architecture doesn't necessarily lead to this. Reliability... again, possibly not. And why not? Is it because you and I have seen so many simple, consistent, yet naive architectural visions? Is it because we so rarely see consistent architectures, based on the fact that, over time, applications are going to require architectures to do things they weren't envisioned to do? Yes! Yes it is. Here's why:

There's value in doing the same things the same ways, but only when it's the right thing to do in the first place. - Reginald Braithwaite, 2007

Doing things consistently badly doesn't get you anywhere. Maintaining a strict adherence to an "architecture" only makes sense as long as it's giving you everything you need. At the highest level, consistency needs to be abandoned when the deficiencies of the design become apparent.

Is this true at the lowest level, the level of code style and idioms? Is there ever a reason to drastically change code style midway through a project, even for something as mundane as whitespace? Of course there is, there must be. How about the practice of writing long methods? Long methods seemed acceptable in some circles 10 years ago, but the industry has moved on. Refactoring tools that allow Extract Method to be performed have been the end of long methods for many programmers. Yet some people still write them, don't they? And the reason always seems to be Consistency! (I say Laziness, but let's not offend).

And, of course, there are reasons to be inconsistent at the object design level. Service location is being replaced by dependency injection as an engineering best practice. Writing new singletons is becoming less and less common (thank god). Unit testing is becoming more and more common. It is easier than ever to write loosely coupled, highly cohesive components with 100% test coverage.

But this presents a huge dilemma for legacy code bases. If your code base is 1+ million lines of code, you certainly have service location code. You certainly have singletons. You certainly have a whole bunch of anti-patterns. And there is no way of migrating from consistently bad engineering practices to consistently good engineering practices. A large code base reaches critical mass where no large refactoring effort will ever be approved for it. It's too risky and too time consuming.

Do you break consistency, break simplicity? Or close the book on the project and move on? Is there a way to refactor and redesign code bases so that they are consistent, yet modern?

No. Refactoring means "changing the design of existing code". Experimenting means "trying an option that has not already been attempted". Bringing legacy codebases into the modern world requires both of these. If your codebase is large and the class API has been released to the public, then you're in a bad spot. Changing to a non-consistent API will make it hard for users to work with the code. But if your legacy codebase hasn't escaped into the public, then it's just a balancing act of risk and reward. Experiment. See what works. See what doesn't work. Be inconsistent, and remember why so you can reflect on it later. And do it all on a small scale. And eventually you'll be in a place to make a larger decision. But don't be held back by consistency. It's a loaded word. Yes, consistency is good. We need to strive for it. But not at all costs. Only when we are consistently doing something well. We, as programmers, are smart people. We have to make design decisions every day. So let's not get hung up on consistency for consistency's sake. That might even be true for things outside of programming too...

A foolish consistency is the hobgoblin of little minds. - Ralph Waldo Emerson, Self Reliance, 1841


Peter Pascale said...

A great post - simple (har) and yet I feel I could re-read it for more nuggets of wisdom. Sometimes the enterprise has the luxury of closing the book on a project - sunsetting an application and replacing it with another existing app, or just starting over with greenfield development.

If this is not the case, then small refactoring and experimentation is the best option. Experimentation strikes me as really just a way of refactoring in the small. And these small refactorings allow one to identify new approaches to consistently apply in the larger codebase. A codebase with 'consistent' problems at least affords the ability to apply a proven 'improvement' to a smell every time you find it.

Gary Keim said...

Thread.currentThread() isn't prefixed with "get" because there is no corresponding setCurrentThread. The designers were being consistent in the usage of that pattern.