4

From Agile Principles, Patterns, and Practices in C# by Robert Martin,

Listing 10-1. A violation of LSP causing a violation of OCP

struct    Point   {double x,  y;}
public    enum    ShapeType   {square,    circle};
public    class   Shape
{
      private ShapeType   type;
      public  Shape(ShapeType t){type =   t;}
      public  static  void    DrawShape(Shape s)
      {
              if(s.type   ==  ShapeType.square)
                      (s  as  Square).Draw();
              else    if(s.type   ==  ShapeType.circle)
                      (s  as  Circle).Draw();
      }
}
public    class   Circle  :   Shape
{
      private Point   center;
      private double  radius;
      public  Circle()    :   base(ShapeType.circle)  {}
      public  void    Draw()  {/* draws   the circle  */}
}
public    class   Square  :   Shape
{
      private Point   topLeft;
      private double  side;
      public  Square()    :   base(ShapeType.square)  {}
      public  void    Draw()  {/* draws   the square  */}
}

DrawShape() violates OCP. It must know about every possible derivative of the Shape class, and it must be changed whenever new derivatives of Shape are created.

The fact that Square and Circle cannot be substituted for Shape is a violation of LSP. This violation forced the violation of OCP by DrawShape . Thus, a violation of LSP is a latent violation of OCP.

How does it violate LSP? (In particular, why can Square and Circle not be substituted for Shape?)

How does violation of LSP cause violation of OCP? (I can see it directly violates OCP, but I can't understand how violation of LSP causes violation of OCP.)

halfer
  • 19,824
  • 17
  • 99
  • 186
Tim
  • 1
  • 141
  • 372
  • 590

1 Answers1

4

This is not an obvious or typical LSP violation, and one could argue that it isn't an LSP violation at all, but here's my interpretation:

The expectation is that a Shape is described by its type field. When DrawShape receives a Shape object, one of a few things can happen. Depending on the value of the type field, it could try to cast the object to a Square and call its Draw function, or try to cast it to a Circle to the same end. However, this is not guaranteed to work as expected for an arbitrary Shape. Specifically, it will only work if the object's dynamic type actually matches the semantic implication of it's type field. If the type field does not match its dynamic type, an exception will occur when trying to perform the dynamic cast. This is the behavior of DrawShape given a Shape object.

However, given a Square or Circle, there is a different expectation. Specifically, the function is expected to always execute one path or another without an exception, since the semantic implication of the type field will always match the object's dynamic type.

In other words, you could consider the DrawShape function to have four interesting paths of execution for a Shape object: an exception occuring at the dynamic Circle cast, an exception occuring at the dynamic Square cast, or a successful execution of either the Square or Circle draw functions.

When a child is substituted, the first two paths mentioned are no longer possible, and for a given child, only one path is possible.

Alternatively, it could be argued that there is no LSP violation; the function still "acts" in the same way for a child substitution as it would for a parent. Squares and Circles simply have an extra implication, being that the type field will definitely match the dynamic type of the object, restricting the execution results of the function at runtime. While it could be thought of as changing the function expectations, it could also simply be thought of as imposing a pre-condition.

Edit

I suppose I forgot to answer part of the question: The reason this supposed LSP violation "causes" an OCP violation is because the function logic which causes the behavior to be different for Shapes versus Squares and Circles, being the dynamic casting to the children, is the very same logic which forces the Shape class to depend on its children. Thus, by violating the LSP using conditional logic about the subclasses, it in turn violates the OCP.

I don't know if I myself would really call this particular case a situation of "causation" so much as simple event intersection, but perhaps this was the author's intention.

Alexander Guyer
  • 2,063
  • 1
  • 14
  • 20
  • When you say "If the type field does not match its dynamic type", why would that happen? – suspicious Nov 06 '21 at 17:11
  • To be honest, I'm not sure what I meant. In the given example, that would *never* happen given a well-defined program. Well, if a *new* derived class was introduced with `type == ShapeType.square`, that would break the `Shape` class. But this has nothing to do with `Square` or `Circle` and whether or not they can be substituted for `Shape`. So in my opinion, there is a clear OCP violation and a strong potential for an LSP violation by introducing a new derived class with a bad `type` value, but I disagree that there's *actually* any LSP violation in the given example as it stands. – Alexander Guyer Dec 07 '21 at 21:11