In my last post, I alluded to the idea of antipatterns. The notion was first surfaced in Koenig’s “Patterns and Antipatterns” article that talked about the GoF book on patterns. Basically, Koenig made a good observation that some proposed patterns look like they work, but in practice fail to solve the problem or create other problems that are worse. Ironically, I think people far more easily understand antipatterns than their converse. At the moment, Wikipedia lists 38 patterns and 52 coding-related antipatterns.
Today I’m going to talk about an antipattern I haven’t seen referred to before that I call the Invisible Design. (The name means that the design of the classes can’t be understood without studying each in depth.) The problem the pattern attempts to solve is “how do I assemble code such that it can be easily changed when requirements change?” The easiest, and first solution is to write a base class that implements the solution you have now and then override parts of that solution in a sub-class later. It’s surprisingly easy for this to go horribly wrong.
In general, the original design is often uninformed. It fails to account for some facet of the problem set, or perhaps the scope of the object changes to acquire new responsibilities. Because code grows organically, it’s usually easy to make the first change by overriding a single method or tweaking a constructor. Occasionally, the original solution is specifically refactored to allow the variation to occur by overriding a method. In another case, a developer realizes that some parts of the solution may need to change in the future and creates a class hierarchy to absorb those variations. But again, because the original design is uninformed, it is rare that the true points of change are encapsulated. It is even rarer that the code that accommodates those changes is placed in the correct place in this hierarchy.
In any case, over some time-frame, there’s a loss of cohesion in the classes in question. In order to understand how an individual unit works, the developer must understand how they all work. New developers struggle with understanding how the parts plug into each other. Old developers are hesitant to change base classes because of uncertainty about which other units will be affected or how much. And unit tests become increasingly difficult to write, if they’re written at all.
This antipattern isn’t just related to classes, though. For instance, in Spring, objects are instantiated by a container, then wired together by the container, and then receive input, often from the container. When these objects are configured through external files, those files can become subject to their own invisible design. For instance, spring.xml may import a database.xml that contains data configuration beans that are used to configure the beans in spring.xml. But what if one of the database beans requires a bean from spring.xml? Then it becomes difficult to say for certain which file to look for any random bean. The problem gets worse as you add more files.
But wait! There’s more! A even more specialized form of invisible design occurs when you introduce the notion of programmatic metadata, such as Java’s annotations or C#’s tags. For instance, Hibernate annotations allow you to embed information about how an object is persisted directly into the object itself. This works particularly well for situations where entity objects are relatively self-contained. But for objects with complex interrelationships, this effectively spreads the configuration of the data objects across every source file.
Some symptoms of an invisible design antipattern:
- deep, narrow class hierarchies — all base classes should have at least two children and hierarchies should rarely go farther than two classes deep
- circular dependencies in configuration files — files should be like a tree, and each file should only use its own resources and those of files further down the same branch
- no or only a few unit tests around a whole set of classes — every class should have its own unit tests that exercises every feature of that class
The forces influencing this antipattern are several. First, developers often want architecture to insulate them from change, rather than to cleanly accommodate variation. There’s a certain amount of ego invested in wanting the system to account for all possible variations. Sadly for software, unexpected change happens, and it is usually better to address those issues as they arise. Secondly, decision makers often want small changes in behavior to correspond to small amounts of time. It seems to reason that if you’re not changing something much, it shouldn’t take long. However, changes accumulate, so the cost to make the same size of change is higher after many other changes have been made. The only way to eliminate that debt is to refactor the code.
Let’s go back to the root problem in this antipattern — distributing cohesive behavior across more than one functional unit. There is a fine line between writing code that is modular with high reuse and spreading one feature across multiple classes. In the case of the invisible design, the refactoring required is to merge units until a cohesive solution emerges. Sometimes this means collapsing a hierarchy of classes, other times it means merging configuration files, and still other times it means moving configuration data out of code and into an external resource. I’ve found that once the code is all forced into the same spot, it becomes clear which pattern should be used to handle the variations that have occurred so far. However, it’s generally only after the code starts to coallese in one place that a refactoring solution can be chosen.
Until next time,
Practice Makes.