What is an example of the Liskov Substitution Prin

2018-12-31 00:48发布

I have heard that the Liskov Substitution Principle (LSP) is a fundamental principle of object oriented design. What is it and what are some examples of its use?

26条回答
看淡一切
2楼-- · 2018-12-31 01:28

The Liskov Substitution Principle (LSP, ) is a concept in Object Oriented Programming that states:

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

At its heart LSP is about interfaces and contracts as well as how to decide when to extend a class vs. use another strategy such as composition to achieve your goal.

The most effective way I have seen to illustrate this point was in Head First OOA&D. They present a scenario where you are a developer on a project to build a framework for strategy games.

They present a class that represents a board that looks like this:

Class Diagram

All of the methods take X and Y coordinates as parameters to locate the tile position in the two-dimensional array of Tiles. This will allow a game developer to manage units in the board during the course of the game.

The book goes on to change the requirements to say that the game frame work must also support 3D game boards to accommodate games that have flight. So a ThreeDBoard class is introduced that extends Board.

At first glance this seems like a good decision. Board provides both the Height and Width properties and ThreeDBoard provides the Z axis.

Where it breaks down is when you look at all the other members inherited from Board. The methods for AddUnit, GetTile, GetUnits and so on, all take both X and Y parameters in the Board class but the ThreeDBoard needs a Z parameter as well.

So you must implement those methods again with a Z parameter. The Z parameter has no context to the Board class and the inherited methods from the Board class lose their meaning. A unit of code attempting to use the ThreeDBoard class as its base class Board would be very out of luck.

Maybe we should find another approach. Instead of extending Board, ThreeDBoard should be composed of Board objects. One Board object per unit of the Z axis.

This allows us to use good object oriented principles like encapsulation and reuse and doesn’t violate LSP.

查看更多
呛了眼睛熬了心
3楼-- · 2018-12-31 01:28

There is a check list to determine whether or not you are violating Liskov.

  • If you violate one of the following items -> you violate Liskov.
  • If you dont violate any -> cant conclude anything.

Check list:

  • No new exceptions should be thrown in derived class: If your base class threw ArgumentNullException then your sub classes were only allowed to throw exceptions of type ArgumentNullException or any exceptions derived from ArgumentNullException. Throwing IndexOutOfRangeException is a violation of Liskov.
  • Pre-conditions cannot be strengthened: Assume your base class works with a member int. Now your sub-type requires that int to be positive. This is strengthened pre-conditions, and now any code that worked perfectly fine before with negative ints is broken.
  • Post-conditions cannot be weakened: Assume your base class required all connections to database should be closed before the method returned. In your sub-class you overrode that method and leaved connection open for further reuse. You have weakened the post-conditions of that method.
  • Invariants must be preserved: The most difficult and painful constraint to fulfill. Invariants are some time hidden in the base class and the only way to reveal them is to read the code of the base class. Basically you have to be sure when you override a method anything unchangeable must remain unchanged after your overridden method executed. The best thing I can think of is to enforce this invariant constraints in the base class but that would not be easy.
  • History Constraint: When overriding a method you are not allowed to modify an un-modifiable property in the base class. Take a look at these code and you can see Name is defined to be un-modifiable (private set) but SubType introduces new method that allows modifying it (through reflection):

    public class SuperType
    {
        public string Name { get; private set; }
        public SuperType(string name, int age)
        {
            Name = name;
            Age = age;
        }
    }
    public class SubType : SuperType
    {
        public void ChangeName(string newName)
        {
            var propertyType = base.GetType().GetProperty("Name").SetValue(this, newName);
        }
    }
    

There are 2 others items: Contravariance of method arguments and Covariance of return types. But it is not possible in C# (I'm a C# developer) so I dont care about them.

Reference:

查看更多
牵手、夕阳
4楼-- · 2018-12-31 01:28

In a very simple sentence, we can say:

The child class must not violate its base class characteristics. It must be capable with it. We can say it's same as subtyping.

查看更多
素衣白纱
5楼-- · 2018-12-31 01:28

I encourage you to read the article: Violating Liskov Substitution Principle (LSP).

You can find there an explanation what is the Liskov Substitution Principle, general clues helping you to guess if you have already violated it and an example of approach that will help you to make your class hierarchy be more safe.

查看更多
裙下三千臣
6楼-- · 2018-12-31 01:30

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

When I first read about LSP, I assumed that this was meant in a very strict sense, essentially equating it to interface implementation and type-safe casting. Which would mean that LSP is either ensured or not by the language itself. For example, in this strict sense, ThreeDBoard is certainly substitutable for Board, as far as the compiler is concerned.

After reading up more on the concept though I found that LSP is generally interpreted more broadly than that.

In short, what it means for client code to "know" that the object behind the pointer is of a derived type rather than the pointer type is not restricted to type-safety. Adherence to LSP is also testable through probing the objects actual behavior. That is, examining the impact of an object's state and method arguments on the results of the method calls, or the types of exceptions thrown from the object.

Going back to the example again, in theory the Board methods can be made to work just fine on ThreeDBoard. In practice however, it will be very difficult to prevent differences in behavior that client may not handle properly, without hobbling the functionality that ThreeDBoard is intended to add.

With this knowledge in hand, evaluating LSP adherence can be a great tool in determining when composition is the more appropriate mechanism for extending existing functionality, rather than inheritance.

查看更多
深知你不懂我心
7楼-- · 2018-12-31 01:31

Robert Martin has an excellent paper on the Liskov Substitution Principle. It discusses subtle and not-so-subtle ways in which the principle may be violated.

Some relevant parts of the paper (note that the second example is heavily condensed):

A Simple Example of a Violation of LSP

One of the most glaring violations of this principle is the use of C++ Run-Time Type Information (RTTI) to select a function based upon the type of an object. i.e.:

void DrawShape(const Shape& s)
{
  if (typeid(s) == typeid(Square))
    DrawSquare(static_cast<Square&>(s)); 
  else if (typeid(s) == typeid(Circle))
    DrawCircle(static_cast<Circle&>(s));
}

Clearly the DrawShape function is badly formed. It must know about every possible derivative of the Shape class, and it must be changed whenever new derivatives of Shape are created. Indeed, many view the structure of this function as anathema to Object Oriented Design.

Square and Rectangle, a More Subtle Violation.

However, there are other, far more subtle, ways of violating the LSP. Consider an application which uses the Rectangle class as described below:

class Rectangle
{
  public:
    void SetWidth(double w) {itsWidth=w;}
    void SetHeight(double h) {itsHeight=w;}
    double GetHeight() const {return itsHeight;}
    double GetWidth() const {return itsWidth;}
  private:
    double itsWidth;
    double itsHeight;
};

[...] Imagine that one day the users demand the ability to manipulate squares in addition to rectangles. [...]

Clearly, a square is a rectangle for all normal intents and purposes. Since the ISA relationship holds, it is logical to model the Square class as being derived from Rectangle. [...]

Square will inherit the SetWidth and SetHeight functions. These functions are utterly inappropriate for a Square, since the width and height of a square are identical. This should be a significant clue that there is a problem with the design. However, there is a way to sidestep the problem. We could override SetWidth and SetHeight [...]

But consider the following function:

void f(Rectangle& r)
{
  r.SetWidth(32); // calls Rectangle::SetWidth
}

If we pass a reference to a Square object into this function, the Square object will be corrupted because the height won’t be changed. This is a clear violation of LSP. The function does not work for derivatives of its arguments.

[...]

查看更多
登录 后发表回答