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:38

LSP is necessary where some code thinks it is calling the methods of a type T, and may unknowingly call the methods of a type S, where S extends T (i.e. S inherits, derives from, or is a subtype of, the supertype T).

For example, this occurs where a function with an input parameter of type T, is called (i.e. invoked) with an argument value of type S. Or, where an identifier of type T, is assigned a value of type S.

val id : T = new S() // id thinks it's a T, but is a S

LSP requires the expectations (i.e. invariants) for methods of type T (e.g. Rectangle), not be violated when the methods of type S (e.g. Square) are called instead.

val rect : Rectangle = new Square(5) // thinks it's a Rectangle, but is a Square
val rect2 : Rectangle = rect.setWidth(10) // height is 10, LSP violation

Even a type with immutable fields still has invariants, e.g. the immutable Rectangle setters expect dimensions to be independently modified, but the immutable Square setters violate this expectation.

class Rectangle( val width : Int, val height : Int )
{
   def setWidth( w : Int ) = new Rectangle(w, height)
   def setHeight( h : Int ) = new Rectangle(width, h)
}

class Square( val side : Int ) extends Rectangle(side, side)
{
   override def setWidth( s : Int ) = new Square(s)
   override def setHeight( s : Int ) = new Square(s)
}

LSP requires that each method of the subtype S must have contravariant input parameter(s) and a covariant output.

Contravariant means the variance is contrary to the direction of the inheritance, i.e. the type Si, of each input parameter of each method of the subtype S, must be the same or a supertype of the type Ti of the corresponding input parameter of the corresponding method of the supertype T.

Covariance means the variance is in the same direction of the inheritance, i.e. the type So, of the output of each method of the subtype S, must be the same or a subtype of the type To of the corresponding output of the corresponding method of the supertype T.

This is because if the caller thinks it has a type T, thinks it is calling a method of T, then it supplies argument(s) of type Ti and assigns the output to the type To. When it is actually calling the corresponding method of S, then each Ti input argument is assigned to a Si input parameter, and the So output is assigned to the type To. Thus if Si were not contravariant w.r.t. to Ti, then a subtype Xi—which would not be a subtype of Si—could be assigned to Ti.

Additionally, for languages (e.g. Scala or Ceylon) which have definition-site variance annotations on type polymorphism parameters (i.e. generics), the co- or contra- direction of the variance annotation for each type parameter of the type T must be opposite or same direction respectively to every input parameter or output (of every method of T) that has the type of the type parameter.

Additionally, for each input parameter or output that has a function type, the variance direction required is reversed. This rule is applied recursively.


Subtyping is appropriate where the invariants can be enumerated.

There is much ongoing research on how to model invariants, so that they are enforced by the compiler.

Typestate (see page 3) declares and enforces state invariants orthogonal to type. Alternatively, invariants can be enforced by converting assertions to types. For example, to assert that a file is open before closing it, then File.open() could return an OpenFile type, which contains a close() method that is not available in File. A tic-tac-toe API can be another example of employing typing to enforce invariants at compile-time. The type system may even be Turing-complete, e.g. Scala. Dependently-typed languages and theorem provers formalize the models of higher-order typing.

Because of the need for semantics to abstract over extension, I expect that employing typing to model invariants, i.e. unified higher-order denotational semantics, is superior to the Typestate. ‘Extension’ means the unbounded, permuted composition of uncoordinated, modular development. Because it seems to me to be the antithesis of unification and thus degrees-of-freedom, to have two mutually-dependent models (e.g. types and Typestate) for expressing the shared semantics, which can't be unified with each other for extensible composition. For example, Expression Problem-like extension was unified in the subtyping, function overloading, and parametric typing domains.

My theoretical position is that for knowledge to exist (see section “Centralization is blind and unfit”), there will never be a general model that can enforce 100% coverage of all possible invariants in a Turing-complete computer language. For knowledge to exist, unexpected possibilities much exist, i.e. disorder and entropy must always be increasing. This is the entropic force. To prove all possible computations of a potential extension, is to compute a priori all possible extension.

This is why the Halting Theorem exists, i.e. it is undecidable whether every possible program in a Turing-complete programming language terminates. It can be proven that some specific program terminates (one which all possibilities have been defined and computed). But it is impossible to prove that all possible extension of that program terminates, unless the possibilities for extension of that program is not Turing complete (e.g. via dependent-typing). Since the fundamental requirement for Turing-completeness is unbounded recursion, it is intuitive to understand how Gödel's incompleteness theorems and Russell's paradox apply to extension.

An interpretation of these theorems incorporates them in a generalized conceptual understanding of the entropic force:

  • Gödel's incompleteness theorems: any formal theory, in which all arithmetic truths can be proved, is inconsistent.
  • Russell's paradox: every membership rule for a set that can contain a set, either enumerates the specific type of each member or contains itself. Thus sets either cannot be extended or they are unbounded recursion. For example, the set of everything that is not a teapot, includes itself, which includes itself, which includes itself, etc…. Thus a rule is inconsistent if it (may contain a set and) does not enumerate the specific types (i.e. allows all unspecified types) and does not allow unbounded extension. This is the set of sets that are not members of themselves. This inability to be both consistent and completely enumerated over all possible extension, is Gödel's incompleteness theorems.
  • Liskov Substition Principle: generally it is an undecidable problem whether any set is the subset of another, i.e. inheritance is generally undecidable.
  • Linsky Referencing: it is undecidable what the computation of something is, when it is described or perceived, i.e. perception (reality) has no absolute point of reference.
  • Coase's theorem: there is no external reference point, thus any barrier to unbounded external possibilities will fail.
  • Second law of thermodynamics: the entire universe (a closed system, i.e. everything) trends to maximum disorder, i.e. maximum independent possibilities.
查看更多
明月照影归
3楼-- · 2018-12-31 01:39

A square is a rectangle where the width equals the height. If the square sets two different sizes for the width and height it violates the square invariant. This is worked around by introducing side effects. But if the rectangle had a setSize(height, width) with precondition 0 < height and 0 < width. The derived subtype method requires height == width; a stronger precondition (and that violates lsp). This shows that though square is a rectangle it is not a valid subtype because the precondition is strengthened. The work around (in general a bad thing) cause a side effect and this weakens the post condition (which violates lsp). setWidth on the base has post condition 0 < width. The derived weakens it with height == width.

Therefore a resizable square is not a resizable rectangle.

查看更多
梦寄多情
4楼-- · 2018-12-31 01:40

LSP concerns invariants.

The classic example is given by the following pseudo-code declaration (implementations omitted):

class Rectangle {
    int getHeight()
    void setHeight(int value)
    int getWidth()
    void setWidth(int value)
}

class Square : Rectangle { }

Now we have a problem although the interface matches. The reason is that we have violated invariants stemming from the mathematical definition of squares and rectangles. The way getters and setters work, a Rectangle should satisfy the following invariant:

void invariant(Rectangle r) {
    r.setHeight(200)
    r.setWidth(100)
    assert(r.getHeight() == 200 and r.getWidth() == 100)
}

However, this invariant must be violated by a correct implementation of Square, therefore it is not a valid substitute of Rectangle.

查看更多
几人难应
5楼-- · 2018-12-31 01:40

Long story short, let's leave rectangles rectangles and squares squares, practical example when extending a parent class, you have to either PRESERVE the exact parent API or to EXTEND IT.

Let's say you have a base ItemsRepository.

class ItemsRepository
{
    /**
    * @return int Returns number of deleted rows
    */
    public function delete()
    {
        // perform a delete query
        $numberOfDeletedRows = 10;

        return $numberOfDeletedRows;
    }
}

And a sub class extending it:

class BadlyExtendedItemsRepository extends ItemsRepository
{
    /**
     * @return void Was suppose to return an INT like parent, but did not, breaks LSP
     */
    public function delete()
    {
        // perform a delete query
        $numberOfDeletedRows = 10;

        // we broke the behaviour of the parent class
        return;
    }
}

Then you could have a Client working with the Base ItemsRepository API and relying on it.

/**
 * Class ItemsService is a client for public ItemsRepository "API" (the public delete method).
 *
 * Technically, I am able to pass into a constructor a sub-class of the ItemsRepository
 * but if the sub-class won't abide the base class API, the client will get broken.
 */
class ItemsService
{
    /**
     * @var ItemsRepository
     */
    private $itemsRepository;

    /**
     * @param ItemsRepository $itemsRepository
     */
    public function __construct(ItemsRepository $itemsRepository)
    {
        $this->itemsRepository = $itemsRepository;
    }

    /**
     * !!! Notice how this is suppose to return an int. My clients expect it based on the
     * ItemsRepository API in the constructor !!!
     *
     * @return int
     */
    public function delete()
    {
        return $this->itemsRepository->delete();
    }
} 

The LSP is broken when substituting parent class with a sub class breaks the API's contract.

class ItemsController
{
    /**
     * Valid delete action when using the base class.
     */
    public function validDeleteAction()
    {
        $itemsService = new ItemsService(new ItemsRepository());
        $numberOfDeletedItems = $itemsService->delete();

        // $numberOfDeletedItems is an INT :)
    }

    /**
     * Invalid delete action when using a subclass.
     */
    public function brokenDeleteAction()
    {
        $itemsService = new ItemsService(new BadlyExtendedItemsRepository());
        $numberOfDeletedItems = $itemsService->delete();

        // $numberOfDeletedItems is a NULL :(
    }
}

You can learn more about writing maintainable software in my course: https://www.udemy.com/enterprise-php/

查看更多
冷夜・残月
6楼-- · 2018-12-31 01:40

Here is an excerpt from this post that clarifies things nicely:

[..] in order to comprehend some principles, it’s important to realize when it’s been violated. This is what I will do now.

What does the violation of this principle mean? It implies that an object doesn’t fulfill the contract imposed by an abstraction expressed with an interface. In other words, it means that you identified your abstractions wrong.

Consider the following example:

interface Account
{
    /**
     * Withdraw $money amount from this account.
     *
     * @param Money $money
     * @return mixed
     */
    public function withdraw(Money $money);
}
class DefaultAccount implements Account
{
    private $balance;
    public function withdraw(Money $money)
    {
        if (!$this->enoughMoney($money)) {
            return;
        }
        $this->balance->subtract($money);
    }
}

Is this a violation of LSP? Yes. This is because the account’s contract tells us that an account would be withdrawn, but this is not always the case. So, what should I do in order to fix it? I just modify the contract:

interface Account
{
    /**
     * Withdraw $money amount from this account if its balance is enough.
     * Otherwise do nothing.
     *
     * @param Money $money
     * @return mixed
     */
    public function withdraw(Money $money);
}

Voilà, now the contract is satisfied.

This subtle violation often imposes a client with the ability to tell the difference between concrete objects employed. For example, given the first Account’s contract, it could look like the following:

class Client
{
    public function go(Account $account, Money $money)
    {
        if ($account instanceof DefaultAccount && !$account->hasEnoughMoney($money)) {
            return;
        }
        $account->withdraw($money);
    }
}

And, this automatically violates the open-closed principle [that is, for money withdrawal requirement. Because you never know what happens if an object violating the contract doesn't have enough money. Probably it just returns nothing, probably an exception will be thrown. So you have to check if it hasEnoughMoney() -- which is not part of an interface. So this forced concrete-class-dependent check is an OCP violation].

This point also addresses a misconception that I encounter quite often about LSP violation. It says the “if a parent’s behavior changed in a child, then, it violates LSP.” However, it doesn’t — as long as a child doesn’t violate its parent’s contract.

查看更多
流年柔荑漫光年
7楼-- · 2018-12-31 01:41

Strangely, no one has posted the original paper that described lsp. It is not an easy read as Robert Martin's one, but worth it.

查看更多
登录 后发表回答