Abstraction: hide the data structure
Change how a class stores its data without touching the code that uses it.
Watch the narrated video
Every step
Problem
Steps 1–3Refactor
Steps 4–8Payoff
Steps 9–10Recap
Steps 11–12The code: before and after
Before
final class Warehouse {
var stock: [String: Int] = [:]
}
struct Receiving {
func receive(_ w: Warehouse, sku: String, qty: Int) {
w.stock[sku, default: 0] += qty
}
}
struct Picking {
func pick(_ w: Warehouse, sku: String, qty: Int) {
let have = w.stock[sku, default: 0]
if have >= qty {
w.stock[sku] = have - qty
}
}
}
struct Report {
func printStock(_ w: Warehouse, sku: String) {
let left = w.stock[sku, default: 0]
print("\(sku): \(left)")
}
}
let w = Warehouse()
Receiving().receive(w, sku: "TEA-1", qty: 20)
Receiving().receive(w, sku: "TEA-1", qty: 10)
Picking().pick(w, sku: "TEA-1", qty: 15)
Report().printStock(w, sku: "TEA-1") // TEA-1: 15After
final class Warehouse {
private var stock: [String: [Int]] = [:]
func receive(_ sku: String, qty: Int) {
stock[sku, default: []].append(qty)
}
@discardableResult
func pick(_ sku: String, qty: Int) -> Bool {
guard quantity(of: sku) >= qty else {
return false
}
var queue = stock[sku, default: []]
var rest = qty
while rest > 0 {
let oldest = queue.removeFirst()
let used = min(oldest, rest)
if oldest > used {
queue.insert(oldest - used, at: 0)
}
rest -= used
}
stock[sku] = queue
return true
}
func quantity(of sku: String) -> Int {
stock[sku, default: []].reduce(0, +)
}
func deliveries(of sku: String) -> [Int] {
stock[sku, default: []]
}
}
struct Receiving {
func receive(_ w: Warehouse, sku: String, qty: Int) {
w.receive(sku, qty: qty)
}
}
struct Picking {
func pick(_ w: Warehouse, sku: String, qty: Int) {
w.pick(sku, qty: qty)
}
}
struct Report {
func printStock(_ w: Warehouse, sku: String) {
let left = w.quantity(of: sku)
print("\(sku): \(left)")
}
}
let w = Warehouse()
Receiving().receive(w, sku: "TEA-1", qty: 20)
Receiving().receive(w, sku: "TEA-1", qty: 10)
Picking().pick(w, sku: "TEA-1", qty: 15)
Report().printStock(w, sku: "TEA-1") // TEA-1: 15
print(w.deliveries(of: "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 dictionary, from product code to quantity, and Swift makes that property internal by default, so any code in the module can change it. 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 an array of delivery quantities, used as a queue. Receiving now appends to the array, and Picking sums it, then takes from the front in a loop. Arrays are values in Swift, so Picking works on a copy and has to write the queue back at the end. Report sums the queue too, and the run still prints 15.
Step 3: Every caller knows the map 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 structs that only wanted to receive, pick and count. The sum over the queue now exists twice, once in Picking and once more 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 line 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 ignores that answer, and the discardable result attribute keeps the compiler from warning about it. 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 of that product, and then prints it.
Step 8: Make the dictionary private Encapsulate Collection
No caller touches the dictionary any more, so the property becomes private, and the compiler stops the code 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 an array of deliveries per product, and receive appends to 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. Receiving, Picking and Report didn't change, and the run 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 queue 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. Deliveries hands back the array itself, and because Swift arrays are values, the caller gets a copy it can't use to change the stock. 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 dictionary any code in the module can change, and Receiving, Picking and Report each read or write it directly. On the right, the dictionary is private and holds the deliveries 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 types read or change one collection, and the way it's stored may change. Skip it when one short function owns the dictionary and nothing outside reads it, because methods around it add lines and hide nothing. You'll spot the need by subscripts, append or reduce called on another object's collection, or by the same lookup copied into several types. Swift's own Array, used in this lesson, works this way, because you call append, and it grows its hidden buffer by itself.