Liskov Substitution Principle

We previously covered the S — Single Responsibility Principle (SRP) and O — Open-Closed Principle (OCP). Now, we come to the letter L in SOLID and this stand for the Liskov Substitution Principle(LSP).

This principle was introduced by Barbara Liskov in her conference keynote data abstraction in 1987.

LSP is extension of of O — Open-Closed Principle (OCP). This principle is there to help you to implement O — Open-Closed Principle (OCP) in a correct way to ensure that you can change one part of code without breaking other parts.

Concept

If S is a sub-type of T, then objects of type T may be replaced with objects of type S without altering any of the desirable properties of the program.

Basically, it means every time we prepare a sub-class that sub-class should be substitutable or we can say replaceable in every place where the original class was accepted without breaking the application.

Example 1

Let’s say we have class called Parent and which has a sub-class called Child that extends parent class.

<?php

class Parent() {
public function doSomething() {
// ...
}
}
<?php

class Child() extends Parent {
public function doSomething() {
// ...
}
}
// ...

function someRandomFunction(Parent $parent){
$parent->doSomething();
}

$parent = new Parent();
someRandomFunction($parent);

// According to this principle following code should also work
$child = new Child();
someRandomFunction($child);

//...

Now let’s say inside Child class’s doSomething function there’s an condition which throws an exception.

<?php
class Child() extends Parent {
public function doSomething() {
if(condtion) {
throw new Exception();
}

// ...
}
}

Then, it violates LSP. According to the principle the child class should be extending the behavior, it shouldn’t alter the behavior of a parent class. But in this scenario we have altered its behavior so it’s violating the LSP.

Example 2

Let’s say we have to send an email using third party service.

abstract class EmailProvider {
abstract public function addSubscriber(User $user): array;

/**
* @throws Exception
*/
abstract public function sendEmail(User $user): void;
}
class MailChimp extends EmailProvider {

public function addSubscriber(User $user): array
{
// Using MailChimp API
}

public function sendEmail(User $user): void
{
// Using MailChimp API
}
}
class ConvertKit extends EmailProvider {
public function addSubscriber(User $user): array
{
// Using ConvertKit API
}

public function sendEmail(User $user): void
{
// Using ConvertKit API
}
}

We have an abstract EmailProvider and we use both MailChimp and ConvertKit for some reason. These classes should behave exactly the same way, no matter what.

So if AuthController adds a new subscriber:

class AuthController {
public function register(
RegisterRequest $request,
EmailProvider $emailProvider
){
$user = User::create($request->validated());

$subscriber = $emailProvider->addSubscriber($user);
}
}

We should be able to use any of these classes without any problem. It should not matter if the current EmailProvider is MailChimp or ConvertKit .

We should be able to switch the argument:

public function register( 
RegisterRequest $request,
ConvertKit $emailProvider
) {}

This sounds obvious, however, there are some important thing that needs to be satisfied:

  • Same method signatures. In PHP we’re not forced to use types so it can happen that the method has different types in MailChimp compared to ConvertKit.
  • It’s also true for return types. Of course, we can type-hint these, but what about an array or a Collection ? It’s not guaranteed that an array contains the same types in multiple classes, right? As you can see, the addSubscriber method returns an array that contains the subscriber’s data received from the APIs. Both MailChimp and ConvertKit return a different shape. They are arrays, yes, but they are completely different data structures. So we cannot be 100% sure that RegisterController works correctly with any email provider implementation. This is why it’s a good idea to have DTOs when working with 3rd parties.
  • The same exceptions should be thrown from each method. Since exceptions cannot be type-hinted in the signature it’s also a source of difference between these classes.

As you can see the principle is quite simple but it’s easy to make mistakes.

By using interfaces we can implement methods that various classes have in common, but each method will have its own implementation, its own pre and post conditions, its own in-variants, etc. We are not tied to a parent class.

⚠️ This does not mean that we start using interfaces everywhere, although they are very good. But sometimes it is better to use base classes and other times interfaces. It all depends on the situation.

Interfaces 🆚 Abstract Class

1 Interface benefits

  • Does not modify the hierarchy tree
  • Allows to implement N Interfaces

2 Benefits of Abstract Class

  • It allows to develop the Template Method¹ pattern by pushing the logic to the model. Problem: Difficulty tracing who the actors are and when capturing errors
  • Private getters (Tell-Don’t-Ask principle)

¹. Design pattern Template Method: It states that in the abstract class we would define a method body that defines what operation we are going to perform, but we would be calling some methods defined as abstract (delegating the implementation to the children). But beware! 👀 this implies a loss of traceability of our code.

Conclusion of Interfaces 🆚 Abstract Class

  • When do we use Interfaces?: When we are going to decouple between layers.
  • When do we use Abstract?: In certain cases for Domain Models (Domain models not ORM models, to avoid anemic domain models)

<< Previous O — Open-Closed Principle (OCP)

>> Next I — Interface Segregation Principle (ISP)

--

--