I'll Be Backend
Principles · C#

Abstraction: hide the data structure

Change how a class stores its data without touching the code that uses it.

12 steps4 chapters~4 min at 1.25×#oop#abstraction#data structure#private fields
The lesson plays right here. Pause anywhere, step back, or jump to a step below. Watch the C# video on YouTube Code on GitHub

Watch the narrated video

Every step

Problem

Steps 1–3

Refactor

Steps 4–8

Payoff

Steps 9–10

Recap

Steps 11–12

The code: before and after

Before

class Warehouse
{
    public Dictionary<string, int> Stock = new();
}

class Receiving
{
    public void Receive(Warehouse w, string sku, int qty)
    {
        w.Stock[sku] = w.Stock.GetValueOrDefault(sku) + qty;
    }
}

class Picking
{
    public void Pick(Warehouse w, string sku, int qty)
    {
        int have = w.Stock.GetValueOrDefault(sku);
        if (have >= qty)
        {
            w.Stock[sku] = have - qty;
        }
    }
}

class Report
{
    public void Print(Warehouse w, string sku)
    {
        int left = w.Stock.GetValueOrDefault(sku);
        Console.WriteLine($"{sku}: {left}");
    }
}

class Shop
{
    static void Main()
    {
        var w = new Warehouse();
        new Receiving().Receive(w, "TEA-1", 20);
        new Receiving().Receive(w, "TEA-1", 10);
        new Picking().Pick(w, "TEA-1", 15);
        new Report().Print(w, "TEA-1");   // TEA-1: 15
    }
}

After

class Warehouse
{
    private readonly
        Dictionary<string, LinkedList<int>> _stock = new();

    public void Receive(string sku, int qty)
    {
        _stock.TryAdd(sku, new LinkedList<int>());
        _stock[sku].AddLast(qty);
    }

    public bool Pick(string sku, int qty)
    {
        if (Quantity(sku) < qty)
        {
            return false;
        }
        var queue = _stock[sku];
        while (qty > 0)
        {
            var oldest = queue.First!;
            int used = Math.Min(oldest.Value, qty);
            oldest.Value -= used;
            if (oldest.Value == 0)
            {
                queue.RemoveFirst();
            }
            qty -= used;
        }
        return true;
    }

    public int Quantity(string sku)
    {
        return _stock.GetValueOrDefault(sku)?.Sum() ?? 0;
    }

    public IReadOnlyList<int> Deliveries(string sku)
    {
        return _stock.GetValueOrDefault(sku)?.ToList() ?? [];
    }
}

class Receiving
{
    public void Receive(Warehouse w, string sku, int qty)
    {
        w.Receive(sku, qty);
    }
}

class Picking
{
    public void Pick(Warehouse w, string sku, int qty)
    {
        w.Pick(sku, qty);
    }
}

class Report
{
    public void Print(Warehouse w, string sku)
    {
        int left = w.Quantity(sku);
        Console.WriteLine($"{sku}: {left}");
    }
}

class Shop
{
    static void Main()
    {
        var w = new Warehouse();
        new Receiving().Receive(w, "TEA-1", 20);
        new Receiving().Receive(w, "TEA-1", 10);
        new Picking().Pick(w, "TEA-1", 15);
        new Report().Print(w, "TEA-1");   // TEA-1: 15
        Console.WriteLine(string.Join(", ", w.Deliveries("TEA-1")));  // 5, 10
    }
}

Transcript, step by step

Problem

Step 1: A shop's warehouse stock Before

A small shop's warehouse keeps its stock in a public dictionary, from product code to quantity. Receiving adds a delivery to the dictionary, Picking takes items out when there are enough, and Report prints what's left. Two deliveries of tea come in, 20 and then 10, and after 15 are picked, the report prints 15.

Step 2: Oldest delivery first Code smell

The tea goes stale, so picking must take the oldest delivery first, and ops need the stock kept per delivery. The quick way turns each value in the dictionary into a linked list of delivery quantities, used as a queue. Receiving now appends to the queue, and Picking sums it, then takes from the front in a loop. Report sums the queue too, and the run still prints 15.

Step 3: Every caller knows the storage Code smell

Every caller works on the dictionary directly, so each one depends on how the warehouse stores its stock. Receiving uses the dictionary here, Picking here, and the report here. One change to the storage meant rewriting three classes that only wanted to receive, pick and count. The sum over the queue now exists twice, written one way in Picking and another way in Report. And when the stock moves to a database next year, all three get rewritten again.

Refactor

Step 4: Refactor first Back it out

Let's take the queue out again. We'll refactor first, so that the warehouse owns its stock, and then we'll change the storage in one place.

Step 5: Receive moves in Move Method

First, the update that adds a delivery moves into Warehouse, as a method called Receive. Receiving now calls Receive on the warehouse, and it no longer touches the dictionary.

Step 6: Pick moves in Move Method

The check and the update move in next, as Pick, which returns true when there was enough stock. Picking calls Pick on the warehouse, and the run still prints 15.

Step 7: Quantity moves in Move Method

The lookup moves in last, as Quantity, which returns zero for a product the warehouse has never seen. Report asks the warehouse for the quantity, and then prints it.

Step 8: Make the dictionary private Encapsulate Collection

No caller touches the dictionary any more, so the field becomes private, and the compiler stops any class outside Warehouse from reaching it. That's abstraction, where callers use what the warehouse does, Receive, Pick and Quantity, and never see how it stores the stock. Encapsulation hides a single field, and abstraction goes one step further, because it hides how a whole collection is stored.

Payoff

Step 9: Keep stock per delivery Substitute Algorithm

Now the change request comes back, and this time it stays inside Warehouse. The private dictionary now holds a linked list of deliveries per product, and Receive appends to it. Quantity sums the list, and Pick checks the quantity, then takes from the oldest delivery first. Each method keeps its signature and gets a new body, and that move is called Substitute Algorithm. Receiving, Picking and Report didn't change, and Shop gains one line that prints the deliveries.

Step 10: One class knows the storage Payoff

The dictionary calls are gone from the callers, so Receiving calls Receive here, Picking calls Pick here, and Report calls Quantity here. The sum over the deliveries is written once, in Quantity, and Pick reuses it for its check. The 15 picked came out of the oldest delivery of 20, so 5 of that one are left, next to the newer 10. The three callers depend only on Receive, Pick and Quantity, so changing how Warehouse stores the stock stayed inside Warehouse.

Recap

Step 11: Where we started, where we ended Before / after

On the left, the stock is a public dictionary, and Receiving, Picking and Report each work on it directly. On the right, the dictionary is private and holds a linked list per product, behind Receive, Pick and Quantity. A caller like Picking makes one call, and the warehouse decides which delivery the items come from.

Step 12: When to reach for it Takeaways

Reach for it when several classes read or change one collection, and the way it's stored may change. Skip it when one short method owns the dictionary and nothing outside reads it, because methods around it add lines and hide nothing. You'll spot the need by indexers or Get Value Or Default on another object's collection, or by the same lookup copied into several classes. The List class in dot net works this way, because you call Add, and it grows its hidden array by itself.