Thursday, July 21, 2011

The Liskov Substitution Principle

In my previous post I talked about the Open-Closed principle and how it is located at the heart of many claims made for OOD such as inheritance abstraction and polymorphism. Having a design open for extension and closed for modification is a major goal that every application architect or OOP programmer should pursue.

I would like to continue with the third of the  S.O.L.I.D. Object Oriented Design Principles, the Liskov Subtitution principle. This principle was defined by Barbara Liskov, a well respected computer scientist and the first women in the US to recieve a PhD in Computer Science.

This principle is an extension of the Open-Closed, and it targets Public Polymorphism mainly. Thus, a proper abstraction needs to be made when a public contract (class or interface) is attempted to be published, a published interface is that one whose public methods are in hands of external users (client classes).

The Liskov Subsitution Principle defines these two important aspects:

- Subclasses should be substitutable for their base classes.
- Methods that use references to base classes (interfaces) must be able to use objects of derived classes (implementer classes) without knowing it.


Design by Contract:
Methods of classes declare pre-conditions and post-conditions. The pre-conditions must be true in order for the method to execute. Upon completion, the method guarantees that the post-condition will be true.

LSP makes clear that in OOD the IS A (inheritance) relationship pertains to behavior. Not intrinsic private behavior, but extrinsic public behavior that clients depend upon. The clients expect constant behavior for published interfaces! 

Expected behavior: Derived methods should expect no more and provide no less.

The following UML class diagram pictures LSP:

Published interfaces used by clients will expect always the same behavior from derived classes as they may not be aware of these new implementation details.

Code sample:
Despite the following code is in C# it can be easily ported to Java or other OOP language.

First, we define the class Circle with the public attribute Radius and the public method CalculateArea, this last implements the formula to obtain the area of a circle.

   1:  public class Circle
   2:  {
   3:      public double Radius { get; set; }
   4:   
   5:      public virtual double CalculateArea()
   6:      {
   7:          // Pi * r^2
   8:          return Math.PI * Math.Pow(this.Radius, 2);
   9:      }
  10:   
  11:      public override string ToString()
  12:      {
  13:          return string.Format("Circle Radius {0} and Area {1}",
  14:              this.Radius, Math.Round(this.CalculateArea(), 3));
  15:      }
  16:  }

Second, we define the CircleChecker class that contains the static method PrintMatchesInSameIndex, this method takes two arrays of Circle instances and checks by the Radius attribute if two circles at the same index position of the the arrays are equivalent. It makes totally sense to compare by the Radius attribute if two Circle instances have the same size.
   1:  public class CircleChecker
   2:  {
   3:   
   4:      public static void PrintMatchesInSameIndex(Circle[] series1,
   5:                                                 Circle[] series2)
   6:      {
   7:          int lastIndex = series1.Length < series2.Length ?
   8:              series2.Length : series1.Length;
   9:          for (int i = 0; i < lastIndex; i++)
  10:          {
  11:              if (series1[i].Radius == series2[i].Radius)
  12:              {
  13:                  Console.WriteLine(string.Format(
  14:                      "Index {0}: \n" + series1[i].ToString()
  15:                      + " = " + series2[i].ToString(), i));
  16:              }
  17:          }
  18:      }
  19:  }

Finally, the CircleConsole class initializes two arrays of the type Circle and sets the values of the Radius attribute for each Circle object in the arrays. This class calls the static method of PrintMatchesInSameIndex in the CircleChecker class. The matching values are in bold.

   1:  class CircleConsole
   2:  {
   3:      static void Main(string[] args)
   4:      {
   5:          const int length= 5;
   6:          int[] radiusSeries1 = new int[length] { 1, 2, 3, 4, 5 };
   7:          int[] radiusSeries2 = new int[length] { 1, 9, 3, 7, 5 };
   8:   
   9:          Circle[] series1 = new Circle[length];
  10:          Circle[] series2 = new Circle[length];
  11:   
  12:          for (int i = 0; i < length; i++)
  13:          {
  14:              series1[i] = new Circle();
  15:              series2[i] = new Circle();
  16:              series1[i].Radius = radiusSeries1[i];
  17:              series2[i].Radius = radiusSeries2[i];
  18:          }
  19:   
  20:          CircleChecker.PrintMatchesInSameIndex(series1, series2);
  21:          Console.ReadLine();
  22:      }
  23:  }

When executing the application, it is clear that the Circle objects at the indexes 0, 2 and 4 are equivalent:


So far so good, everything works as expected. To demonstrate the LSP we create the class Sphere that inherits from Circle. Our original thought may be that a Sphere is a specialization of a Circle, just made in 3D, right? Based on this premise, we override the CalculateArea method to calculate the surface of such Sphere and we extend the class by adding the CalculateVolume method.

   1:  public class Sphere : Circle
   2:  {
   3:      public override double CalculateArea()
   4:      {
   5:          // 4 * Pi * r^2
   6:          return 4 * Math.PI * Math.Pow(this.Radius, 2);
   7:      }
   8:   
   9:      public virtual double CalculateVolume()
  10:      {
  11:          // (4/3) * Pi * r^3
  12:          return (4 / 3) * Math.PI * Math.Pow(this.Radius, 3);
  13:      }
  14:  }

In theory, everything looks perfect! We have applied the Open-Closed principle as our designed was open for extension and we did not change other classes. However, we can see how the LSP is broken with the following implementation of the class SphereConsole, we instantiate Sphere objects for the series2 array of the type Circle.

   1:  class SphereConsole
   2:  {
   3:      static void Main(string[] args)
   4:      {
   5:          const int length = 5;
   6:          int[] radiusSeries1 = new int[length] { 1, 2, 3, 4, 5 };
   7:          int[] radiusSeries2 = new int[length] { 1, 9, 3, 7, 5 };
   8:   
   9:          Circle[] series1 = new Circle[length];
  10:          Circle[] series2 = new Circle[length];
  11:   
  12:          for (int i = 0; i < length; i++)
  13:          {
  14:              series1[i] = new Circle();
  15:              series2[i] = new Sphere();
  16:              series1[i].Radius = radiusSeries1[i];
  17:              series2[i].Radius = radiusSeries2[i];
  18:          }
  19:   
  20:          CircleChecker.PrintMatchesInSameIndex(series1, series2);
  21:          Console.ReadLine();
  22:      }
  23:  }

When executing the application, we get unexpected results as seen in the following picture:


We can see that we have Circle instances with the same Radius value but different Areas! The LSP is broken as the static method PrintMatchesInSameIndex only understands the behavior of Circle objects, thus such behavior is expected.

Our main problem was bad abstraction, a Sphere is NOT a Circle! The client class CircleChecker is expecting constant behavior from any object that IS A Circle.

Conclusions:
We have seen why the Liskov Substitution Principle targets Public Polymorphism as clients of published interfaces can be affected by wrong sub-classing of such interfaces. Class inheritance needs to be used carefully. For the same reason, OOP languages provide guidelines to prevent this, we can declare a class to be sealed in C# or final in Java. By sealing the class Circle, we could avoid the Sphere class problem.

I will cover the Interface Segregation Principle in my next post, see you then!

[RobertMartin96] The Liskov Substitution Principle, Robert Martin, 1996

2 comments:

  1. Your blog postings on S.O.L.I.D. is really "SOLID" so far. I look forward to the last two entries as well.

    I explain LSP with Rectangle and Square. We've all be taught from elementary school on that a square is a type of rectangle where the sides are the same length.

    It's very easy to derive Square from Rectangle. "Is-a" even appears in our earliest definitions.

    However, unlike simple geometry, we can often change the length of the sides of a Rectangle class, and this is where the Square runs into problems.

    We can still use the Rectangle to implement the Square. We just treat Rectangle as an attribute to Square and delegate all geometric calculations from Square to the private Rectangle. Here we can control the sides of the rectangle keeping them the same without any LSP concerns.

    ReplyDelete
  2. Thanks for your comment. Definitely, we can encapsulate a Rectangle instance in the Square class definition, and delegate the calculation to the Rectangle object, thus the IS_A relationship turns into a HAS_A. There is a famous OOD phrase which goes: "COMPOSITION OVER INHERITANCE."

    We just need to be careful when defining our class abstractions and sub-classing.

    ReplyDelete