Apply “Tell Don’t Ask” and reduce coupling

March 14, 2016 · 4 minute read

A few days ago I wrote an article on how coupling (when left unchecked) can impact your code. We looked at exceptions and considered the impact of throwing them in one part of your code, to catch them somewhere else.

I had some great feedback on that post and one comment in particular regarding the viability of the delegate approach in a more realistic example.

Let’s remind ourselves where we ended up.

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, Action itemNotFound)
    {
        if (!_products.ContainsKey(item))
        {
            itemNotFound?.Invoke();
        }
        else
            _total += _products[item];
    }
}

So we have a simple shopping cart with a coping strategy (delegate callback) to handle an unrecognised item being scanned.

What Exactly Did I Scan?

But what happens when we want to do something more realistic, like return the details of the item which has been scanned.

Let’s take a simple approach first.

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

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

Well we’ve tackled the requirement, to return item details when an item is scanned.

To support this we’ve switched our dictionary from strings to ItemDetails.

However because execution doesn’t stop when we invoke our callback delegate we end up having to return null when the item isn’t recognised.

This is a bit ugly. In fact, by returning null here and handling that null somewhere else we’ve introduced yet more coupling.

But what does it all mean?

We can discuss coupling in terms of Connascence which is a taxonomy for different types of coupling.

In connascence parlance this would be Connascence of Meaning because we’ve given semantic meaning to null. This code and any calling code has to agree on the meaning of null. Should we ever change the null to something else, we would have to change code in multiple places.

Connascence-of-Meaning

So what to do? Well we could always stick to our guns and introduce another callback delegate to handle successful scans.

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

This has the clear benefit of removing the null. Now if the itemScanned delegate is invoked we can be confident that it is because the item was found.

The Tell Don’t Ask Principle suggests we should endeavour to tell objects what we want them to do, not ask them questions about their state, make a decision, then tell them what to do. The delegate here ensures we never request state from the shopping cart.

If we’re not keen on the delegate callbacks, but want to stick to telling objects what to do, we could consider another option.

public ShoppingCart(IReceipt receipt)
{
    _products["apple"] = new ItemDetails("apple", 10);
    _products["pear"] = new ItemDetails("apple", 20);
    _receipt = receipt;
}

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

By passing in an instance of an IReceipt interface, we can directly record that the item was successfully scanned.

Now we can choose what type of receipt adapter to use, for testing we might use a simple in-memory version which can be switched out for one that prints to paper in production.

public class SimpleReceipt : IReceipt
{
    List<ReceiptItem> _lines = new List<ReceiptItem>();        

    public void Add(ItemDetails itemDetails)
    {
        _lines.Add(new ReceiptItem(itemDetails.Item, itemDetails.Price));
    }

    internal sealed class ReceiptItem
    {
        public string Item { get; }
        public int Price { get; }

        public ReceiptItem(string item, int price)
        {
            Item = item;
            Price = price;
        }            
    }
}

In summary

As always we have lots of options. The key is that by learning to spot coupling (connascence) and developing strategies to address it we can take great strides towards making our code more habitable.

If you want to learn more about the different forms of connascence I’d recommend connascence.io as a great starting point.

Also Kevin Rutherford has posted a number of articles on the subject.

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.