left-icon

SOLID Principles Succinctly®
by Gaurav Kumar Arora

Previous
Chapter

of
A
A
A

CHAPTER 6

Liskov Substitution Principle

Liskov Substitution Principle


Let’s look at Wikipedia’s definition of the Liskov Substitution Principle:

If S is a subtype of T, then objects of type T may be replaced with objects of type S (i.e. objects of type S may substitute objects of type T) without altering any of the desirable properties of that program (correctness, task performed, etc.).”

I interpret this definition to mean that the parent should be easily replaced by the child object.

To understand the definition a bit more, let's look into another example using email operations such as the one in our OCP chapter. First, we have a class named EmailNotifications. If we also need to send emails for printing, what can we do?

We can create a new class. Let's call it NotificationsForPrint. It will inherit our class EmailNotifications. Both classes, the base and child, have at least one similar method.

Can we use our child class to substitute our base class? No, in this situation, we can never do that—which means we need to use inheritance. We’ll define two separate interfaces, one for building the message and another for sending the message, then we’ll decide on the implementation of where and for what we need to build and send messages.

Let me revisit our discussion of the Open-Closed Principle. OCP provides us with a way to create code that is maintainable and reusable. In other words, OCP is a guideline we can follow in order to extend code by changing old code (or working code).

We know that OCP is abstraction and polymorphism, and that raises questions. In fact, during a presentation I gave at a conference in Chandigarh, India, I was asked two questions that will lead us into an analysis of the Liskov Substitution Principle:

  • What is the best way to achieve inheritance here?
  • What are scenarios in which we violate OCP?

We’ll answer these questions by taking a look at what Barbara Liskov actually wrote about the principle:

“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.”

Note: Barbara Liskov is a computer scientist at the Massachusetts Institute of Technology.

Liskov’s definition is simple and straightforward, but I had difficulty correlating it with the real world, and I struggled to map this principle with my project. I am very thankful to Robert C. Martin for defining it in a way that made sense to me:

Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.

I can correlate this with scenarios and examples from real-world past projects I’ve worked on. 

A couple of years ago, while I was working on a database-synchronization project, we refreshed our development database from the production database (with an algorithm so that actual data requiring secrecy or security wouldn’t be copied into actual form). Validation was the backbone of this project.

Actual validations can become much more complex than the examples we looked at earlier, so let me draft out a few code snippets from the validations module of this project.

We have our main validation interface in Code Listing 19.

Code Listing 19

namespace LSP_Violation

{

    public interface IValidator

    {

        void Load();

        bool IsValid();

    }

}

With the use of the Load() method, we simply load all validations or validation settings on the fly-in system, and, with the use of the IsValid() method, we perform the validation.

Here are few of the various important validations that implement the IValidator interface:

  • TypeValidator. This validates the required type for the local database from the production/external database, as shown in Code Listing 20

Code Listing 20

using System;

namespace LSP_Violation

{

    public class TypeValidator : IValidator

    {

        public bool IsValid()

        {

            throw new NotImplementedException();

        }

        public void Load()

        {

            throw new NotImplementedException();

        }

    }

}

  • IPValidator. This includes a few restrictions, such as not being able to sync data if someone trying to sync from that IP is not on our whitelist or if the IP is not on the list of valid external database IP. Code Listing 21 depicts such a scenario.

Note: A whitelist is a list of entities (such as IP, external database names, email ID, etc.). Refer to: https://en.wikipedia.org/wiki/Whitelist.

Note: There are several external database servers from which our systems sync and import the data. In Code Listing 21, we are using IP to get connected with external databases. Class IPValidator is responsible for making sure our system is syncing and importing the data from an authenticated database server by using a valid IP.

Code Listing 21

using System;

namespace LSP_Violation

{

    public class IPValidator : IValidator

    {

        public bool IsValid()

        {

            throw new NotImplementedException();

        }

        public void Load()

        {

            throw new NotImplementedException();

        }

    }

}

  • DateValidator. There are several date validations on the basis of region, zone, and area. This validator simply validates quick-date type for the relevant base and performs the validation check, as we see in Code Listing 22.

Code Listing 22

using System;

namespace LSP_Violation

{

    public class DateValidator : IValidator

    {

        public bool IsValid()

        {

            throw new NotImplementedException();

        }

        public void Load()

        {

            throw new NotImplementedException();

        }

    }

}

This module works as a service that allow us to reduce the overburdening of our application. Code Listing 22 depicts a few code snippets that show the usage of these validators in a real project.

Next, let’s make a client and take a look how our code works, although, as you’ll see in Code Listing 23, it actually violates LSP.

Code Listing 23

using System;

using System.Collections.Generic;

namespace LSP_Violation

{

    class Program

    {

        static void Main(string[] args)

        {

            var validators = LoadAllValidationRules();

            Console.WriteLine("RuleValidations are {0}", IsValidationRulePassed(validators) ? "passing" : "failing");

        }

        private static IEnumerable<IValidator> LoadAllValidationRules()

        {

            var validators = new List<IValidator> {

                                                    new TypeValidator(),

                                                    new IPValidator(),

                                                    new DateValidator(),

                                                    new DynamicValidator()

                                                   };

            validators.ForEach(v => v.Load());

            return validators;

        }

        private static bool IsValidationRulePassed(IEnumerable<IValidator> validators)

        {

            bool isValid = false;

            foreach (var v in validators)

            {

                if (v is DynamicValidator)

                    continue;

                isValid = v.IsValid();

                if (!isValid)

                    return false;

            }

            return false; ;

        }

    }

}

Note: The previous code is not a complete code.

Can you revisit Code Listing 23 and identify which SOLID principle it’s violating?

It looks good. The code seems pretty clean, it’s well abstracted, and it’s neat and readable by humans. However, it’s breaking our first SOLID principle, SRP (refer to chapter 5).

There are other types of validators that do not require either a Load() method or an IsValid() method. For example, DynamicValidator doesn’t require an IsValid() method. I refer to it as a magic validator, as it has performed a few magical acts. For instance, it didn’t perform a validation check by itself, but instead used other validators, which means it didn’t require an IsValid() method. Here it is at work in Code Listing 24.

Code Listing 24

using System;

namespace LSP_Violation

{

    public class DynamicValidator : IValidator

    {

        public bool IsValid()

        {

            throw new NotImplementedException();

        }

        public void Load()

        {

            throw new NotImplementedException();

        }

    }

}

Note: In this case, can you identify other ways to bypass the IsValid() method check without making a conditional decision?

In order to include this new validator, we have to revisit and rewrite our program—something similar to Code Listing 25.

Code Listing 25

private static IEnumerable<IValidator> LoadAllValidationRules()

        {

            var validators = new List<IValidator>

            {

                new TypeValidator(),

                new IPValidator(),

                new DateValidator(),

                new DynamicValidator()

            };

            validators.ForEach(v => v.Load());

            return validators;

        }

And finally, our main triggering method would look like Code Listing 26.

Code Listing 26

private static bool IsValidationRulePassed(IEnumerable<IValidator> validators)

        {

            bool isValid = false;

            foreach (var v in validators)

            {

                if (v is DynamicValidator)

                    continue;

                isValid = v.IsValid();

                if (!isValid)

                    return false;

            }

            return false;

        }

In this code, we are skipping the IsValid() check for DynamicValidator. We found a way by simply skipping that particular validator so that we can avoid certain issues. However, what if we have more than 10 validators with the same kind of behavior? Do we need to perform the same check?

In that case, far too many conditions will need to be defined.

Let’s think about fixes for this problem. In one instance, someone might suggest making some logic in the IsValid() for DynamicValidator class. That will work, but it’s not a good practice.

Tip: Avoid giving a method or code a name that doesn’t match what it does.

Let’s think of something that does not violate any principles and will produce expected results.

Making fancy stuff by type-checking isn’t important or required in order to perform such operations. If you’re doing that, you are surely somehow violating LSP.

A proper solution for this issue is to follow the Interface Segregation Principle (we will discuss ISP in detail in the coming chapter). In order to write a proper solution, we can think about our two methods, Load() and IsValid().

Load() merely loads all validation settings and rules.

IsValid() actually performs validation checks.

In the above path, we have to make changes as follows.

Code Listing 27

namespace LSP_Follow

{

    public interface IValidator

    {

       bool IsValid();

    }

}

So, we must split it into two interfaces, IValidatorCheck and IValidatorLoader, as we see in Code Listing 28.

Code Listing 28

public interface IValidatorLoader

{

    void Load();

}

Next, we can implement the required interface for our Validator class in Code Listing 29.

Code Listing 29

public class DynamicValidator : IValidatorLoader

{

    public void Load()

    {

        throw new NotImplementedException();

    }

}

Nice, we did it. Let’s rewrite our main code in Code Listing 30.

Code Listing 30

  private static IEnumerable<IValidatorLoader> LoadAllValidationRules()

        {

            var validators = new List<IValidatorLoader>

            {

                new TypeValidator(),

                new IPValidator(),

                new DateValidator(),

                new DynamicValidator()

            };

            validators.ForEach(v => v.Load());

            return validators;

        }

Code Listing 30  names only types of IValidatorLoader (or types required only to load settings or validation rules).

Finally, with Code Listing 31 we need to perform validation checks for only those validators that actually require it.

Code Listing 31

private static bool IsValidationRulePassed(IEnumerable<IValidatorCheck> validators)

        {

            bool isValid = false;

            foreach (var v in validators)

            {

                isValid = v.IsValid();

                if (!isValid)

                    return false;

            }

            return false; ;

        }

The preceding implementation actually shows ISP, which, according to Wikipedia, means:

"No client should be forced to depend on methods that it does not use.” The definition continues, stating that large interfaces should be broken into small and more specific interfaces.

That will be the topic of our next chapter. But first, let’s revisit the main point of this chapter:

The Liskov Substitution Principle tells us how to handle real-time problems.

Scroll To Top
Disclaimer
DISCLAIMER: Web reader is currently in beta. Please report any issues through our support system. PDF and Kindle format files are also available for download.

Previous

Next



You are one step away from downloading ebooks from the Succinctly® series premier collection!
A confirmation has been sent to your email address. Please check and confirm your email subscription to complete the download.