In inheritance, child class extends parent class so child class knows what parent class does and this knowledge creates dependency. If there would be a way where child class ignores the knowledge of what parent class does rather asks what it needs from parent class will be a blessing.
Knowing things about other classes, as always, creates dependencies, and dependencies couple objects together.
– Sandi Metz
Let’s try to solve the below problem where different bicycles have different and some common behavior.
Example – 1: Child classes call Parent class
As child classes depend upon parent class they get coupled to parent class. Without calling parent class if new child class extends the parent class it will break which means every new child class needs to aware of how to call parent class function to achieve the behavior.
<?php
Abstract class Bicycle
{
public string $size;
public string $chain;
public string $tire_size;
public function __construct(...$args)
{
$this->size = $args['size'];
$this->chain = $args['chain'] ?? $this->default_chain();
$this->tire_size = $args['tire_size'] ?? $this->default_tire_size();
}
public function spares()
{
return [
'chain' => $this->chain,
'tire_size' => $this->tire_size,
];
}
protected function default_chain()
{
return '11-speed';
}
abstract function default_tire_size();
}
class RoadBike extends Bicycle
{
public string $tape_color;
public function __construct(...$args)
{
$this->tape_color = $args['tape_color'];
parent::__construct(...$args);
}
public function spares()
{
return array_merge(
parent::spares(), [
'tape_color' => $this->tape_color
]);
}
public function default_tire_size()
{
return '23';
}
}
class MountainBike extends Bicycle
{
public string $front_shock;
public string $rear_shock;
public function __construct(...$args)
{
$this->front_shock = $args['front_shock'];
$this->rear_shock = $args['rear_shock'];
parent::__construct(...$args);
}
public function spares()
{
return array_merge(parent::spares(), [
'front_shock' => $this->front_shock
]);
}
protected function default_chain()
{
return '10-speed';
}
public function default_tire_size()
{
return '28';
}
}
$aRoadBike = new RoadBike(size: 'M', tape_color: 'red');
print $aRoadBike->size;
print_r($aRoadBike->spares());
$aMountainBike = new MountainBike(size: 'S', front_shock: 'Manitu', rear_shock: 'Fox');
print $aMountainBike->size;
print_r($aMountainBike->spares());
It pushes knowledge of the algorithm down into the subclasses, forcing each to explicitly send parent to participate. It causes duplication of code across subclasses, requiring that all send parent in exactly the same places. And it raises the chance that future programmers will create errors when writing new subclasses, because programmers can be relied upon to include the correct specializations but can easily forget to send parent.
When a subclass sends parent, it’s effectively declaring that it knows the algorithm; it depends on this knowledge. If the algorithm changes, then the subclasses may break even if their own specializations are not otherwise affected.
Example – 2: Child classes does not call Parent class
In this example, child classes achieve the same behavior as example – 1 but they don’t call parent class.
<?php
abstract class Bicycle
{
public string $size;
public string $chain;
public string $tire_size;
public function __construct(...$args)
{
$this->size = $args['size'];
$this->chain = $args['chain'] ?? $this->default_chain();
$this->tire_size = $args['tire_size'] ?? $this->default_tire_size();
$this->post_initialize($args);
}
protected function post_initialize($args){}
public function spares(): array
{
return array_merge(
['chain' => $this->chain, 'tire_size' => $this->tire_size,],
$this->local_spares()
);
}
protected function local_spares(): array
{
return [];
}
protected function default_chain(): string
{
return '11-speed';
}
abstract function default_tire_size();
}
class RoadBike extends Bicycle
{
public string $tape_color;
protected function post_initialize($args)
{
$this->tape_color = $args['tape_color'];
}
protected function local_spares(): array
{
return [
'tape_color' => $this->tape_color
];
}
public function default_tire_size(): string
{
return '23';
}
}
class MountainBike extends Bicycle
{
public string $front_shock;
public string $rear_shock;
protected function post_initialize($args)
{
$this->front_shock = $args['front_shock'];
$this->rear_shock = $args['rear_shock'];
}
protected function local_spares(): array
{
return [
'front_shock' => $this->front_shock
];
}
protected function default_chain(): string
{
return '10-speed';
}
public function default_tire_size(): string
{
return '28';
}
}
$aRoadBike = new RoadBike(size: 'M', tape_color: 'reds');
print $aRoadBike->size;
print_r($aRoadBike->spares());
$aMountainBike = new MountainBike(size: 'S', front_shock: 'Manitu', rear_shock: 'Fox');
print $aMountainBike->size;
print_r($aMountainBike->spares());
Putting control of the timing in the superclass means the algorithm can change without forcing changes upon the subclasses.
RoadBike and MountainBike are more readable now that they contain only specializations. It’s clear at a glance what they do, and it’s clear that they are specializations of Bicycle.
Reference
Example and few paragraphs I borrowed from Practical Object-Oriented Design: An Agile Primer Using Ruby, 2/e – Sandi Metz.