CodeBork | Tales from the Codeface

The coding blog of Alastair Smith, a software developer based in Cambridge, UK. Interested in DevOps, Azure, Kubernetes, .NET Core, and VueJS.


Project maintained by Hosted on GitHub Pages — Theme by mattgraham

Ok, so this is my first post on Code Complete for a little while; it turned out the busy period at work lasted a good couple of weeks longer than I thought it would! It was a quite a while back that I made these notes (mid-June, in fact), so if the post seems less coherent or I’ve got something obviously wrong, please leave a comment. Here be dragons.

This post deals with design heuristics. We’ve already touched on what heuristics are in the Software Development Metaphors post, so you might want to refresh your understanding before reading on. Inside, I will cover McConnell’s description and critical evaluation of the most common design heuristics. These can be viewed as smaller steps in a larger process, or as individual methods to use at different stages of the design process.

[img_assist nid=103 title=Chocolate Heuristics desc=Copyright © 2006 maadmob link=url url=http://www.flickr.com/photos/maadmob/ align=center width=400 height=330]

The first heuristic covered is by-the-book Object Orientation, finding real-world objects that model the system’s concepts. This is not stated in the sense that “oh, the Peregrine Falcon is a perfect description of the order processing subsystem: efficient and graceful”, but rather that an order is placed for a customer; has a number of lines on the invoice, each of which is for a stock item, and a total price; and might be processed by a particular member of staff. What I’ve done in that last sentence is to identify some of the common objects (customer, order, item, employee) and attributes (lines, total price). Furthermore, we can look at the actions that can be done to each object (“place the order”, “charge the customer”, “ship the order”), and what each object can do others (an Account Manager might place the order on behalf of the customer). At a greater level of detail, we can determine the visibility of the object’s members (e.g., the customer might not be able to view the item’s identifier; the employee might not be able to view the customer’s credit card details; the order can view the totals of all the order lines), and from that define each object’s public interface.

The next heuristic is to form consistent abstractions across the system you are designing. This is accomplished by defining good class interfaces, and providing larger programming abstractions. McConnell illustrates this last point with the metaphor of building a house: a builder will install doors after the carpenter has crafted them from the base materials (e.g., wood, steel, glass, etc.). The builder works with the larger abstraction of “door” over the smaller abstraction of “large sheet of glass”.

The third heuristic, encapsulating implementation details, is an effective technique for managing complexity. If the encapsulation is done well, it prevents you from looking at the complexity, and then the old adage of “out of sight, out of mind” starts to take hold. If your encapsulation is poor, you end up having to manage the implementation detail (and therefore more of the complexity) on a daily basis.

The OO idea of inheritance is a powerful one, and as such McConnell suggests it be treated with care and respect. In fact, he goes a bit further than that to suggest that it should only be used when it simplifies the design. There was recently an example at work that bears this out. In my component, we have two Change Password operations, one for each of before and after the user has logged in. The “external” (pre-login) Change Password class inherits from the “internal” (post-login) class, which has saved a fair bit in terms of duplication of code, etc. However, there’s not a great deal of inheritance used elsewhere in the component, and so tracing the flow of execution between the two is non-obvious and can be quite tricky, not least because both classes are called ChangePassword!

McConnell’s fifth heuristic covers the idea of information hiding, specifically secrets. This has benefits in that again it emphasises hiding complexity, this time based on the iceberg principle (i.e., that the vast majority of the inner workings of the class, and therefore its complexity, should be behind the public interface). Information hiding will help you at all levels of the design, be it working out how the high-level components of the system fit together, or how a subsystem processes input from a file.

[img_assist nid=102 title=Complexity should be 7/8 below the surface too desc= link=node align=center width=219 height=299]

There are some barriers to information hiding, however. Most notably, these include circular dependencies and perceived performance penalties from creating more classes, hitting more methods, etc. In reality, these days, the performance penalty caused by information hiding is negligible in all but the most esoteric of situations. Other behaviours that can cause difficulties with information hiding are excessive distribution of information (where too many diverse bits of the system know about the same bit of information), and class data being mistaken for global data.

However, information hiding has proven to be useful in many systems implemented, and McConnell states that it has a unique ability to produce effective design solutions. You should, therefore, get into the habit of asking “What should I hide?”.

The sixth heuristic is to identify areas likely to change. McConnell proposes a three-step process for achieving this:

  1. Identify items likely to change
  2. Separate items that are likely to change
  3. Isolate items that seem likely to change

There are a number of items that are likely to change. Examples include business rules (e.g., keeping up with changing tax regulations, etc.), hardware dependencies, non-standard language features, status variables, areas that are difficult to design and construct, and more. Separating these items involves compartmentalising them into their own classes, or grouping them into a class with similar items. To achieve step 3, isolation of items likely to change, you need to ensure the public interfaces are insensitive to the potential changes. This does involve anticipating areas of change, which is no mean feat, but I am starting to find that this is the sort of thing that experience teaches you.

Reducing coupling, and keeping it loose, is an excellent design heuristic, and something that the Pragmatic Programmers describe at some length. Taking their analogy, a tightly-coupled system is like a helicopter: moving the joystick requires adjustments to the rudder and other controls to keep the craft balanced. Everything is inter-related, and a small change here could affect something else in a non-obvious, or trivial, or confusing way. A loosely-coupled system, however, provides small, direct, visible and flexible relations. The size of a coupling is measured by the number of connections, whilst the visibility is measured by the prominence of the connection. Flexibility describes how easily the connections change.

McConnell lists a small number of examples of the types of coupling (in ascending order, loosely-coupled to tightly-coupled):

Simple-data-parameter:
all data are of primitive types and passed through parameter lists
Simple-object:
instantiates the object
Object-parameter:
if A requires B to pass it a C then B needs to know about C too
Semantic:
A uses some semantic knowledge of another module's inner workings

I found these slightly confusingly-named, but well-explained. Hopefully the summarised explanations above show that the concepts aren’t too tricky to understand.

McConnell then covers the idea of using common patterns in your designs. These can reduce complexity through their ready-made abstractions, and can reduce errors by institutionalising details of common solutions. There is a huge amount of value in being able to say “we’re writing a Database Abstraction Layer here, the Bridge pattern describes this situation well”, both in terms of easing your own understanding of the problem and the implementation. In and of themselves, they provide heuristic value by suggesting design alternatives, and can streamline communication by moving the conversation to a higher level. It’s much easier to talk about an interface that’s implementing the Façade pattern by referring to a Façade than it is by frequently and repeatedly describing the interface as “something the simplifies the use of the underlying subsystem by grouping the most common and useful functionality into the single interface”.

Last of all, McConnell covers a small selection of other design heuristics. I’m not going to cover them in any detail here, but instead simply list them with a short description.

Aim for strong cohesion:
i.e., highly-focussed classes and modules
Build hierarchies:
improve organisation
Formalise class constructs:
preconditions and postconditions
Assign responsibilities
Design for test:
TDD can help, but it's not the be-all and end-all
Avoid failure:
consider ways the system might fail, e.g., threat modelling
Choose binding time consciously
Make central points of control:
One Right Place
Consider using brute force
Draw a diagram