1. What is Richter’s substitution principle
The Richter Substitution principle was developed by Ms. Liskov of the MIT Computer Science Laboratory in an article called “Data Abstraction and Hierarchy” that was published at the 1987 OOPSLA conference. It outlined principles of inheritance, That is, when inheritance should be used, when inheritance should not be used, and what it implies. In 2002, Robert C. Martin, the Software engineering guru mentioned earlier in the single Responsibility Principle, published Agile Software Development Principles Patterns and Practices, In this article, he ultimately simplified the Reeves substitution principle into a sentence: “Subtypes must be substitutable for their base types”. That is, subclasses must be able to be replaced by their base class. Let’s explain the Richter substitution principle more fully: in a software system, subclasses should be able to replace any base class that can occur, and the code will still work after the substitution.
2. First example: A square is not a rectangle
“A square is not a rectangle” is a classic example of understanding Richter’s substitution principle. In mathematics, a square is unquestionably a rectangle; it is a rectangle of equal length and width. So, in a software system we developed for geometry, it was easy to make a square inherit from a rectangle. Now, let’s take a code snippet of the system for analysis:
// Rectangle: class Rectangle {double length; double width; public doublegetLength() { return length; }
public void setLength(double height) { this.length = length; }
public double getWidth() { return width; }
public void setWidth(double width) { this.width = width; } // Rectangle extends Rectangle {public voidsetWidth(double width) {
super.setLength(width);
super.setWidth(width);
}
public void setLength(double length) { super.setLength(length); super.setWidth(length); }}Copy the code
Since the degree and width of a square must be equal, the length and width are assigned the same value in the setLength and setWidth methods. The Rectangle class TestRectangle is a component of our software system. It has a Rectangle resize method that uses the base Rectangle class to simulate an incrementalized Rectangle width:
Rectangle: class Rectangle {public void resize(Rectangle objRect) {while(objRect.getWidth() <= objRect.getLength()) { objRect.setWidth( objRect.getWidth () + 1 ); }}Copy the code
If we pass a normal rectangle as a parameter to the resize method, we will see a gradual increase in the width of the rectangle. When the width is greater than the length, the code will stop. If we pass another square as an argument to the resize method, we see that the square’s width and length grow, and the code keeps running until the system generates an overflow error. So, normal rectangles are appropriate for this code, squares are not.
We conclude that a Rectangle parameter cannot be replaced by a Square parameter in the resize method. Thus, the inheritance relationship between the Square class and the Rectangle class violates the Richter substitution principle. The inheritance relationship between the Square class and the Rectangle class is not true.
3. Second example: Ostriches are not birds
“The ostrich is not a bird” is also a classic example of understanding Richter’s substitution principle. Another version of “ostrich not bird” is “penguin not bird”, which is essentially the same, assuming that the bird cannot fly. The biological definition of a bird is: “Endothermy, oviparous, fully feathered, linear, horny beak, eyes on either side of head. Forelimbs degenerated into wings, hind limbs with scaly skin and four toes “. So, from a biological point of view, the ostrich must be a bird.
We designed a system with birds, and ostriches are naturally derived from birds, and all the characteristics and behaviors of birds are inherited from ostriches. Most birds are known to be able to fly, so we have designed a method named fly for birds and given some properties related to flight, such as velocity.
// Bird: class Bird {double velocity; publicfly() { //I am flying; };
public setVelocity(double velocity) { this.velocity = velocity; };
public getVelocity() { return this.velocity; };
}
Copy the code
// What if ostriches can't fly? // Let's just show it by flapping its wings, doing nothing in the fly method. // As for its flight speed, if it can't fly, it has to be set to 0, so we have ostrich design. Class extends Bird {public} extends Bird {publicfly() { //I do nothing; };
public setVelocity(double velocity) { this.velocity = 0; };
public getVelocity() { return 0; };
}
Copy the code
Ok, all the classes are designed, and we provide the Bird class for other code (consumers) to use. Consumers now use the Bird class to fulfill a requirement to calculate how long it takes a Bird to fly across the Yellow River. The Bird class has two methods, fly and getVelocity. The Bird class has two methods, fly and getVelocity. The Bird class has two methods, fly and getVelocity.
TestBird: class TestBird {public calcFlyTime(Bird Bird) {try{double riverWidth = 3000; System.out.println(riverWidth / bird.getVelocity()); }catch(Exception err){ System.out.println("An error occured!"); }}}Copy the code
If we take a bird to test this code, there is no problem, the result is correct, in line with our expectation, the system outputs the time needed for the bird to fly over the Yellow River; If we take the ostrich to test this code, the result of the code occurred system division by zero exception, obviously not in line with our expectations.
For TestBird class, it is only a consumer of Bird class. When it uses Bird class, it only needs to use the method provided by Bird class. It does not care about whether ostrich can fly or not, and it does not need to know. It is in accordance with the “required time = the width of the Yellow River/bird flight speed” rule to calculate the bird across the Yellow River needed time.
Conclusion: In the calcFlyTime method, parameters of type Bird cannot be replaced by parameters of type Ostrich. Therefore, the inheritance relationship between Ostrich and Bird violates the Richter’s substitution principle, the inheritance relationship between them is not valid, Ostrich is not a Bird.
4. Is an ostrich a bird?
The conclusion that an ostrich is or is not a bird seems to be a paradox. There are two reasons for this confusion:
Cause 1: The definition of class inheritance is not clear.
Object-oriented design is concerned with the behavior of objects, it is the use of “behavior” to classify objects, only the behavior of the same object can be abstracted out of a class. I often say that class inheritance Is an “IS-A” relationship, which Is actually A behavioral “IS-A” relationship that can be described As “act-As.”
Again, the square is not a rectangle. A square is clearly different from a rectangle in its length and width. Behavior of a rectangle: When you set the length of a rectangle, its width stays the same. When you set the width, its length stays the same. Square behavior: When you set the length of a square, the width changes accordingly. When you set the width, the length changes.
So, if we add this behavior to the base rectangle, the square will not inherit this behavior. When we “force” the square to inherit from the rectangle, we fail to achieve the desired result.
“Ostriches are not birds” is basically the same way. When we talk about birds, we think they can fly. Some birds can fly, but not all birds can fly. That’s the problem. If the behavior of “flying” as a measure of “bird”, ostriches are clearly not birds; If according to the biological criteria: wings, feathers and other characteristics as a measure of the “bird” criteria, ostriches are naturally a bird. The ostrich has no “fly” behavior, we imposed this behavior on it, so when faced with the “fly over the Yellow River” requirement, the code will have a runtime failure.
Reason 2: Design depends on user requirements and the specific environment.
Inheritance requires that subclasses have all the behavior of the base class. The behavior here refers to the behavior that falls within the scope of the demand. A needs to expect birds to provide behaviors related to flying. Even though ostriches are 100% similar to ordinary birds in appearance, within the scope of A’s needs, ostriches are not consistent with other ordinary birds in flying. They do not have this ability, so ostriches cannot be derived from birds, because ostriches are not birds. Birds are expected to provide feather-related behavior, so ostriches are in line with other common birds in this regard. Although it can’t fly, but this is not in the range of B requirements, so it has all the behavioral characteristics of birds, ostriches can be derived from birds, ostriches are birds.
The behavior of all derived classes must be consistent with what the user expects of their base class. If a derived class fails to do so, it must violate the Richter substitution principle. Incorrect derivation can be very harmful in real development. As the scale of software development grows, so does the number of developers involved, each using and contributing components to others. Eventually, all the components developed by everyone are packaged and combined into a complete system. When each developer uses someone else’s component, he only needs to know the external exposed interface of the component, which is the collection of all its behavior. As for how to realize the internal, he cannot know and does not need to know. Therefore, for the consumer, it can only fulfill its expectations through the interface, and if the behavior provided by the component interface does not match the expectations of the consumer, an error occurs. Richter’s substitution principle is to avoid inconsistency between derived classes and base classes at design time.
5. How to correctly apply Richter’s substitution principle
The purpose of Richter’s substitution principle is to ensure the correctness of inheritance. Do we have to think so hard about every inheritance in a real project? No, most of the time it Is fine to design inheritance relationships according to “IS-A”. Only in rare cases, you need to deal with it carefully. This kind of situation Is generally recognized by people with some development experience, and there are rules to follow. Typically, the user’s code must include code that performs actions based on the subclass type:
// Animal: public class Animal{String name; public Animal(String name) { this.name = name; } public voidprintName(){
try{
System.out.println("I am a " + name + "!");
}catch(Exception err){
System.out.println("An error occured!"); }}} public extends Animal{public extends Animal (String name){super(name); } public voidMew(){
try{
System.out.println("Mew~~~ ");
}catch(Exception err){
System.out.println("An error occured!"); }}} public extends Animal {public Dog(String name) {super(name); } public voidBark(){
try{
System.out.println("Bark~~~ ");
}catch(Exception err){
System.out.println("An error occured!"); TestAnimal public class TestAnimal {public void TestLSP(Animal Animal){if (animal instanceof Cat ){
Cat cat = (Cat)animal;
cat.printName();
cat.Mew();
}
if(animal instanceof Dog ){ Dog dog = (Dog)animal; dog.printName(); dog.Bark(); }}}Copy the code
Such code is obviously not in line with the Richter substitution principle, it causes great trouble for users to use, even can not use, for the future maintenance and expansion bring huge hidden trouble. The key step to realize the open close principle is abstraction, and the inheritance relationship between base class and subclass is an embodiment of abstraction. Therefore, The Richter substitution principle is a specification for abstraction. Violating The Richter’s substitution principle means violating the open close principle, not the other way around. Richter’s substitution principle is an important guarantee to make code comply with the open – close principle.
We see code like this all the time, at least in my previous Java and PHP projects. For example, there is a web page, to realize the function of viewing, adding, modifying and deleting customer information, the general Server side corresponding processing class has such a section:
if(action) Equals (" add ")) {/ /do add action
}
else if(action) Equals (" view ")) {/ /do view action
}
else if(action) Equals (" delete ")) {/ /do delete action
}
else if(action) Equals (" modify ")) {/ /do modify action
}
Copy the code
As you are all familiar with, this violates the Richter’s substitution principle, resulting in poor maintainability and scalability. Some people say: I use so, the effect seems to be good, why pay attention to so much, the realization of demand is the first. In addition, this looks very intuitive and is good for maintenance. In fact, everyone is in different environments, different understanding of specific issues, inevitably limited to their own field of thinking. As for this statement, I think it should be explained as follows: as a design principle, it is the guiding content extracted by people after a lot of project practice. If it’s a significant increase in effort and complexity for your project, then I don’t think it’s too much to violate in moderation. Everything is a matter of degree. Too much of a good thing is never good. In large and medium-sized projects, it is necessary to pay attention to the idea of software engineering, pay attention to norms and processes, otherwise personnel cooperation and later maintenance will be very difficult. For small projects, the corresponding simplification may be much, depending on time, resources, business and other factors, but thinking from the perspective of software engineering is very beneficial to the improvement of system robustness, maintainability and other performance indicators. Like a system with a one-month life cycle, you have a whole bunch of principles to think about, unless you get kicked in the head.
The key step to realize the open close principle is abstraction, and the inheritance relationship between base class and subclass is an embodiment of abstraction. Therefore, The Richter substitution principle is a specification for abstraction. Violating The Richter’s substitution principle means violating the open close principle, not the other way around. Richter’s substitution principle is an important guarantee to make code comply with the open – close principle.
6. Enlightenment through Richter’s substitution principle?
The principle of class inheritance: If an object that inherits from a class is likely to have a runtime error in the same place as the base class, the subclass should not inherit from that base class, or the relationship between them should be redesigned.
Action correctness guarantee: Class extensions that comply with the Richter substitution principle do not introduce new errors into existing systems.