1. 程式人生 > >Making the Most of Polymorphism with the Liskov Substitution Principle

Making the Most of Polymorphism with the Liskov Substitution Principle

The Liskov Substitution Principle

In object-oriented design, a common technique of creating objects with like behavior is the use of super- and sub-types. A supertype is defined with some set of characteristics that all of its subtypes then inherit. In turn, subtypes may then choose to override the supertype’s implementation of some behavior, thus allowing for behavior differentiation through polymorphism. This is an extremely powerful technique; however, it raises the question of what

exactly makes one object a subtype of another. Is it enough for a particular object to inherit from another? In 1987, Barbara Liskov proposed an answer to this question, arguing that an object should only be considered a subtype of another object if it is interchangeable with its parent object so far as any interacting function is concerned. Liskov and co-author Jeannette Wing further clarified this idea in their 1994 paper, A Behavioral Notation of Subtyping
[1], in which they set out a requirement for constraining the behavior of subtypes:

Subtype Requirement: Let ?(x) be a property provable about objects x of type T. Then ?(y) should be true for objects y of type S where S is a subtype of T.

This is perhaps too academic of a definition for our purposes, but it hints at something important: if a parent object has some necessarily provable attribute, then its subtypes must have the same provable attribute. In his development of the SOLID principles, Robert C. Martin took this definition a step further by trying to restate it in a way that was more meaningful for day-to-day software development [2]. In Martin’s definition, the LSP is stated as follows:

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

In other words, LSP-adherent design is about implicit contracts between derived classes and functions that use their parent classes. A derived class (or in Liskov’s terminology, a subtype) must behave in a manner that does not break a function that uses the derived class’ parent class. This idea of contracts in classes is closely related to Bertrand Meyer’s idea of Design by Contract, which roughly states that methods of classes should declare pre-conditions that must be true for a method to execute and post-conditions that are guaranteed to be true after the method executes [3]. In LSP terms, the validity of pre- and post-conditions is guaranteed by adherence to the following standards:

  • A derived object cannot expect users to obey a stronger pre-condition than its parent object expects.
  • A derived object may not guarantee a weaker post-condition than its parent object guarantees.
  • Derived objects must accept anything that a base class could accept and have behaviors/outputs that do not violate the constraints of the base class.

In studying principles such as the LSP it’s easy to lose sight of why they matter and what they are really saying. The complex language and apparent dogma of such principles have a habit of overshadowing real-world considerations. But once you strip out the academic / technical language, the LSP is really just saying that subtypes should not break the contracts set by their parent types. In practical terms, this means that if a given function uses some object, then you should be able to replace that object with one of its subtypes without anything breaking.

Once you strip out the academic / technical language, the Liskov Substitution Principle is really just saying that subtypes should not break the contracts set by their parent types

As for why this is a good practice, the answer is that failure to adhere to the LSP quickly raises problems as a codebase expands. Without LSP adherence, changes to a program are likely to have unexpected consequences and/or require opening a previously closed class. On the other hand, following the LSP allows easy extension of program behavior because subclasses can be inserting into working code without causing undesired outcomes.