I'll Be Backend
Principles · Ruby

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 Ruby 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
  attr_reader :stock

  def initialize
    @stock = Hash.new(0)
  end
end

def restock(w, sku, qty)
  w.stock[sku] += qty
end

def pick_order(w, sku, qty)
  have = w.stock[sku]
  w.stock[sku] = have - qty if have >= qty
end

def print_report(w, sku)
  puts "#{sku}: #{w.stock[sku]}"
end

w = Warehouse.new
restock(w, "TEA-1", 20)
restock(w, "TEA-1", 10)
pick_order(w, "TEA-1", 15)
print_report(w, "TEA-1") # TEA-1: 15

After

class Warehouse
  def initialize
    @stock = Hash.new { |hash, sku| hash[sku] = [] }
  end

  def receive(sku, qty)
    @stock[sku].push(qty)
  end

  def pick(sku, qty)
    return false if quantity(sku) < qty

    queue = @stock[sku]
    while qty > 0
      oldest = queue.shift
      used = [oldest, qty].min
      queue.unshift(oldest - used) if oldest > used
      qty -= used
    end
    true
  end

  def quantity(sku)
    @stock[sku].sum
  end

  def deliveries(sku)
    @stock[sku].dup.freeze
  end
end

def restock(w, sku, qty)
  w.receive(sku, qty)
end

def pick_order(w, sku, qty)
  w.pick(sku, qty)
end

def print_report(w, sku)
  puts "#{sku}: #{w.quantity(sku)}"
end

w = Warehouse.new
restock(w, "TEA-1", 20)
restock(w, "TEA-1", 10)
pick_order(w, "TEA-1", 15)
print_report(w, "TEA-1") # TEA-1: 15
p 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 hash, from product code to quantity, and attr reader hands that hash to anyone who asks. Restock adds a delivery to the hash, pick order takes items out when there are enough, and print report shows 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 hash into an array of delivery quantities. Restock now pushes onto the array, and pick order sums it, then shifts from the front in a loop. Print report sums the array too, and the run still prints 15.

Step 3: Every caller knows the hash Code smell

Every caller works on the hash directly, so each one depends on how the warehouse stores its stock. Restock uses the hash here, pick order here, and the report here. One change to the storage meant rewriting three methods that only wanted to receive, pick and count. The sum over the array now exists twice, once in pick order and once in print 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 arrays 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 moves into Warehouse, as a method called receive. Restock now calls receive on the warehouse, and it no longer touches the hash.

Step 6: Pick moves in Move Method

The check and the update move in next, as pick, which returns false when there isn't enough stock. Pick order 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, and the hash's default gives zero for a product the warehouse has never seen. Print report asks the warehouse for the quantity, and then prints it.

Step 8: Drop the reader Encapsulate Collection

No caller touches the hash any more, so the attr reader line goes, and the instance variable is all that's left. Code outside Warehouse that still calls stock now gets a no method error from Ruby, and none of these three does. 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 hash now holds an array of deliveries per product, and receive pushes onto it. Quantity sums the array, 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. The three callers didn't change, and the script gains one line that prints the deliveries as a frozen copy, so callers can't change them.

Step 10: One class knows the storage Payoff

The hash calls are gone from the callers, so restock calls receive here, pick order calls pick here, and the report calls quantity here. The sum over the array 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, attr reader hands out the hash, and all three methods index into it. On the right, the hash is private and holds an array per product, behind receive, pick and quantity. A caller like pick order 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 methods or classes read or change one collection, and the way it's stored may change. Skip it when one short method owns the hash and nothing outside reads it, because methods around it add lines and hide nothing. You'll spot the need by an attr reader on a hash, then indexing into it from other classes, or the same lookup copied around. Ruby's own Set works this way, because you call add and include, and it keeps its storage to itself.