Liskov Substitution Principle – Deep Dive

Table of Contents

Intro

Alone we can do so little, together we can do so much.

(Helen Keller)

The above quote is so simple but has a profound meaning to SOLID principles, how? ?

writing about design principles made me realise that the real benefit of using those principles are when combined. In other words, a drawback of one principle might be covered or strengthen by another ?.

Going back to our topic, we previously discussed the open-closed principle and we mimicked an e-payment gateway with it. To continue the road trip with SOLID. We will discuss the third principle of SOLID, which is the Liskov Substitution Principle (LSP). 

In addition, we will rewrite the e-payment gateway to comply with this principle.  However, to spice things up, we will use Dart programming language with a cool exception handling technique ??‍?. 

Principle Definition

I started my research about this principle by reading the definition of the principle as anyone would do. However, the definition made me go like, huh? ? what the hell is she saying? ? wait a minute, why she did that? ? oooh, I get it now ?.

My shock moments was mainly due to the depth of academic linguistic used in the definition. For instance, below are the two common definitions of the principle.

Let Φ(x) be a property provable about objects x of type T. Then Φ(y) should be true for objects y of type S where S is a subtype of T. 

If S is a declared subtype of T, objects of type S should behave as objects of type T are expected to behave, if they are treated as objects of type T

Both definitions sound like a math formula for me, and the use of generics and those weird looking characters didn’t help either ?‍♂️.  So what does this all mean?

Simply, we should be able to change one sub-class with another without breaking the code if they extend the same superclass or implement the same interface . That’s the core of the principle. Nevertheless, there are more to it, the principle comes with a set of rules that must be followed. Noncompliance with any LSP rule leads to LSP principle violation.

Side note, I wouldn’t list the rules as pointers. Instead, we will experience them together in action by auditing the compliance of the E-payment gateway code with LSP, after the real life example.

Real Life Example

Assume that you have a driving license. With that license, you can drive the vehicles that the license applies to. For instance, in my country we have a light, heavy-duty, motorcycle, just to mention a few.

We will assume that you have light driving licences, and you normally drive Toyota Corolla 1970, while your friend roll with Ford Mustang 2019. Now your car broke ?,  you know how old cars behave. However,  your friend offered you to have his car for a while. Knowing that both cars fall under the same driving licences category, answer the below questions.

  1. Would you change the shape of your leg to drive the Mustang?
  2. Would you need extra fingers to hold the steering wheel? 
  3. Would there be anything significant you have to do to be able to drive the mustang? 

The obvious answer is NO for all questions, the main reason is that both cars behave similarly. Therefore, you are able to swap between cars that fall under the same driving license. 

This is exactly what the principle is all about, changing between subclasses that extends a common superclass should not be a hassle. 

LSP Validation of E-Payment Gateway

In our attempt to follow the open-closed principle, we created an interface for our e-payment gateway as follows.

				
					public interface PaymentGateway {
   String generatePaymentLink(PurchaseOrder purchaseOrder);
}
				

The interface had a single method which is generatePaymentLink. However, let’s stop for a minute and think? Does this interface cover all the cases? we will soon find out.

Case Scenario

Assume that you have an urgent requirement to add Cash as a new payment option ?. Noticeably, this particular option does not generate a link, so how you will solve it? Here is a suggested solution by programmer X.

Programmer X had two solutions in mind. The first is to implement the PaymentGateway and return an empty string in the generatePaymentLink  method. The second is to implement the PaymentGateway  and  throw NotImplementedException on generatePaymentLink method. Both solutions are shown below. 

				
					public class CashPayment implements PaymentGateway {
    @Override
    public String generatePaymentLink(PurchaseOrder purchaseOrder) {
        throw new NotImplementedException();
    }
}
				
				
					public class CashPayment implements PaymentGateway {
    @Override
    public String generatePaymentLink(PurchaseOrder purchaseOrder) {
        return "";
    }
}
				

The first solution is returning an empty string which is the same as having an empty method!!.

The second solution throws an exception which changes the behaviour and the expected result method. In addition, the exception was not defined in the interface. Besides, a developer using this code will not anticipate an exception, instead, a developer will safely assume that this method returns a string.

In both solutions, extra logic is needed to handle the exception and the empty string, resulting in a code smell and LSP violation. In short, subclasses should behave similarly, implement all the required methods, and return the expected return type.

Going further, it is time to re-write the code using dart.

Coding Prerequisites

This section defines the prerequisites needed for this example. 

  1. I will assume that you already installed Dart SDK. But, If you haven’t, follow the steps on the official website.  
  2. Download any IDE that you like, just make sure it supports dart or has extensions for it. If you wonder, I use IntelliJ IDEA Community Edition
  3. Create a new console application.  
  4. Define the folder structure as illustrated below in the Folder Structure section 
  5. Add two external libraries illustrated below in the External Libraries section 
  6. You ready to start coding!!! 

Folder Structure

As seen in the above picture, the folder structure for this example is inspired by a flutter clean architecture. For more details visit the link.

External Libraries

This project needs two external libraries which are dartz and intl. To do so, open your pubspec.yaml file and added both libraries to the dependencies section as shown below. 

				
					dependencies:
  dartz: ^0.9.2
  intl: ^0.17.0
				

Dartz will give superpowers for dart programming language ?  just kidding it will enable functional programming within dart. On the other hand, intl is just a helper library for date and time manipulation.

Let’s Code

Now it’s time to start writing the actual code. But before we start, I want to know how to better follow along with the article. First of all, don’t worry about the code you will find it all here

Quite the game of copying the code blindly

I provided the code here to make it easier for the reader to understand, I expect you to read the entire article, understand the code and the concept behind it thoroughly then apply it by yourself. 

Entities Preparation

In this step, we will create classes that will be used mainly to set and get data from. Therefore, go to your entities folder and create two dart files as shown in the below image.

payment_result.dart 

				
					class PaymentResult {
  bool isActionable;
  String actionLink;
  // constructor used to pass the data
  PaymentResult(this.isActionable, this.actionLink);
  // helper function to get the data in json format
  Map<String, dynamic> toJson() {
    final data = <String, dynamic>{};
    data['is_actionable'] = isActionable;
    data['action_link'] = actionLink;
    return data;
  }
}
				

This class will hold the result of processing the payment. Noticeably, the class contains a boolean value called isActionable, the intention of this variable is to determine if the payment type requires action from the frontend or not. The action assumed to be showing a web view to process the payment. 

purchase_order.dart 

				
					class PurchaseOrder {
  // private variables for this class
  final String productName;
  final int quantity;
  final double price;
  final int userId;
  final int paymentType;
  // PurchaseOrder constructor used to pass the values
  PurchaseOrder(this.productName, this.quantity, this.price, this.userId,
      this.paymentType);
  // toJson helper method to print the values in json format
  Map<String, dynamic> toJson() {
    final data = <String, dynamic>{};
    data['product_name'] = productName;
    data['quantity'] = quantity;
    data['price'] = price;
    data['userId'] = userId;
    data['payment_type'] = paymentType;
    return data;
  }
}
				

This class mimics the purchase order coming from the Frontend. 

Side note: toJson() method is just a helper method to convert the class data to JSON format. In addition, dart offers a nice way to create a constructor as well as passing the data easily as seen in both models. 

Failure Class

In this step, we will define a class that will be used to hold data related to any exception in the application. Therefore, go to the exceptions folder and create failure.dart with the below code.

				
					import 'package:intl/intl.dart';
class Failure {
  String failureMessage;
  List<String> failureDetails;
  String failureDateTime =
      DateFormat('y/MMMM/d hh:mm:ss').format(DateTime.now());
  // constructor with failure message only
  Failure(this.failureMessage);
  // constructor with failure message only and details
  Failure.withDetails(this.failureMessage, this.failureDetails);
  Map<String, dynamic> toJson() {
    final data = <String, dynamic>{};
    data['failure_message'] = failureMessage;
    data['timestamp'] = failureDateTime;
    // ensures the list in not null nor empty
    if (failureDetails != null && failureDetails.isNotEmpty) {
      data['failure_details'] = failureDetails;
    }
    return data;
  }
}

				

This class provides two constructors Failuer and Failuer.withDetails. The main reason for this is to provide a way for extra details if needed. 

Payment Provider Interface

This part is the most important part in this example. We will define an interface that will be used by all the payment providers such as Cash, PayPal or Stripe. To do so, go to the interfaces folder under the domain and create a payment_provider.dart file with the below content.

				
					import 'package:dartz/dartz.dart';
import '../entities/payment_result.dart';
import '../entities/purchase_order.dart';
import '../exceptions/failure.dart';
abstract class PaymentProvider {
  Future<Either<Failure, PaymentResult>> ProcessPayment(
      PurchaseOrder purchaseOrder);
}
				

The syntax of the function might look weird but it is really just plain English. let’s take it apart. 

Future →  this function will happen in the future and we don’t know how much time it takes to complete. 

Ethier →  this function will return one of the two types either Failure or PaymentResult.

Therefore, this function will take the purchase order and process the payment and return either a success PaymentResult object or a Failure object.

In addition, we cannot estimate the time it will take to process the payment, especially that we might talk to external payment providers. Therefore, this function will utilise asynchronous programming to run this function and wait for it to finish. 

Cash Payment

We will now implement the interface with the required payment type from the case scenario which is cash payment.  To do so, create cash_payment_usecase.dart file in the payment folder under the usecases folder, with the below content. 

				
					import 'dart:math';
import 'package:dartz/dartz.dart';
import '../../entities/payment_result.dart';
import '../../entities/purchase_order.dart';
import '../../exceptions/failure.dart';
import '../../interfaces/payment_provider.dart';
class CashPayment implements PaymentProvider {
  // _cashPaymentLogic private logic for the payment by cash
  Future<Either<Failure, PaymentResult>> _cashPaymentLogic(
      PurchaseOrder purchaseOrder) async {
    // a real logic will take sometime so lets mimic that
    await Future.delayed(Duration(seconds: 1));
    // the logic might produce an error or success
    // this code will randomly do that
    if (Random().nextBool()) {
      // returning success result
      return right(PaymentResult(false, ''));
    }
    // returning payment failure
    return left(Failure('Sorry something went wrong during cash payment'));
  }
  @override
  Future<Either<Failure, PaymentResult>> ProcessPayment(
      PurchaseOrder purchaseOrder) async {
    return await _cashPaymentLogic(purchaseOrder);
  }
}

				

The class mimics a cash payment that takes 1 second to complete. In addition,  it will randomly return either Failure or PaymentResult.  Notice that the way Either work is by returning right or left with the corresponding object.

Going further, I will let you code the Paypal payment provider or any payment provider that you like.

Next, we will create a payment provider repository, a payment provider service and finally use the service in main.dart. But before that let’s take a meme break.

Payment Repository

The payment repository will be responsible to provide any payment type that a class require, this is a good example of single responsibility principle.

Side note, this not the actual way of a proper repository pattern, it just a mimic to simplify things, otherwise, this post will be long and overwhelming ?. 

To start with the repository, go to the repositories folder under data and create payment_repository.dart with the below content.

				
					import 'package:dartz/dartz.dart';
import '../../exceptions/failure.dart';
import '../../interfaces/payment_provider.dart';
import '../../usecases/payment/cash_payment_usecase.dart';
import '../../usecases/payment/paypal_payment_usecase.dart';
class PaymentRepository {
  // internal variable to hold the singleton instance
  static PaymentRepository _instance;
  // an internal constructor to set the instance
  PaymentRepository._getInstance() {
    _instance = this;
  }
  // public constructor to get access to this class
  factory PaymentRepository() {
    // set the instance if its null
    if(_instance == null) {
       PaymentRepository._getInstance();
    }
    return _instance;
  }
  final Map _paymentTypeList = {
    1: PaypalPayment(),
    2: CashPayment(),
  };
  // provides PaymentGateway given the ID of the e-payment
  // throws an Exception if payment provider dose not exist
  Future<Either<Failure, PaymentProvider>> paymentProviderByID(
      int paymentTypeID) async {
    // finding the the first entry that matches the ID of payment type
    // else set the result to null
    final result = _paymentTypeList.entries.firstWhere(
      (paymentType) => paymentType.key == paymentTypeID,
      orElse: () => null,
    );
    // returning the result if payment type was found
    // else returning a Failure
    return result != null
        ? right(result.value)
        : left(
            Failure.withDetails(
              'Payment Type Dose Not Exist',
              [
                'Details example',
                'Details example 2',
              ],
            ),
          );
  }
}

				

Let’s talk about some interesting snippets of this code, which are Map , factory and firstWhere. Below is explination of each keyword.

  • Map is a (key, value) based list. Therefore, 1 and 2 are the key part, which assumed to be the unique ID’s of the payment type. Whereas, the value part of the Map is the actual instance of the payment type.
  • factory is a keyword used when implementing a constructor that doesn’t always create a new instance of its class. In other words, singleton.
  • firstWhere is SQL like syntax where you get the first value result from the list according to a certain condition.

We are almost done ?‍?  two steps ahead, lets go to a meme a break before we finalize the code.

Payment Service

This service layer will be the glue between the logic in the domain layer and the application layer ?‍?hmm before you give the wondering face let me explain.

Imagine it this way, the domain layer represents a library that you don’t have access to it and have no idea about what voodoo magic it runs ?  (e.g. Dart Packages).

Your application will call the library as a service with the required input and get the result without knowing the actual implementation. 

The same idea applies here, we will create a service called payment_service.dart under the services folder with the below content. 

				
					import 'package:dartz/dartz.dart';
import '../../domain/data/repositories/payment_repository.dart';
import '../../domain/entities/payment_result.dart';
import '../../domain/entities/purchase_order.dart';
import '../../domain/exceptions/failure.dart';
class PaymentService {
  //write the singlton here
  Future<Either<Failure, PaymentResult>> attemptProcessingPayment(
      PurchaseOrder purchaseOrder) async {
    final paymentRepo = PaymentRepository();
    // gets the payment from the PaymentRepository
    final paymentProviderResult = await paymentRepo.paymentProviderByID(
      purchaseOrder.paymentType,
    );
    // handling the result from repository
    // left  -> payment provider not found
    // right -> we got the payment provider
    return paymentProviderResult.fold(
      (paymentProviderFailure) => left(paymentProviderFailure),
      (paymentProvider) async {
        // process the payment through the payment provider
        final processingPaymentResult = await paymentProvider.ProcessPayment(
          purchaseOrder,
        );
        //  handling the result from ProcessPayment
        // left  -> process payment error
        // right -> payment done successfully
        return processingPaymentResult.fold(
          (paymentFailure) => left(paymentFailure),
          (paymentResult) => right(paymentResult),
        );
      },
    );
  }
}
				

This class has two parts, a singleton similar to the previous class, and the attemptProcessingPayment method.

Side Note: I hid the singleton part so that you try by your self.

The attemptProcessingPayment method works in two steps.

First, it will get the payment provider from PaymentRepositorty according to the paymentType from the PurchaseOrder object. Then,  paymentProviderResult.fold is used to get either Failure or PaymentProvider object.

The second step is to process the payment if we got the payment provider by using ProcessPayment method from paymentProvider object.

Finally, we fold the result and return back the final result.

Final Step

The final step is to call the service from main.dart we can do so as follows.

				
					import 'app/services/payment_service.dart';
import 'domain/entities/purchase_order.dart';
Future<void> main(List<String> arguments) async {
  var purchaseOrderMock = PurchaseOrder('Mango 1KG', 1, 1.2, 1241, 1);
  var attemptPaymentResult =
      await PaymentService().attemptProcessingPayment(purchaseOrderMock);
  attemptPaymentResult.fold(
      (paymentFailure) => print(paymentFailure.toJson()),
      (paymentResult) => print(paymentResult.toJson()));
}
				

In this class, we created a new purchase order, called the payment service, attempted to process the payment and finally printed the result. 

Final Thoughts

This principle might seem intimidating in the beginning, but once you get the hang of it, you will realise that it is simple but yet crucial. Practising the principle will improve the way you think about class abstraction, resulting in better code swapability.

Side note: If you haven’t found the word swapability in the dictionary, its okay I just invented it ?. What I mean by it, is that you will be able to swap one subclass with another without breaking the code. 

Support Ideas

If you liked the content and thought of supporting the creator, below are some ideas to do so.

  • your engagement in the comment will help giving more value to the content.
  • spreading the content in social media will help attract more readers and google ranking
  • you can simply unblock the ad blocker if you have one
  • also consider subscribing to the newsletter list

9 Comments

  1. Great article and impressively detailed implementation. Although I am not a Dart programmer, I think most programming languages force this principle through enforcing methods implementation in subclasses which extend either an interface or a superclass However, this principle is only relevant when using inheritance. Most of the time, programmers prefer using composition instead to reduce code complexity resulting from using inheritance. The application would be composed from several independent components. Maybe with a small manageable inheritance tree like the one you demonstrated in your article.

    • Thank you Faisal for the lovely encouraging comment. I haven’t dig deeper yet into the difference between inheritance and composition. However, one day I will inshallah and get back to you with a detailed replay.

      Thanks again for the comment, and I would love to read more insightful comments in new posts.

  2. Thanks for sharing knowledge, good technique to prevent any breaking action, as always enjoyable article mr.aljahwari ?

  3. Simply want to say your article is as amazing.The clearness in your post is just spectacular and i can assume youre an expert on this subject.Fine with your permission allow me to grab your feed to keep up to date with forthcoming post.Thanks a million and please carry on the enjoyable work

    • Thank you very much for the encouraging comment and feel free to subscribe to the mailing list to get my latest post once I post them. To be honest, I am not an expert on the subject nor I like job titles or anything of that sort. I am just a software developer with an aim of becoming better every day.

  4. […] we discussed Liskov Substitution Principle and we came to know that having empty methods or returning not implemented exception is a violation […]

  5. Very interested
    It is useful

Leave a Reply

Physical Address

304 North Cardinal St.
Dorchester Center, MA 02124