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
import "fmt"
type Warehouse struct {
Stock map[string]int
}
func NewWarehouse() *Warehouse {
return &Warehouse{Stock: map[string]int{}}
}
func Restock(w *Warehouse, sku string, qty int) {
w.Stock[sku] += qty
}
func PickOrder(w *Warehouse, sku string, qty int) {
if w.Stock[sku] >= qty {
w.Stock[sku] -= qty
}
}
func PrintReport(w *Warehouse, sku string) {
fmt.Printf("%s: %d\n", sku, w.Stock[sku])
}
func main() {
w := NewWarehouse()
Restock(w, "TEA-1", 20)
Restock(w, "TEA-1", 10)
PickOrder(w, "TEA-1", 15)
PrintReport(w, "TEA-1") // TEA-1: 15
}After
import (
"fmt"
"slices"
)
type Warehouse struct {
stock map[string][]int
}
func NewWarehouse() *Warehouse {
return &Warehouse{stock: map[string][]int{}}
}
func (w *Warehouse) Receive(sku string, qty int) {
w.stock[sku] = append(w.stock[sku], qty)
}
func (w *Warehouse) Pick(sku string, qty int) bool {
if w.Quantity(sku) < qty {
return false
}
queue := w.stock[sku]
for qty > 0 {
used := min(queue[0], qty)
queue[0] -= used
if queue[0] == 0 {
queue = queue[1:]
}
qty -= used
}
w.stock[sku] = queue
return true
}
func (w *Warehouse) Quantity(sku string) int {
total := 0
for _, n := range w.stock[sku] {
total += n
}
return total
}
func (w *Warehouse) Deliveries(sku string) []int {
return slices.Clone(w.stock[sku])
}
func Restock(w *Warehouse, sku string, qty int) {
w.Receive(sku, qty)
}
func PickOrder(w *Warehouse, sku string, qty int) {
w.Pick(sku, qty)
}
func PrintReport(w *Warehouse, sku string) {
fmt.Printf("%s: %d\n", sku, w.Quantity(sku))
}
func main() {
w := NewWarehouse()
Restock(w, "TEA-1", 20)
Restock(w, "TEA-1", 10)
PickOrder(w, "TEA-1", 15)
PrintReport(w, "TEA-1") // TEA-1: 15
fmt.Println(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 an exported map, from product code to quantity. Restock adds a delivery to the map, 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 map into a slice of delivery quantities, used as a queue. Restock now appends to the slice, and Pick Order sums it, then takes from the front in a loop. Print Report sums the slice too, and the run still prints 15.
Step 3: Every caller knows the map Code smell
Every caller works on the map directly, so each one depends on how the warehouse stores its stock. Restock uses the map here, Pick Order here, and the report here. One change to the storage meant rewriting three functions that only wanted to receive, pick and count. The sum over the slice 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 slice 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 on a pointer to the warehouse. Restock now calls Receive on the warehouse, and it no longer touches the map.
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. 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, which returns zero for a product the map has never seen. Print Report asks the warehouse for the quantity, and then prints it.
Step 8: Unexport the map Encapsulate Collection
No caller touches the map any more, so the field gets a lowercase name, which makes it unexported. Go keeps it inside this package, so the compiler stops code in other packages, and in this file only the methods still use 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 unexported map now holds a slice of deliveries per product, and Receive appends to it. Quantity sums the slice, 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 functions didn't change, and main gains one line that prints the deliveries, from a cloned slice that callers can't use to change the stock.
Step 10: One type knows the storage Payoff
The map 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 slice 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 an exported map, and all three functions index into it. On the right, the map is unexported and holds a slice 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 functions or types read or change one collection, and the way it's stored may change. Skip it when one short function owns the map and nothing outside reads it, because methods around it add lines and hide nothing. You'll spot the need by indexing or append on another type's map or slice, or by the same lookup copied into several functions. Go's own strings Builder works this way, because you call Write String, and it grows its unexported byte slice by itself.