Polymorphism: subclasses instead of a type field
Let each kind answer for itself, so a new kind adds no match arm.
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 Rental
{
public function __construct(
private readonly string $type,
private readonly int $days,
) {
}
public function price(): int
{
return match ($this->type) {
'economy' => 30 * $this->days,
'suv' => 50 * $this->days + 20,
default => throw new InvalidArgumentException($this->type),
};
}
public function freeKm(): int
{
return match ($this->type) {
'economy' => 200 * $this->days,
'suv' => 150 * $this->days,
default => throw new InvalidArgumentException($this->type),
};
}
}
$city = new Rental('economy', 3);
$trip = new Rental('suv', 3);
echo $city->price() . ' ' . $city->freeKm() . "\n"; // 90 600
echo $trip->price() . ' ' . $trip->freeKm() . "\n"; // 170 450After
abstract class Rental
{
public function __construct(
protected readonly int $days,
) {
}
abstract public function price(): int;
abstract public function freeKm(): int;
}
final class EconomyRental extends Rental
{
public function price(): int
{
return 30 * $this->days;
}
public function freeKm(): int
{
return 200 * $this->days;
}
}
final class SuvRental extends Rental
{
public function price(): int
{
return 50 * $this->days + 20;
}
public function freeKm(): int
{
return 150 * $this->days;
}
}
final class VanRental extends Rental
{
public function price(): int
{
return 70 * $this->days;
}
public function freeKm(): int
{
return 100 * $this->days;
}
}
$city = new EconomyRental(3);
$trip = new SuvRental(3);
$move = new VanRental(3);
echo $city->price() . ' ' . $city->freeKm() . "\n"; // 90 600
echo $trip->price() . ' ' . $trip->freeKm() . "\n"; // 170 450
echo $move->price() . ' ' . $move->freeKm() . "\n"; // 210 300Transcript, step by step
Problem
Step 1: A car rental desk Before
At a car rental desk, a rental knows its type and how many days it runs. Price and free kilometers each match on the type to find the rule for that car. The script at the bottom 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 arm to the price match, and then another one to the free kilometers match. The script rents a van for three days, and the run prints two hundred ten and three hundred.
Step 3: One string, a match per method Code smell
A type field, which Fowler calls a type code, is a string that says what kind of thing an object is. So every method has to match on that string before it can do its job. You can see the type field here, the match in price here, and the match in free kilometers here. Every new kind of car means opening Rental and editing every match in it, so the van's rules end up spread over every method. A deposit method would add a third match, 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 class, and then vans will be one new class.
Step 5: One class per kind Replace Type Code with Subclasses
First, Rental becomes abstract, and each kind of car gets a subclass, a class that extends it. Its two properties become protected, because the subclasses will need the days soon. Economy Rental and SUV Rental have only a constructor, which passes its type string and the days to the parent constructor. The script now creates the subclasses, and both matches still work unchanged.
Step 6: Each kind sets its price Replace Conditional with Polymorphism
Next, each subclass overrides price with the line from its own arm. Rental's price becomes abstract, so the price match is gone, and Rental only declares the question. This is polymorphism, because the script asks every rental the same question, and each subclass answers it its own way. Last lesson pulled shared code up into a superclass, and this one pushes the code that differs down into subclasses.
Step 7: Each kind sets its kilometers Replace Conditional with Polymorphism
Free kilometers gets the same move, and each subclass overrides it with the line from its own arm. Rental's free kilometers becomes abstract too, so the second match is gone as well.
Step 8: The type string goes Remove Dead Code
Nothing reads the type field anymore, because no match is left to ask it. So the property and the two strings passed up are deleted, and Rental's constructor takes only the days. The subclass constructors go too, because a PHP subclass without one inherits its parent's constructor.
Payoff
Step 9: Vans, as one class Add vans
Now vans come back, and this time they are one new class, Van Rental. It overrides price with seventy dollars a day, and free kilometers with a hundred a day. Rental, Economy Rental and SUV Rental didn't change, and the script only gains the two van lines.
Step 10: Each kind answers for itself Payoff
The type field used to sit here, and the two matches used to sit here and here. All the van's rules sit in one class now, so the next kind of car is one new class 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 the script asks, price and free kilometers, stay the same. And if a new class leaves out free kilometers, PHP stops with a fatal error as soon as the class loads, and PHPStan reports it before that.
Recap
Step 11: Where we started, where we ended Before / after
On the left, Rental keeps a type string, and price and free kilometers each match on it. On the right, Rental holds only the days and declares the two questions as abstract methods. Each kind of car, like Economy Rental, answers both questions in its own class.
Step 12: When to reach for it Takeaways
Reach for it when two or more methods match on the same type field. Skip it when an object changes kind while it lives, like a rental upgraded mid-trip, because an object can't change its class, so use State or Strategy instead. You'll spot the need by the same match, switch or if chain on one field, repeated in several methods. PHP's own Filter Iterator works this way, because accept is abstract, and each subclass decides in its own way which items pass.