Lesson 02 / 12
Inheritance and Abstract Classes
A change inside a parent class shifting the result of a subclass implicitly, an added abstract method stopping the subclasses, and the same counter built with composition leaving the same change in place.
Published Updated
In this lesson
- Shows that an abstract class cannot be instantiated and a final method cannot be overridden
- Measures that an internal change in the parent class silently shifts the subclass's counter
- Verifies that building the same counter with composition keeps the change in place
Contents
In Classes and Objects a name inside a class changed and the consumer was unaffected, because visibility stood in between. With inheritance nothing stands in between: the subclass calls the parent’s methods and protected properties directly. So the question changes. When the inside of the parent class changes, what does the subclass experience?
Building a Subclass
Inheritance is a class taking over the properties and methods of another class. The class that takes over is the subclass, the class taken from is the parent class. In PHP the extends keyword declares this. The protected visibility opens a member to the class itself and its subclasses and keeps it closed to the outside. If a subclass writes a method under the same name, it overrides the method; the parent’s version is still reachable with parent::.
<?php // ItemList.php — a parent class that carries a protected property and three methods class ItemList { protected array $items = []; public function add(string $item): void { $this->items[] = $item; } public function addAll(array $items): void { foreach ($items as $item) { $this->add($item); } } public function count(): int { return count($this->items); } }
<?php // CountingList.php — a subclass that overrides the add() method class CountingList extends ItemList { public int $counter = 0; public function add(string $item): void { $this->counter++; parent::add($item); } }
<?php // list_usage.php — the subclass uses the parent's method; the protected property is closed from outside require 'ItemList.php'; require 'CountingList.php'; $list = new CountingList(); $list->addAll(['a', 'b', 'c']); echo $list->counter, ' ', $list->count(), "\n"; try { echo count($list->items); } catch (Error $error) { echo get_class($error), "\n"; }
3 3 Error
OM5. addAll() was not written in the subclass, it was inherited; add() was called for each item and CountingList::add() raised the counter on every call: 3 3. The protected property was open to the subclass but stayed closed to the outside. What stands out here is that the subclass’s counter depends on an internal detail of the parent: addAll() calling add() one by one.
Abstract Classes and final
An abstract class cannot be instantiated and can carry methods without a body; subclasses must write those methods. The class below declares one method without a body and supplies one method with a body.
<?php // Report.php — a method without a body and a method with a body that depends on it abstract class Report { abstract public function title(): string; public function render(): string { return strtoupper($this->title()); } }
<?php // Daily.php — a concrete subclass that writes the abstract method class Daily extends Report { public function title(): string { return 'daily'; } }
<?php // report_usage.php — the concrete subclass is built, the abstract class cannot be require 'Report.php'; require 'Daily.php'; echo (new Daily())->render(), "\n"; try { new Report(); } catch (Error $error) { echo get_class($error), "\n"; }
DAILY Error
If a method is marked final, subclasses cannot override it. The file stops as soon as it is loaded. The script below writes two classes, one of which overrides a final method, into a file and prints the exit code.
# final.sh — a class that overrides a final method cannot be loaded cat > Fixed.php <<'PHP' <?php class Base { final public function identity(): string { return 'base'; } } class Child extends Base { public function identity(): string { return 'child'; } } PHP php -d display_errors=0 Fixed.php; echo "exit=$?"
exit=255
OM6. new Daily() ran and Report::render() used the subclass’s title() method. new Report() and the file that overrides a final method, however, gave a signal: the first threw Error, the second stopped with exit 255. The abstract class and final are the two ends of inheritance: one imposes an obligation to write on the subclass, the other takes away the authority to write.
The Parent’s Constructor
If a subclass writes its own constructor, the parent’s constructor does not run on its own. For the state the parent builds to be ready, parent::__construct() is called explicitly. If the call is forgotten, the parent’s properties are left unset.
<?php // Person.php — a parent constructor, a subclass that calls it and one that forgets class Person { public function __construct(protected string $name) {} public function greet(): string { return 'hello ' . $this->name; } } class Employee extends Person { public function __construct(string $name, private int $number) { parent::__construct($name); } public function card(): string { return $this->greet() . ' #' . $this->number; } } class Forgetful extends Person { public function __construct(private int $number) {} public function read(): string { return $this->name; } } echo (new Employee('ali', 7))->card(), "\n"; try { echo (new Forgetful(1))->read(); } catch (Error $e) { echo get_class($e), "\n"; }
hello ali #7 Error
Because Employee called the parent’s constructor, name was set. Because Forgetful did not, name was never written and reading it raised Error. In inheritance the constructor chain works only if it is written.
Signatures When Overriding and a final Class
When a subclass overrides a method, it has to stay compatible with the parent’s signature; code that holds the parent in a typed variable must be able to make the same call with the subclass. Adding a required parameter breaks that compatibility. If a class is not to be extended at all, it is marked final; the file that extends it stops as soon as it is loaded.
# signature.sh — adding a required parameter and extending a final class make the declaration impossible cat > Signature.php <<'PHP' <?php class Base { public function add(string $item): void {} } class Child extends Base { public function add(string $item, int $quantity): void {} } PHP cat > ClosedClass.php <<'PHP' <?php final class Closed {} class Opener extends Closed {} PHP php -d display_errors=0 Signature.php; echo "signature exit=$?" php -d display_errors=0 ClosedClass.php; echo "final exit=$?"
signature exit=255 final exit=255
In both cases the error came not during execution but during the class declaration. A final method takes one behavior away from subclasses, a final class takes all of it.
When Inheritance Fits
Inheritance fits when there is an “is a” relationship between two classes: an Employee is a Person, a Daily is a Report. Inheritance built only to share code ties the subclass to all of the parent’s methods and to every future change in them. This is exactly what is measured throughout this lesson: when the inside of the parent changes, the subclasses tied to it are affected. If the shared code describes a capability rather than a relationship, an interface or a trait creates fewer ties; they are the next two lessons.
The Measurement Core
The measurement in this lesson uses the file reach.php; this is the core explained line by line in Classes and Objects, and it is rewritten in every lesson’s working directory.
<?php // reach.php — helpers that measure where a change reaches function write_files(string $dir, array $files): void { foreach ($files as $name => $content) { if ($content === null) { continue; // null: the file was deleted } $path = "$dir/$name"; if (!is_dir(dirname($path))) { mkdir(dirname($path), 0777, true); } file_put_contents($path, $content); } } function read_files(string $dir): array { $files = []; $walker = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS) ); foreach ($walker as $file) { $name = substr($file->getPathname(), strlen($dir) + 1); $files[$name] = file_get_contents($file->getPathname()); } return $files; } function run(string $dir): array { $command = 'cd ' . escapeshellarg($dir) . ' && php -d display_errors=0 main.php 2>/dev/null'; exec($command, $lines, $code); return [$code, implode("\n", $lines)]; } function measure(string $name, array $base, array $change, array $seams = []): string { if (array_key_exists('main.php', $change)) { throw new LogicException("$name: the consumer is not edited"); } write_files("measure/$name/before", $base); write_files("measure/$name/after", array_merge($base, $change)); [$codeBefore, $outputBefore] = run("measure/$name/before"); [$codeAfter, $outputAfter] = run("measure/$name/after"); if ($codeBefore !== 0) { throw new LogicException("$name: the baseline did not run"); } $reach = match (true) { $codeAfter !== 0 => 'breaking', $outputBefore !== $outputAfter => 'implicit', array_intersect(array_keys($change), $seams) !== [] => 'at-seam', default => 'in-place', }; printf( "%-18s files=%d exit=%-3d output %-7s -> %s\n", $name, count($change), $codeAfter, $outputBefore === $outputAfter ? 'same' : 'changed', $reach ); return $reach; }
The Parent’s Inside Changes
The first observation uses the ItemList and CountingList pair above. The parent’s addAll() method no longer calls add() one by one; it merges the array in one go with array_merge(). The subclass and the consumer main.php stay the same. The contract the parent offers to the outside does not change either: the two methods carry the same name and signature.
<?php // inheritance.php — the parent no longer calls the single-add method in bulk add require_once 'reach.php'; measure('parent-class', [ 'item_list.php' => <<<'PHP' <?php class ItemList { protected array $items = []; public function add(string $item): void { $this->items[] = $item; } public function addAll(array $items): void { foreach ($items as $item) { $this->add($item); } } } PHP, 'main.php' => <<<'PHP' <?php require 'item_list.php'; class CountingList extends ItemList { public int $counter = 0; public function add(string $item): void { $this->counter++; parent::add($item); } } $list = new CountingList(); $list->addAll(['a', 'b', 'c']); echo $list->counter, "\n"; PHP, ], [ 'item_list.php' => <<<'PHP' <?php class ItemList { protected array $items = []; public function add(string $item): void { $this->items[] = $item; } public function addAll(array $items): void { $this->items = array_merge($this->items, $items); } } PHP, ]); foreach (['before', 'after'] as $stage) { [$code, $output] = run("measure/parent-class/$stage"); echo $stage, ': ', $output, "\n"; }
parent-class files=1 exit=0 output changed -> implicit before: 3 after: 0
OM7. The exit code is 0, but the output changed: the counter went from 3 to 0. The subclass had overridden only the add() method; since the parent no longer calls add(), the subclass’s method never ran. There is no error and no warning in the consumer, only a wrong number. This is the fragile base class problem: the subclass trusted not the parent’s contract but its inner workings, and the inner workings changed. The lesson’s reach is implicit. In Object-Oriented Java what was measured was which type chooses the method that gets called; here what is measured is whether a change in the parent reaches the subclass, and it did.
Adding an Abstract Method
The opposing measurement moves the change from the inside of the parent to its contract. A new abstract method is added to an abstract class. A subclass written earlier, like Daily, does not know this method.
<?php // abstract.php — a new abstract method is added to the abstract parent require_once 'reach.php'; measure('abstract-method', [ 'report.php' => <<<'PHP' <?php abstract class Report { abstract public function title(): string; public function render(): string { return strtoupper($this->title()); } } PHP, 'main.php' => <<<'PHP' <?php require 'report.php'; class Daily extends Report { public function title(): string { return 'daily'; } } echo (new Daily())->render(), "\n"; PHP, ], [ 'report.php' => <<<'PHP' <?php abstract class Report { abstract public function title(): string; abstract public function footer(): string; public function render(): string { return strtoupper($this->title()); } } PHP, ]);
abstract-method files=1 exit=255 output changed -> breaking
OM8. The same consumer, the same subclass, the same single-file change; this time the consumer stopped. An abstract method tells a subclass “you must write this”, and when the subclass does not write it, the class cannot be declared. The exit code is not 0, the reach is breaking. Because the contract has no body, the language catches the change; there was no contract that would catch an internal change in the parent.
The Bounding Measurement
Is the cause of the implicit result inheritance itself, or the counter being tied to the inside of the parent? When the same counter is built with composition, the measurement separates the two. In composition the class does not extend another class, it carries an instance of it and counts in its own two methods. The change made in the parent is the same as before: addAll() no longer calls add().
<?php // composition.php — the counter is built with composition, not inheritance; the same parent change require_once 'reach.php'; measure('composition', [ 'item_list.php' => <<<'PHP' <?php class ItemList { protected array $items = []; public function add(string $item): void { $this->items[] = $item; } public function addAll(array $items): void { foreach ($items as $item) { $this->add($item); } } } PHP, 'main.php' => <<<'PHP' <?php require 'item_list.php'; class CountingList { public int $counter = 0; private ItemList $inner; public function __construct() { $this->inner = new ItemList(); } public function add(string $item): void { $this->counter++; $this->inner->add($item); } public function addAll(array $items): void { $this->counter += count($items); $this->inner->addAll($items); } } $list = new CountingList(); $list->addAll(['a', 'b', 'c']); echo $list->counter, "\n"; PHP, ], [ 'item_list.php' => <<<'PHP' <?php class ItemList { protected array $items = []; public function add(string $item): void { $this->items[] = $item; } public function addAll(array $items): void { $this->items = array_merge($this->items, $items); } } PHP, ]); foreach (['before', 'after'] as $stage) { [$code, $output] = run("measure/composition/$stage"); echo $stage, ': ', $output, "\n"; }
composition files=1 exit=0 output same -> in-place before: 3 after: 3
OM9. The parent’s change is the same, the consumer is the same; the result stayed 3. In composition the counter does not look at which method of the parent calls which: it counts in its own two methods and reaches the parent only through the contract, that is, the names and signatures of the two methods. The same internal change was implicit in inheritance and stayed in place in composition. The distinction measured is one of design, not of language: inheritance ties the subclass to the inside of the parent, composition does not. The cost comes from the same place: CountingList is no longer an ItemList.
Summary
extendsbuilds a subclass,protectedopens a member to the subclass while keeping it closed to the outside, andparent::reaches the parent’s method.- An abstract class cannot be instantiated and obliges subclasses to write the methods without a body; a
finalmethod cannot be overridden. - When the parent stopped calling
add()in bulk add, the subclass’s counter dropped from3to0; the reach of this lesson is implicit. - Adding an
abstractmethod to the abstract class stopped the same consumer, a breaking reach; because the contract has no body, the language catches it. - When the counter was built with composition, the same parent change stayed in place:
3.
Next Step
The abstract method brought a contract without a body, and adding a method to the contract stopped the subclass. The Interfaces lesson separates this contract from inheritance: when a method is added to an interface, what do the classes that implement it experience?
To keep your progress and take notes, Log in
My notes
Log in to take notes.