Design Hygiene

Without ongoing attention to design hygiene, design integrity is bound to deteriorate over time. My latest IEEE design column, Enabling Change, briefly examines what it takes to keep a code base ready to absorb new design changes.

At the very least you should leave code as clean as it was before you made a change (if not better). One credo that Scott Bain extols in his new book, Emergent Design, is whenever you touch the code “do no harm”. Do what you know to be best at the time (given the constraints you are under). Scott is a realist, but I like his stance on when to fight for one particular design approach over another, too. When you think it will be really difficult to refactor/cleanup the design later due to a poor design decision that is on the verge of being made now, argue for what you think now is a better solution.

But even with good intentions and experience, we need to refactor and rework our code as we refine our design ideas–and learn from the code. Yep, we should refactor. Continually. But we don’t always do so. Emerson Murphy-Hill, a Ph.D. student at Portland State and Andrew Black, a professor at Portland State wrote an intriguing paper, Refactoring Tools: Fitness for Purpose, that also appears in the upcoming issue of IEEE Software. They explain why in practice refactoring tools aren’t used as much as they should be and propose some principles that characterize successful regularly used refactoring tools. They also distinguish between “floss refactorings”–those which should be done a regular ongoing basis during programming in order to avoid more pain later on– from “root canal” refactorings. They found that refactoring tools could be better designed and point out some that are better designed for daily use than others.

Alan Shalloway’s Hat Trick

Why does design seems so effortless in the hands of a master and why do beginners find design so difficult? Alan Shalloway‘s talk about Emergent Design at Software Development Best Practices last September demonstrated three ways to get to an identical good design: apply design patterns, use commonality-variability analysis, and practice test driven development techniques. The problem he used to illustrate his talk was fairly simple. Design classes to support monitoring microwave chips and cards by requesting their status over either a TCP/IP connection or via SMTP. Messages may optionally be encrypted with either 64 or 128 bit encryption. Cards queue information and send it out no more than every 10 minutes unless there is an error (then they send it immediately). Chips send results immediately.

Alan started by describing a tall, ugly hierarchy with intermediate abstract classes and 24 concrete subclasses for each hardware type, encryption mechanism, transmission protocol combination. This design is clearly bad because it contains redundant behaviors and a combinatorial explosion of leaf classes.

Next he applied the GOF authors’ advice of favoring composition over inheritance, to create a design that had three families of classes—one for each variation of hardware type, transmission protocol, and encryption. The specific hardware class plugged in the appropriate encryption and transmission protocol helper classes together to implement the appropriate behavior variations. Alan then showed how test-driven development starts with a simpler game plan. Instead of understanding all variations upfront, create a clean design which supports just one partial feature (or story) at a time. Refactor as you add more capabilities following a few good design principles. The idea with TDD is to let the design emerge, one story at a time. The first story Allan specified was to request the status of a chip with 64 bit encryption transmitted via TCP/IP. The next story added the flexibility to support different encryptions, the third transmission via SMTP, and so forth. He concluded his design demonstration by applying commonality-variability analysis to create the same end result. In a nutshell commonality-variability analysis involves identifying common abstractions and variations, relationships between them, assigning them responsibilities, and then linking them together. It differs fundamentally from TDD in that you upfront analyze variations before creating classes that are then configured to work together.

Voila! Identical designs following different approaches and a handful of design heuristics. Is this a realistic expectation outside of a canned talk? Can mere mortals, students new to designs, or developers faced with considerably larger more complicated design variations perform such clean design factorings in a real-world setting? I’m highly skeptical. I’ve seen so many different designs for a more complex problem I present to students of my design class that I’ve stopped believing I know every reasonable solution. I am constantly amazed by the sheer number of different acceptable design solutions students create. Sure, there are recurring patterns and themes among a range of acceptable design solutions. But I don’t expect identical solutions.

Alan’s demonstration that a good design can be achieved three different way—an amazing hat trick—really hit home the point that you needn’t always start by knowing everything upfront. But toss in a few more wrinkles—add a communications port, a mechanism to gain access to that port, define different card and chip types (with different reports), cards that report at different time intervals, cards and chips that can be programmed to report or have to be polled (or both)—and I expect design solutions to really diverge. One designer may embed an if-then-else decision into a method while another may factor out a variation into a family of classes. Some designers check a condition before asking a helper to perform an action, rather than find the polymorphic way to hide those details. Instead of having an “empty encrypter” class, designers may use a variable to represent bits of encryption, and only invoke the encrypter if the variable’s value isn’t zero. Even though I strive for squeaky clean polymorphism and to eliminate external checks, I often can’t convince students or some designers of the benefits of using null objects. To them, creating another (null) class seems like more work than it’s worth.

So while I don’t expect different designers to produce identical results, I firmly believe that abstractions can either be developed as you go or plotted out ahead of time. Alan advises designers to selectively use these approaches. Work out pattern-oriented solutions ahead of time when you know what variations are prevalent. When design variations aren’t so straightforward, start simply and add support for variations as you go. By using test driven development and rigorously refactoring before adding new functionality, you can keep an emerging design clean. Just don’t expect everyone’s design aesthetics to line up. Commonality-variability analysis seems to be a useful anlytic technique whenever I’m laying out the facts as I consider how to support a related set of variations.