DEV Community

loading...
Cover image for Learning Liskov Substitution Principle by Example

Learning Liskov Substitution Principle by Example

Coding Bugs
・5 min read

Welcome to this third article in which I will show the importance of applying the Liskov Substitution Principle when implementing the code of our application.

In the same way as the previous articles talking about the Single Responsibility Principle and the Open-Closed Principle, the advantages to applying this principle are: code comprehension, component reusability, increased development speed, ease of testability, etc.

What Liskov Substitution means

The Liskov Substitution Principle is framed within the set of SOLID principles, a set of rules to guide our code in what it's called Clean Code. In this case, each letter represents a different principle:

This principle was established by Barbara Liskov in 1988:


What is wanted here is something like the following substitution property: If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.


After reading the statement, we could not have clear what it means. So many words, so many general terms may be. If we do some more reads and take care of the sense of the words, we realized that the following is the formal sentence of talking about inheritance or subtyping:

  • We have a program called P,
  • P is using an object of type S,
  • We also have an object of type T,
  • If we change P to use the object of type T and its behavior doesn't change, T is a subtype of S

It's relevant to notice that there is no programming language mentioned here. So, think more on a conceptual rule and take advantage of the programming language selected to apply it.

In my example, as I'm using JavaScript, I like to use functions instead of classes because they are just syntactic sugar to the prototype-based approach. More information in the ES6 specification.

Coding for learning

I keep working with the code of the previous article where we have a Timer entity that runs a Job entity with a specific frequency. Besides, I added the Rule entity to change the behavior of the job depending on the needs.

At this time, we have identified bugs in our system when running several jobs. We simulate this by creating a new Job entity that throws an error each time it runs. In addition, we want to register helpful information about the error.

In this case, we will implement a new Timer entity based on the functionality of the previous one because we want to be independent of the jobs executed. We don't want to implement a new way of launching Job entities because it is working fine.

So, the approach is to decorate the Jobs including an error control to avoid a crash in the system. In this case, the start method of the existing Timer object is responsible for executing the passed Job. The new Timer entity called SafeTimer will encapsulate the current job inside a wrapper with the code to control the errors.

As you can imagine from my previous articles, there is a new entity in the game called Decorator. As I mentioned in the Learning Single Responsibility Principle by Example article, we need to separate responsibilities and create as many functions or objects as we need to give sense to our code.

Now, the new ErrorHandlerDecorator entity comes with the following simple and easy to read code:

function ErrorHandlerDecorator(method) {
  return function() {
    try {
      method.apply(null, arguments);
    }
    catch(err) {
      // Add any code to inform about the error to the proper log or system
      console.log('error captured');
    }
  };
}
Enter fullscreen mode Exit fullscreen mode

The most complex thing to understand is how the apply method works. Here I share the link to the MDN documentation for this method.

Now, it's time to code the new SafeTimer entity. We need to ensure that all errors from any job passed are controlled.

function SafeTimer(frequency) {
  let timer = new Timer(frequency);

  let originalMethod = timer.start;
  timer.start = function(job) {
    originalMethod(new ErrorHandlerDecorator(job));
  };

  return timer;
}
Enter fullscreen mode Exit fullscreen mode

I overwrite the start method but keeping the same definition as we have in our original Timer entity.

The code of the App entity is changed as well to call to the new SafeTimer object implemented and keep the execution of our system safe.

function App(startAt, endsAt) {
  let frequency = 1; // In seconds

  let timer = new SafeTimer(frequency);
  let rule = new StopTimerWhenNumberIsHigherOrEqualToRule(timer, endsAt);
  let job = new PrintNumbersStartingAtJob(startAt, rule);

  timer.start(job.next);
}
Enter fullscreen mode Exit fullscreen mode

More and more functionality based on the entities defined and created can be implemented to cover the requirements and needs to come. For example, change the frequency from seconds to milliseconds, or adding more control when starting and stopping timers.

This article shows how to evolve the code of our apps and systems but it doesn't mean that the first time we design the entities we do perfectly fine. Everything can change and adapt to a better design and sometimes we probably go up defining and designing super classes or parent objects instead of going down and be more and more specific each round.

Wrapping up

This article shows how to evolve our code and its maintainability by applying the Liskov Substitution Principle.

It could be something difficult or strange to apply at the beginning if you are not used to it but makes you grow up on how to design the components of your app. Creating objects or classes more abstracts or more specific each time you redesign incurrs in problems of code organization. You're code structure should be something to take care always.

What do you think about the exercise made in this article?
Hope this can be useful to you or just have fun reading it.

Discussion (0)