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?
相关问题
- how to define constructor for Python's new Nam
- Keeping track of variable instances
- Object.create() bug?
- std::vector of objects / pointers / smart pointers
- Name for a method that has only side effects
相关文章
- 接口B继承接口A,但是又不添加新的方法。这样有什么意义吗?
- NameError: name 'self' is not defined, eve
- Implementation Strategies for Object Orientation
- Check if the Type of an Object is inherited from a
- When to use Interfaces in PHP
- Are default parameters bad practice in OOP?
- How to return new instance of subclass while initi
- In OOP, what is the best practice in regards to us
LSP is necessary where some code thinks it is calling the methods of a type
T
, and may unknowingly call the methods of a typeS
, whereS extends T
(i.e.S
inherits, derives from, or is a subtype of, the supertypeT
).For example, this occurs where a function with an input parameter of type
T
, is called (i.e. invoked) with an argument value of typeS
. Or, where an identifier of typeT
, is assigned a value of typeS
.LSP requires the expectations (i.e. invariants) for methods of type
T
(e.g.Rectangle
), not be violated when the methods of typeS
(e.g.Square
) are called instead.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.
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 subtypeS
, must be the same or a supertype of the typeTi
of the corresponding input parameter of the corresponding method of the supertypeT
.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 subtypeS
, must be the same or a subtype of the typeTo
of the corresponding output of the corresponding method of the supertypeT
.This is because if the caller thinks it has a type
T
, thinks it is calling a method ofT
, then it supplies argument(s) of typeTi
and assigns the output to the typeTo
. When it is actually calling the corresponding method ofS
, then eachTi
input argument is assigned to aSi
input parameter, and theSo
output is assigned to the typeTo
. Thus ifSi
were not contravariant w.r.t. toTi
, then a subtypeXi
—which would not be a subtype ofSi
—could be assigned toTi
.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 ofT
) 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:
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.
LSP concerns invariants.
The classic example is given by the following pseudo-code declaration (implementations omitted):
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:However, this invariant must be violated by a correct implementation of
Square
, therefore it is not a valid substitute ofRectangle
.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.
And a sub class extending it:
Then you could have a Client working with the Base ItemsRepository API and relying on it.
The LSP is broken when substituting parent class with a sub class breaks the API's contract.
You can learn more about writing maintainable software in my course: https://www.udemy.com/enterprise-php/
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:
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:
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:
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.
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.