Why coupling will destroy your application and how to avoid it

March 9, 2016 · 4 minute read

Left unchecked, tight coupling between components (especially when distributed around your application) can slowly kill your software; rendering it hard to maintain and much more likely to suffer from bugs.

In software engineering, coupling is the manner and degree of interdependence between software modules; a measure of how closely connected two routines or modules are; the strength of the relationships between modules.

Wikipedia 

When developing software, we tend to keep a mental picture of the software we write as we go along. This is why developers so often complain about interruptions; because it takes time to build that mental map of the problem you’re working on and any interruption can set you back causing you to pick up the pieces and start building the picture again.

Even when you split your code into multiple classes you have knowledge of the “other half” of the equation (the calling or called classes).

Born together

As a result, it’s easy to couple the software together. At the exact moment you write the code, the coupling is hidden from you because your mental map encompasses both halves of the solution. In effect it is entirely possible to simply take an implementation, divide it in half and spread it between two classes. The two classes end up depending on each other and consequently they are forever joined and rippling changes are inevitable.

Low cohesion coupling

Probably the most blatant coupling you’ll ever see in code is magic strings. As soon as we use “Apple” in two places we’ve coupled our code together. It only takes one of them to be renamed to break our application.

When it comes to how damaging this coupling is, distance matters.

Small gap

We can weaken the coupling by bringing it closer together. If these two strings exist within the same small method then the coupling may well be manageable. Anyone who looks at the code will see both usages of the string and understand that changes to the declaration will affect the usage.

But when the coupling is spread around your app then the risk of bugs dramatically increases.

Coupling is usually contrasted with cohesion. Low coupling often correlates with high cohesion, and vice versa. Low coupling is often a sign of a well-structured computer system and a good design, and when combined with high cohesion, supports the general goals of high readability and maintainability.

Wikipedia 

Exceptional coupling

Magic strings are easy enough to spot; here’s a different example.

public class ShoppingCart
{
    private Dictionary<string, int> _products = new Dictionary<string, int>();
    private int _total;

    public ShoppingCart()
    {
        _products["apple"] = 10;
        _products["pear"] = 20;
    }        

    public void Scan(string item)
    {
        if (!_products.ContainsKey(item))
            throw new UnknownItemException();
        else
            _total += _products[item];
    }                   
}

Our favourite Kata presents a different problem. If the calling code attempts to scan a non-existent item, what should happen?

The approach taken here introduces a few issues.

  • Tight coupling between the exception being thrown and the code which handles it.
  • The calling code has to assume knowledge of which exceptions might be thrown and then handle them.
  • The exception may bubble up through the application and may not be handled at all.
  • If the exception is eventually caught, the handling code may be a long way away from the shopping cart where the problem originated.
  • When reasoning about the code starting at the top of the application it’s entirely possible we’ll have to click through several layers to find what might throw such an exception in the first place.

We can weaken this coupling by ensuring that our calling code doesn’t rely on knowledge of the logic inside our shopping cart. One way is to use a delegate method.

public class ShoppingCart
{
    public void Scan(string item, Action itemNotFound)
    {
        if (!_products.ContainsKey(item))
            itemNotFound();
        else
            _total += _products[item];
    }                   
}

By providing our shopping cart with a coping strategy for missing items, we’ve ensured that the calling application retains control of the details of how to handle the error and our shopping cart need only execute the provided coping strategy when it makes sense to do so.

public class Application
{
    public void Main()
    {
        var cart = new ShoppingCart();
        cart.Scan("mango", () => Render.ErrorMessage("Item not recognised"));
    } 
}

Bonus C# 6 Feature

There is one slight wrinkle with our code, if null is passed as the second argument to our scan method our code will blow up. We should really check if we have a handler before calling it.

public void Scan(string item, Action itemNotFound)
{
    if (!_products.ContainsKey(item))
    {
        if(itemNotFound != null)
            itemNotFound();
    }               
    else
        _total += _products[item];
}   

Luckily, if you happen to be using C# 6 you can take advantage of the null-conditional operator to seamlessly handle this possibility without the conditional if statement.

public void Scan(string item, Action itemNotFound)
{
    if (!_products.ContainsKey(item))
    {
        itemNotFound?.Invoke();
    }               
    else
        _total += _products[item];
}     

In summary

Of course there are many other forms of coupling.  Put the effort into learning to spot them. By identifying and reducing coupling in your code, your application can live a long and happy life.

Join the Practical ASP.NET Newsletter

Ship better Blazor apps, faster. One practical tip every Tuesday.

I respect your email privacy. Unsubscribe with one click.