Object-oriented programming: turn a script into objects
Keep each piece of data with the rules that use it, so features stop copying code.
Watch the narrated video
Every step
Problem
Steps 1–3Refactor
Steps 4–7Payoff
Steps 8–9Recap
Steps 10–12The code: before and after
Before
import "fmt"
func main() {
names := []string{"Ana", "Ben", "Cara"}
plans := []string{"basic", "pro", "pro"}
months := []int{3, 12, 2}
for i := range names {
price := 10
if plans[i] == "pro" {
price = 30
}
total := price * months[i]
fmt.Println(names[i], "owes", total)
}
}After
import "fmt"
type Subscription struct {
name string
plan string
months int
}
func (s Subscription) monthlyPrice() int {
if s.plan == "pro" {
return 30
}
return 10
}
func (s Subscription) total() int {
return s.monthlyPrice() * s.months
}
func main() {
subs := []Subscription{
{name: "Ana", plan: "basic", months: 3},
{name: "Ben", plan: "pro", months: 12},
{name: "Cara", plan: "pro", months: 2},
}
for _, s := range subs {
fmt.Println(s.name, "owes", s.total())
}
for _, s := range subs {
if s.total() > 100 {
fmt.Println("Reminder:", s.name)
}
}
}Transcript, step by step
Problem
Step 1: A gym bills its members Before
This gym keeps its members in three slices, one for names, one for plans and one for months. The loop walks them by index, picks a monthly price from the plan, and multiplies it by the months. Then it prints each bill, so Ana owes 30, Ben owes 360 and Cara owes 60.
Step 2: Add a reminder Code smell
The front desk now wants a reminder for every member who owes more than 100. The quick way is a second loop that copies the price and total lines, then prints a reminder for the big bills.
Step 3: Parallel slices, a rule in a loop Code smell
In a script, the data sits in parallel slices that belong together only by position, and the rule that turns them into a bill lives inside one loop, so nothing else can use it. You can see the price rule here and copied here, the total here and copied here, and one member spread across three slices here. When the pro plan goes up to 35, two lines must change, and the one you miss keeps the old 30. So Ben's bill would say 420, while his reminder still works from 360. And every new detail about a member, like a start date, is a fourth slice that must stay in the same order as the other three.
Refactor
Step 4: Refactor first Back it out
Let's take the second loop out again. We'll refactor first, so that adding the reminder becomes a small change, and then we'll add it.
Step 5: One member, one struct Replace Array with Object
First, a struct called Subscription holds the three things we know about a member, its name, its plan and its months. The three slices become one slice of three subscriptions, and each struct literal names the field every value goes into. The loop now ranges over that slice with no index, and it reads s dot plan and s dot months. One member is now one value, so its name, plan and months can't drift apart.
Step 6: Name the bill rule Extract Function
The price and total lines move into a plain function called total, which takes one subscription. The loop prints total of s, and every bill comes out the same as before.
Step 7: Move total to its data Move Method
Total only ever read the subscription's own plan and months, so it moves onto Subscription as a method. Its parameter moves in front of the name and becomes the receiver, which is how Go ties a method to a struct. The loop now asks s dot total. Now the data and the rule that uses it live in one place, and that is what an object is.
Payoff
Step 8: Give the price a name Extract Method
The price rule gets its own method, monthly price, which returns 30 for pro and 10 for any other plan. Total becomes monthly price times months, so the rule now has a name you can read and call.
Step 9: Add the reminder again Payoff
Now the reminder goes in as a second loop that asks each subscription for its total. Subscription and its two methods didn't change, and neither did the bill loop. Ben owes 360, so he's the one member who gets a reminder.
Recap
Step 10: One home for each rule Payoff
The price rule is in one place now, the total is in one place, and each member is one struct value. A pro price of 35 is one edit in monthly price, and both the bill and the reminder pick it up. A new member detail, like a start date, is one new field on one struct instead of a fourth slice kept in order. Each member's name, plan and months now sit in one struct with the methods that read them, and that is object-oriented programming in this code.
Step 11: Where we started, where we ended Before / after
On the left, three slices hold each member by position, and the loop works out the price and the total inline. On the right, one Subscription struct holds a member, and its methods own the monthly price and total rules. The bill loop asks each subscription for its total, and it never sees the price rule.
Step 12: When to reach for it Takeaways
Reach for objects when a few values always travel together and several parts of the code compute from them. Skip them for a one-off script that runs once, because when nothing else uses its rules, a few lines top to bottom do the job. You'll spot the need by parallel slices matched by index, and by the same calculation copied into a second loop. Go's own time package works this way, because a Time value keeps a date together with methods like Add Date.