SOLID principles PHP Edition. Today, Liskov Substitution Principle
SOLID principles PHP Edition. Today: Liskov Substitution Principle
In the noble art of coding, you should remember the SOLID principles always. Sometimes, I forget some of them, so here is a brief explanation of each principle:
- Single Responsibility Principle: A class should have only one reason to change.
- Open/Closed Principle: A class should be open for extension but closed for modification.
- Liskov Substitution Principle: You should be able to use any subclass in place of its parent class.
- Interface Segregation Principle: A class should not be forced to implement an interface it doesn't use.
- Dependency Inversion Principle: High-level modules should not depend on low-level modules. Both should depend on abstractions.
Today we are going to focus on the Liskov Substitution Principle.
This principle states that "functions that use pointers or references to base classes must be able to use objects of a derived class without knowing it".
In other words, derived classes must be completely substitutable for their base classes. If a derived class cannot be substituted for a base class, then the class hierarchy is not well designed and violates Liskov's substitution principle.
Here you have an example on PHP about it:
// Bad
class Bird {
public function fly() {
return "I can fly";
}
}
class Penguin extends Bird {
public function fly() {
return "I can't fly";
}
}
function letItFly(Bird $bird) {
return $bird->fly();
}
echo letItFly(new Bird()); // "I can fly"
echo letItFly(new Penguin()); // "I can't fly"
In this example, Penguin
is a subclass of Bird
. However, not all birds can fly, so when we try to make a Penguin
fly, we get an unexpected result. This violates Liskov's substitution principle.
A better way to do this would be to have a Bird
base class and a Flyable interface
that only implement birds that can fly:
class Bird {
}
interface Flyable {
public function fly();
}
class Sparrow extends Bird implements Flyable {
public function fly() {
return "I can fly";
}
}
class Penguin extends Bird {
}
function letItFly(Flyable $bird) {
return $bird->fly();
}
echo letItFly(new Sparrow()); // "I can fly"
In this example, if we try to fly a Penguin
, we will get a compile-time error, because Penguin
does not implement the Flyable
interface.
Another example:
// Bad
class Animal {
public function eat() {
return "I can eat";
}
}
class Lion extends Animal {
public function eat() {
return "I can eat meat";
}
}
class Rabbit extends Animal {
public function eat() {
return "I can eat vegetables";
}
}
class Plant extends Animal {
public function eat() {
throw new Exception("Plants do not eat");
}
}
function feed(Animal $animal) {
return $animal->eat();
}
echo feed(new Lion()); // "I can eat meat"
echo feed(new Rabbit()); // "I can eat vegetables"
echo feed(new Plant()); // Exception: Plants do not eat
// Good
class LivingEntity {
}
interface Eatable {
public function eat();
}
class Lion extends LivingEntity implements Eatable {
public function eat() {
return "I can eat meat";
}
}
class Rabbit extends LivingEntity implements Eatable {
public function eat() {
return "I can eat vegetables";
}
}
class Plant extends LivingEntity {
}
function feed(Eatable $entity) {
return $entity->eat();
}
echo feed(new Lion()); // "I can eat meat"
echo feed(new Rabbit()); // "I can eat vegetables"
I hope we understood it. See you in the next principle!.