I'll Be Backend
Principles · PHP

Inheritance: pull up duplicated behavior

Write shared behavior once in a superclass, so every subclass gets each change.

11 steps4 chapters~4 min at 1.25×#oop#inheritance#duplicated code#superclass
The lesson plays right here. Pause anywhere, step back, or jump to a step below. Watch the PHP video on YouTube Code on GitHub

Watch the narrated video

Every step

Problem

Steps 1–3

Refactor

Steps 4–7

Payoff

Steps 8–9

Recap

Steps 10–11

The code: before and after

Before

declare(strict_types=1);

abstract class Shipment
{
    abstract public function price(): int;
}

final class DomesticShipment extends Shipment
{
    private readonly string $tracking;
    private readonly int $weightKg;

    public function __construct(string $tracking, int $weightKg)
    {
        $this->tracking = $tracking;
        $this->weightKg = $weightKg;
    }

    public function price(): int
    {
        return 5 + $this->weightKg;
    }

    public function label(): string
    {
        return $this->tracking . ' | ' . $this->weightKg
            . ' kg | $' . $this->price();
    }
}

final class InternationalShipment extends Shipment
{
    private readonly string $tracking;
    private readonly int $weightKg;

    public function __construct(string $tracking, int $weightKg)
    {
        $this->tracking = $tracking;
        $this->weightKg = $weightKg;
    }

    public function price(): int
    {
        return 20 + $this->weightKg * 4;
    }

    public function label(): string
    {
        return $this->tracking . ' | ' . $this->weightKg
            . ' kg | $' . $this->price();
    }
}

$home = new DomesticShipment('DM-1', 2);
$abroad = new InternationalShipment('IN-7', 2);
echo $home->label() . "\n";   // DM-1 | 2 kg | $7
echo $abroad->label() . "\n"; // IN-7 | 2 kg | $28

After

declare(strict_types=1);

abstract class Shipment
{
    private bool $fragile = false;

    public function __construct(
        protected readonly string $tracking,
        protected readonly int $weightKg,
    ) {
    }

    abstract public function price(): int;

    public function markFragile(): void
    {
        $this->fragile = true;
    }

    public function label(): string
    {
        $suffix = $this->fragile ? ' | FRAGILE' : '';
        return $this->tracking . ' | ' . $this->weightKg
            . ' kg | $' . $this->price() . $suffix;
    }
}

final class DomesticShipment extends Shipment
{
    public function price(): int
    {
        return 5 + $this->weightKg;
    }
}

final class InternationalShipment extends Shipment
{
    public function price(): int
    {
        return 20 + $this->weightKg * 4;
    }
}

$home = new DomesticShipment('DM-1', 2);
$abroad = new InternationalShipment('IN-7', 2);
$abroad->markFragile();
echo $home->label() . "\n";   // DM-1 | 2 kg | $7
echo $abroad->label() . "\n"; // IN-7 | 2 kg | $28 | FRAGILE

Transcript, step by step

Problem

Step 1: A courier's two shipments Before

A parcel courier's dispatch desk works with two kinds of shipment. Shipment is the superclass, the abstract class the others extend, and each subclass, a class that extends it, says what its parcel costs. Both subclasses also keep a tracking code and a weight, and both build a label from them. The script at the bottom prints one label for a domestic parcel and one for an international one.

Step 2: Fragile parcels Code smell

The courier now needs the word fragile on the label of fragile parcels. The quick way adds a fragile property, a mark fragile method and a label suffix to Domestic Shipment, and then the same three again to International Shipment. The script marks the parcel going abroad, so only the second label ends in fragile.

Step 3: Two copies of everything Code smell

Duplicated code across subclasses is when two classes that share a superclass each write the same properties and methods on their own. You can see the properties here and here, and the label here and here. So every change to the label is made twice, and the day someone edits only one copy, a fragile parcel ships without the word on it. A third shipment type would copy all of it a third time.

Refactor

Step 4: Refactor first Back it out

Let's take the fragile flag out again. We'll refactor first, so that the shared parts move up into Shipment, and then we'll add the flag in one place.

Step 5: Properties move up Pull Up Field

First, the tracking code and the weight move up into Shipment, as protected properties that every subclass can see. Both subclass copies of the properties are gone, and the two constructors still assign them. They can't be readonly yet, because PHP lets only the class that declares a readonly property set it, and here the subclasses do.

Step 6: One constructor Pull Up Constructor Body

Next, Shipment gets a constructor, and its promoted parameters declare and assign both properties in one place. Each subclass constructor would now only pass its two arguments up, and a PHP subclass inherits the constructor of its superclass, so both are deleted. Only Shipment sets the properties now, so they can be readonly again.

Step 7: One label Pull Up Method

Now the two label bodies are identical, so one copy moves up into Shipment, and both subclass copies are deleted. Label still works because it calls price, and each subclass still answers price its own way. This is inheritance, where a subclass gets the properties and methods of its superclass, so the shared parts are written once, in Shipment. What's left in each subclass is its price. A superclass method that calls a step each subclass fills in, like label calling price, is the Template Method pattern.

Payoff

Step 8: Fragile, once Add the flag

Now the fragile flag comes back, and this time it goes into Shipment only. Shipment gains one fragile property and one mark fragile method, and label adds the suffix once. Domestic and International Shipment didn't change, and the script only gains one call that marks the parcel going abroad.

Step 9: Shared code lives once Payoff

The properties now exist once, here, and the label exists once, here. The fragile rule is written once too, so the two labels can't drift apart, and the run prints the word only on the parcel going abroad. It fits because both classes are shipments, each with a tracking code, a weight and a label, and only the price differs. Abstraction hid how a class stores its data, and inheritance shares what two classes both do.

Recap

Step 10: Where we started, where we ended Before / after

On the left, each subclass keeps its own properties, its own constructor body and its own label. On the right, Shipment holds the properties, the constructor, the fragile flag and the label, each written once. A subclass like Domestic Shipment keeps only its own price.

Step 11: When to reach for it Takeaways

Reach for it when two or more subclasses of one superclass repeat the same properties or the same method body. Skip it when two classes look alike today but will change for different reasons, because a superclass ties them together, so use Extract Class and delegate to it. You'll spot the need by the same method copied into sibling subclasses, and the same properties and constructor lines in each. PHP's Filter Iterator works this way, because a subclass writes accept, and inherits current, key, next and rewind.