Liskov substitution principle (LSP)

      Comments Off on Liskov substitution principle (LSP)

Home Forums Programming Software engineering Object-oriented programming Liskov substitution principle (LSP)

This topic contains 0 replies, has 1 voice, and was last updated by  Васильев Владимир Сергеевич 2 weeks, 4 days ago.

  • Author
    Posts
  • #4091

    This article provides a short and clear description for one of the main code design rules (SOLID) – the Liskov substitution principle. It includes examples in C# programming language, one of them demonstates the violation of this rule, another one shows its clever use.

    Liskov Substitution Principle (LSP) claims: subtypes should be interchangeable with their base types. What does this mean?

    In the original formulation the substitution principle sounds as: if S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program.

    The second explanation of this principle is a more readable: functions that use references to base classes must be able to use objects of derived classes without knowing it.

    In more simple words we can say that the behavior of derived classes must not contradict the behavior specified by the base class, which means that the behaviorof inherited classes should be pridictable for the code that uses the variable of its base type.

    Paraphrasing again, we get the following statement: the attempt to pass an object of derived class instead of the base class should not destroy any existing functionality in the caller. You should be able to substitute all implementations of this interface.

    Not to make all of the above sound unfounded, let us confirm the theory with practical examples, and first let’s look at the example of program that violates the principle of substitution. It represents the base class Bird. Naturally, the main functional behavior for birds is the ability to fly, so the method, which is implemented in this class – Fly().

    Two classes derived from it are Pegion and Ostrich. Both of them represent birds. In the class Pegion method Fly() is overridden in its own way)), and in the class Ostrich – we have to face the fact that, unfortunately, this kind of birds are not capable to fly …

    using System;
    using System.Collections.Generic;
    
    class Bird
    {
       public virtual void Fly()
       {
          Console.WriteLine("Flying ....");
       }
    }
    class Pigeon: Bird
    {
       public override void Fly()
      {
          //implementing some flying abilities
          base.Fly();
          Console.WriteLine("...and shitting on your head wherever you go ;)");
       }
    }
    class Ostrich : Bird
    {
       public override void Fly()
       {
          //Oops....
         Console.WriteLine("I wish i could but ....");
       }
    }
    
    class Program
    {
       static void Main()
       {
           var birds = new List<Bird>();        
           birds.Add(new Pigeon());
           birds.Add(new Ostrich());        
           foreach (Bird i in birds)        
          {
             i.Fly();       }
       }
    }

    When we are testing the work of all three classes in the entry point – it becomes obvious that although syntactically we can substitute instances of class Ostrich, wherever the instance of the Bird class is expected, we can not count on the appropriate use of the functional abilities in these descendants. It turns out that in the case of class Ostrich, even though it is a descendant of the class Bird, you can not rely on the behavior characteristic typical for other kinds of birds.

    Now let’s look at another example which shows the class for a general description of a postal address. It lists all the attributes of a postal address, such as country, city, zip code and others. All of these attributes are present in the description of a postal address of any country. But different countries determine the sequence of these attributes in their own way.

    Further, as we can see, there are 3 different implementation of a base interface presented: the UkrainePostalAddress, the UKPostalAddress, and the GermanyPostalAddress classes, where the method WriteAddress() defines what the postal address looks like in the UK, Germany and the Ukraine. In the entry point we are creating a collection of objects that inherit class PostalAddress and testing the WriteAddress() method for each of them. Whichever attributes sequence may have been in a particular case, it does not change the general format definition of the postal address.

    Thus, wherever the use of any specific implementations of postal address claases, they all look quite clearly, predictably and correctly. Therefore, we can conclude that this code example demonstrates the correct use of the substitution principle . Now let’s see the details:

    using System;
    using System.Collections.Generic;
    
    abstract class PostalAddress
    {
       public string Addressee { get; set; }
       public string Country { get; set; }
       public string PostalCode { get; set; }
       public string City { get; set; }
       public string Street { get; set; }
       public int House { get; set; }
       public int Apartment { get; set; }
    
       public abstract string WriteAddress();
    }
    
    class UkrainePostalAddress : PostalAddress
    {
       public override string WriteAddress()
       {
          return String.Format("{0}\n{1}\n{2}\n{3} street, 
    {4}, apartment {5}\n{6}", Country, PostalCode, City, Street, 
     House, Apartment, Addressee);
       }
    }
    class UKPostalAddress : PostalAddress
    {
       public override string WriteAddress()
       {
           return String.Format("{0}\n{1}, 
    {2}-{3},\n{4}\n{5}\t{6}", Addressee, House, Street, Apartment, City, 
    PostalCode, Country);
       }
    }
    class GermanyPostalAddress : PostalAddress
    {
       public override string WriteAddress()
       {
          return String.Format("{0}\n{1} street, {2}, 
    apartment #{3},\n{4}\t{5}\n{6}", Addressee, Street, House, Apartment, 
    PostalCode, City, Country);
       }
    }
    class AddressWriter
    {
       public string PrintPostalAddress(PostalAddress writer)
       {
          return writer.WriteAddress();
       }
    }
    class Program
    {
       static void Main()
       {
          var writer = new AddressWriter();
          var ukrAddress = new UkrainePostalAddress() { 
    Country = "Ukraine", PostalCode = "61098", City = "Kharkiv", Street = 
    "Svobody", House = 12, Apartment = 42, Addressee = "Bella Earlich" };
           var ukAddress = new UKPostalAddress() { 
    Country = "UK", PostalCode = "GU177 7AS", City = "Aldershot", Street = 
    "Lovelyview Road", House = 7, Apartment = 1, Addressee = "Ian Durbridge"
     };
           var germanAddress = new 
    GermanyPostalAddress() { Country = "Deutschland", PostalCode = "12345", 
    City = "Berlin", Street = "Musterstr", Apartment = 120, House = 14, 
    Addressee = "Max Mustermann" };
    
           var postAddreses = new 
    List<PostalAddress>() { ukrAddress, ukAddress, germanAddress };
    
           foreach (var address in postAddreses) {
            Console.WriteLine(writer.PrintPostalAddress(address));
            Console.WriteLine("----------------------------");
          }
    
       }
    }

    The example shows that the class AddressWriter can accept the reference to any type that implements the PostalAddress interface as a parameter of the PrintPostalAddress() method .

    Hopefully, the two examples represented in this article make it clear what is the substitution principle (LSP) and what might be its violation, which is a misuse of the inheritance hierarchy.

You must be logged in to reply to this topic.