Polymorphism: subclasses instead of a type field
Let each kind answer for itself, so a new kind adds no switch case.
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 Rental struct {
kind string
days int
}
func (r Rental) Price() int {
switch r.kind {
case "economy":
return 30 * r.days
case "suv":
return 50*r.days + 20
default:
panic("unknown kind: " + r.kind)
}
}
func (r Rental) FreeKm() int {
switch r.kind {
case "economy":
return 200 * r.days
case "suv":
return 150 * r.days
default:
panic("unknown kind: " + r.kind)
}
}
func main() {
city := Rental{"economy", 3}
trip := Rental{"suv", 3}
fmt.Println(city.Price(), city.FreeKm()) // 90 600
fmt.Println(trip.Price(), trip.FreeKm()) // 170 450
}After
import "fmt"
type Rental interface {
Price() int
FreeKm() int
}
type base struct{ days int }
type EconomyRental struct{ base }
func NewEconomyRental(days int) EconomyRental {
return EconomyRental{base{days}}
}
func (r EconomyRental) Price() int { return 30 * r.days }
func (r EconomyRental) FreeKm() int { return 200 * r.days }
type SuvRental struct{ base }
func NewSuvRental(days int) SuvRental {
return SuvRental{base{days}}
}
func (r SuvRental) Price() int { return 50*r.days + 20 }
func (r SuvRental) FreeKm() int { return 150 * r.days }
type VanRental struct{ base }
func NewVanRental(days int) VanRental {
return VanRental{base{days}}
}
func (r VanRental) Price() int { return 70 * r.days }
func (r VanRental) FreeKm() int { return 100 * r.days }
func main() {
var city Rental = NewEconomyRental(3)
var trip Rental = NewSuvRental(3)
var move Rental = NewVanRental(3)
fmt.Println(city.Price(), city.FreeKm()) // 90 600
fmt.Println(trip.Price(), trip.FreeKm()) // 170 450
fmt.Println(move.Price(), move.FreeKm()) // 210 300
}Transcript, step by step
Problem
Step 1: A car rental desk Before
At a car rental desk, a rental knows its kind and how many days it runs. Price and free kilometers each switch on the kind to find the rule for that car. Main rents an economy car and an SUV for three days, and prints the price and free kilometers of each.
Step 2: Vans join the fleet Code smell
The fleet now has vans, at seventy dollars a day with a hundred free kilometers a day. The quick way adds a van case to the price switch, and then another one to the free kilometers switch. Main rents a van for three days, and the run prints two hundred ten and three hundred.
Step 3: One string, a switch per method Code smell
A type field, which Fowler calls a type code, is a string that says what kind of thing a value is. So every method has to switch on that string before it can do its job. You can see the type field here, the switch in price here, and the switch in free kilometers here. Every new kind of car means opening Rental and editing every switch in it, so the van's rules end up spread over every method. A deposit method would add a third switch, and every later kind would edit all three.
Refactor
Step 4: Refactor first Back it out
Let's take the vans out again. We'll refactor first, so each kind of car gets its own type, and then vans will be one new type.
Step 5: One type per kind Replace Type Code with Subclasses
First, Rental becomes an interface that asks for the two methods, price and free kilometers. The old struct is now called base, and it keeps the kind, the days and both switches. Each kind of car gets a type of its own, Economy Rental and SUV Rental, and each one embeds base. Their constructors pass the kind string and the days into base. Go satisfies interfaces implicitly, so both types count as a Rental, because base promotes its two methods to them. Main now creates them as rentals, and both switches still work unchanged.
Step 6: Each kind sets its price Replace Conditional with Polymorphism
Next, each type gets its own price method, with the line from its own case. Nothing calls the price method on base anymore, so it's deleted, and the price switch goes with it. This is polymorphism, because main asks every rental the same question, and each type answers it its own way.
Step 7: Each kind sets its kilometers Replace Conditional with Polymorphism
Free kilometers gets the same move, and each type answers it with the line from its own case. The free kilometers method on base is deleted too, so the second switch is gone as well.
Step 8: The kind string goes Remove Dead Code
Nothing reads the kind field anymore, because no switch is left to ask it. So the field and the two strings passed into base are deleted.
Payoff
Step 9: Vans, as one type Add vans
Now vans come back, and this time they are one new type, Van Rental. It answers price with seventy dollars a day, and free kilometers with a hundred a day. Rental, base, Economy Rental and SUV Rental didn't change, and main only gains the two van lines.
Step 10: Each kind answers for itself Payoff
The type field and both switches used to sit in base, and now base holds only the days. All the van's rules sit in one type now, so the next kind of car is one new type too. This is the Open Closed Principle, because Rental is open for extension and closed for change. It fits here because the fleet grows by kinds of car, while the questions main asks, price and free kilometers, stay the same. And if a new type leaves out free kilometers, it doesn't satisfy Rental, so Go won't compile the line that assigns it.
Recap
Step 11: Where we started, where we ended Before / after
On the left, Rental keeps a kind string, and price and free kilometers each switch on it. On the right, the Rental interface asks for the two methods, and base holds only the days. Each kind of car, like Economy Rental, answers both questions with methods of its own.
Step 12: When to reach for it Takeaways
Reach for it when two or more methods switch on the same type field. Skip it when a value changes kind while it lives, like a rental upgraded mid-trip, because its type can't change, so use State or Strategy instead. You'll spot the need by the same switch or if chain on one field, repeated in several methods. Go's I O Reader works this way, because every source answers Read its own way, from an open file to a bytes reader.