Monday, February 13, 2017

Simplifying the Liskov Substitution Principle of SOLID in C#

The Liskov Substitution Principle says that the object of a derived class should be able to replace an object of the base class without bringing any errors in the system or modifying the behavior of the base class.

In short: if S is subset of T, an object of T could be replaced by object of S without impacting the program and bringing any error in the system. Let’s say you have a class Rectangle and another class Square. Square is as Rectangle, or in other words, it inherits the Rectangle class. So as the Liskov Substitution principle states, we should able to replace object of Rectangle by the object of Square without bringing any undesirable change or error in the system.

Let’s take a closer look at this principle with some examples.

 

Understanding the problem

Let us say we have two classes, Rectangle and Square. In this example, the Square class inherits the Rectangle class. Both classes are created as listed below:

 

public class Rectangle

  {

      public virtual int Height { getset; }

      public virtual int Width { getset; }

  }

 

The Square class inherits the Rectangle class and overrides the properties as shown in the listing below:

public class Square : Rectangle

  {

      private int _height;

      private int _width;

      public override int Height

      {

          get

          {

              return _height;

          }

          set

          {

              _height = value;

              _width = value;

          }

      }

      public override int Width

      {

          get

          {

              return _width;

          }

          set

          {

             _width = value;

             _height = value;

          }

      }

 

  }

 

We need to calculate area of the Rectangle and the Square.  For this purpose, let us create another class called AreaCalculator.

  public class AreaCalculator

        {

            public static int CalculateArea(Rectangle r)

            {

                return r.Height * r.Width;

            }

 

            public static int CalculateArea(Square s)

            {

                return s.Height * s.Height;

            }

        }

 

Let us go ahead and write Unit tests to calculate area of the Rectangle and the Square. A unit test to calculate these areas as shown in the listing below should pass.

 

        [TestMethod]

        public void Sixfor2x3Rectangle()

        {

            var myRectangle = new Rectangle { Height = 2, Width = 3 };

            var result = AreaCalculator.CalculateArea(myRectangle);

            Assert.AreEqual(6, result);

        }

 

 

On the other hand, a test to calculate area of the Square should also pass:

  [TestMethod]

        public void Ninefor3x3Squre()

        {

            var mySquare = new Square { Height = 3 };

            var result = AreaCalculator.CalculateArea(mySquare);

            Assert.AreEqual(9, result);

        }

 

In the both tests, we are creating:

1.       The Object of Rectangle to find the area of the Rectangle

2.       The Object of Square to find the area of the Square

And the tests pass as expected. Now let us go ahead and create a test in which we will try to substitute the object of Rectangle with the object of Square. We want to find area of Rectangle using the object of Square and for the unit test for this is written below:

 

  [TestMethod]

        public void TwentyFourfor4x6RectanglefromSquare()

        {

            Rectangle newRectangle = new Square();

            newRectangle.Height = 4;

            newRectangle.Width = 6;

            var result = AreaCalculator.CalculateArea(newRectangle);

            Assert.AreEqual(24, result);

        }

 

The above test would fail, because the expected result is 24, however the actual area calculated would be 36.

This is the problem. Even though the Square class is a subset of the Rectangle class, the Object of Rectangle class is not substitutable by object of the Square class without causing a problem in the system. If the system adhered to the Lisokov Substitution Principle, you may avoid the above problem.

 

Solve problem with No-Inheritance

We can solve the above problem by following the below steps:

1.       Get rid of the AreaCalculator class.

2.       Let each shape define its own Area method.

3.       Rather than Square class will inherit Rectangle class, let us create a common abstract base class Shape and both classes will inherit that.

 

A common base class Shape can be created as shown in listing below:

public  abstract class Shape

        {

 

        }

 

Next, the Rectangle class can be rewritten as follows:

 

  public class Rectangle :Shape

  {

      public  int Height { getset; }

      public  int Width { getset; }

      public int Area()

      {

          return Height * Width;

      }

  }

And the Square class can be rewritten as shown in the listing below:

 

public class Square : Shape

  {

      public int Sides;

      public int Area()

      {

          return Sides * Sides;

      }

 

  }

 

Now we can write a unit test for the area function in the Rectangle class as shown in listing below:

 

   [TestMethod]

        public void Sixfor2x3Rectangle()

        {

            var myRectangle = new Rectangle { Height = 2, Width = 3 };

            var result = myRectangle.Area();

            Assert.AreEqual(6, result);

        }

 

The above test should pass without any difficulty. In the same way we can unit test the Area function of the Square class as shown in the listing below:

 [TestMethod]

        public void Ninefor3x3Squre()

        {

            var mySquare = new Square { Sides = 3 };

            var result = mySquare.Area();

            Assert.AreEqual(9, result);

        }

 

Next let us go ahead and write the test in which we will substitute object of Shape with the objects of Rectangle and Square.

public void TwentyFourfor4x6Rectangleand9for3x3Square()

        {

            var shapes = new List<Shape>{

                new Rectangle{Height=4,Width=6},

                new Square{Sides=3}

            };

            var areas = new List<int>();

            foreach(Shape shape in shapes){

                if(shape.GetType()==typeof(Rectangle))

                {

                    areas.Add(((Rectangle)shape).Area());

                }

                if (shape.GetType() == typeof(Square))

                {

                    areas.Add(((Square)shape).Area());

                }

 

            }

            Assert.AreEqual(24, areas[0]);

            Assert.AreEqual(9, areas[1]);

        }

 

The above test will pass and we are successfully able to substitute the objects without impacting the system. However there is one problem in the above approach: we are violating the open closed principle. Each time a new class inherits the Shape class, we will have to add one more if the condition is in the test and we certainly do not want this.

The above problem can be solved by modifying the Shape class as shown in the listing below:

 

public  abstract class Shape

        {

        public abstract int Area();

        }

 

Here we have moved an abstract method Area in the Shape class and each sub class will give its own definition to the Area method. Rectangle and Square class can be modified as shown in the listing below:

public class Rectangle :Shape

  {

      public  int Height { getset; }

      public  int Width { getset; }

      public override int Area()

      {

          return Height * Width;

      }

  }

 

 

public class Square : Shape

  {

      public int Sides;

      public override int Area()

      {

          return Sides * Sides;

      }

 

  }

 

Here the above classes are following the Liskov Substitution principle, and we can rewrite the test without “if” conditions as shown in the listing below:

 [TestMethod]

        public void TwentyFourfor4x6Rectangleand9for3x3Square()

        {

            var shapes = new List<Shape>{

                new Rectangle{Height=4,Width=6},

                new Square{Sides=3}

            };

            var areas = new List<int>();

         

 

            foreach (Shape shape in shapes)

            {

                areas.Add(shape.Area());

            }

            Assert.AreEqual(24, areas[0]);

            Assert.AreEqual(9, areas[1]);

        }

 

In this way we can create relationship between the sub class and the base class by adhering to the Liskov Substitution principle. Common ways to identify violations of LS principles are as follows:

1.       Not implemented method in the sub class.

2.       Sub class function overrides the base class method to give it new meaning.

I hope you find this post useful – thanks for reading and happy coding!

 

No comments:

Post a Comment