1 What is the Richter replacement principle
The Richter replacement principle, presented in an article "Data Abstraction and Hierarchy", published at the OOPSLA conference in 1987, by Ms Liskov of the MIT Computer Science Laboratory, mainly elaborates on some principles of inheritance. , that is, when you should use inheritance, when you should not use inheritance, and the principle of its implication. In 2002, the software engineering master Robert C. Martin, mentioned in the single-responsibility principle, published a "Agile Software development Principles Patterns and practices", In this article, he finally reduced the principle of the Richter scale to a sentence: "Subtypes must is substitutable for their base types". That is, subclasses must be able to replace them with their base classes.
We explain the Richter substitution principle more completely: in a software system, subclasses should be able to replace any base class that can appear, and after being replaced, the code will work properly.
2 First example: A square is not a rectangle
The "Square is not a rectangle" is one of the most classic examples of how to understand the Richter substitution principle. In the field of mathematics, the square is undoubtedly a rectangle, it is a long and wide equal rectangle. So, we developed a geometry-related software system, let the square inherit from the rectangle is a smooth Cheng Zhang thing. Now, we intercept a snippet of the system for analysis:
//Rectangle class Rectangle: class Rectangle { DoubleLengthDoubleWidth Public DoubleGetLength () {returnLength } Public voidSetLength (DoubleHeight) { This. length = length; } Public DoubleGetWidth () {returnWidth } Public voidSetWidth (Doublewidth) { This. width = width; } }//Square category square: class Square extends Rectangle { Public voidSetWidth (Doublewidth) {Super. SetLength (width);Super. SetWidth (width); } Public voidSetLength (DoubleLength) {Super. setLength (length);Super. setwidth (length); } }
Since the degrees and widths of the squares must be equal, the lengths and widths are assigned the same values in the methods SetLength and SetWidth. Class Testrectangle is a component of our software system, and it has a resize method to use the function of the base class Rectangle,resize method is to simulate the gradual growth of the rectangular width effect:
//测试类TestRectangle:class TestRectangle { publicvoidresize(Rectangle objRect) { while(objRect.getWidth() <= objRect.getLength()) { objRect.setWidth( 1 ); } }}
We run this code and we find that if we pass a normal rectangle as a parameter into the resize method, we will see the effect of the rectangular width increasing, and when the width is greater than the length, the code will stop, and the result of this behavior is in line with our expectation. If we pass a square as a parameter into the resize method, we will see that the width and length of the square are growing, and the code will run until the system generates an overflow error. So, the normal rectangle is suitable for this piece of code, the square is not suitable.
We conclude that in the resize method, the parameters of the rectangle type cannot be replaced by the square type of the parameter, and if the substitution is made, the expected result is not obtained. Thus, the inheritance relationship between the square class and the rectangle class violates the principle of the Richter substitution, and the inheritance between them is not true, and the square is not a rectangle.
3 Second Example: Ostrich is not a bird
"Ostrich non-bird" is also a classic example of understanding the principle of replacing the Richter scale. Another version of "Ostrich non-bird" is the "Penguin Non-Bird", these two statements are essentially no difference, the premise is that the birds can not fly. The definition of birds in biology: "Constant temperature animals, oviparous, the whole body is draped with feathers, the body is streamlined, has a keratin beak, eyes on the sides of the head." The forelegs degenerated into wings, and the hind limbs were scaly, with four toes. " So, from a biological point of view, an ostrich must be a bird.
We design a bird-related system in which ostriches are naturally derived from birds, and all the characteristics and behavior of birds are inherited by ostrich classes. Most birds fly in the image of people, so we devised a method named Fly for birds, and gave some of the properties associated with flying, such as velocity.
//鸟类Bird:class Bird { double velocity; publicfly//I am flying; }; publicsetVelocity(doublethis.velocity = velocity; }; publicgetVelocityreturnthis.velocity; };}
鸵鸟不会飞怎么办?我们就让它扇扇翅膀表示一下吧,在fly方法里什么都不做。至于它的飞行速度,不会飞就只能设定为0了,于是我们就有了鸵鸟类的设计。
//鸵鸟类Ostrich:class Ostrich extends Bird { public//I do nothing; }; public setVelocity(doublethis0; }; publicreturn0; };}
好了,所有的类都设计完成,我们把类Bird提供给了其它的代码(消费者)使用。现在,消费者使用Bird类完成这样一个需求:计算鸟飞越黄河所需的时间。对于Bird类的消费者而言,它只看到了Bird类中有fly和getVelocity两个方法,至于里面的实现细节,它不关心,而且也无需关心,于是给出了实现代码:
//测试类TestBird:class TestBird { publiccalcFlyTime(Bird bird) { try{ double3000; System.out.println(riverWidth / bird.getVelocity()); }catch(Exception err){ System.out.println("An error occured!"); }
If we take a bird to test this code, no problem, the result is correct, in line with our expectations, the system output the birds fly over the Yellow River the time required; If we take the ostrich to test this code, the result code has a system-zero exception, which is obviously not in line with our expectations.
For the Testbird class, it is just a consumer of the bird class, when using the bird class, it is only necessary to use the method provided by the bird class, do not care about the ostrich will fly such problems, and do not need to know. It is to calculate the time required for a bird to fly over the Yellow River according to the "Time needed = width of the Yellow river/speed of Flight of the bird".
We conclude that in the Calcflytime method, the parameters of the bird type cannot be replaced by the parameters of the ostrich type, and the expected result is not obtained if the substitution is made. Thus, the inheritance relationship between the Ostrich class and the bird class violates the principle of the Richter substitution, and the inheritance between them is not true, and the ostrich is not a bird.
Are 4 ostriches really birds?
"Ostrich is not a bird", ostrich is a bird is not a bird, this conclusion seems to be a paradox. There are two reasons for this confusion:
Reason one: the definition of the inheritance relationship of a class is not clear.
面向对象的设计关注的是对象的行为,它是使用“行为”来对对象进行分类的,只有行为一致的对象才能抽象出一个类来。我经常说类的继承关系就是一种“Is-A”关系,实际上指的是行为上的“Is-A”关系,可以把它描述为“Act-As”。关于类的继承的细节,我们可以单独再讲。我们再来看“正方形不是长方形”这个例子,正方形在设置长度和宽度这两个行为上,与长方形显然是不同的。长方形的行为:设置长方形的长度的时候,它的宽度保持不变,设置宽度的时候,长度保持不变。正方形的行为:设置正方形的长度的时候,宽度随之改变;设置宽度的时候,长度随之改变。所以,如果我们把这种行为加到基类长方形的时候,就导致了正方形无法继承这种行为。我们“强行”把正方形从长方形继承过来,就造成无法达到预期的结果。“鸵鸟非鸟”基本上也是同样的道理。我们一讲到鸟,就认为它能飞,有的鸟确实能飞,但不是所有的鸟都能飞。问题就是出在这里。如果以“飞”的行为作为衡量“鸟”的标准的话,鸵鸟显然不是鸟;如果按照生物学的划分标准:有翅膀、有羽毛等特性作为衡量“鸟”的标准的话,鸵鸟理所当然就是鸟了。鸵鸟没有“飞”的行为,我们强行给它加上了这个行为,所以在面对“飞越黄河”的需求时,代码就会出现运行期故障。
Reason two: The design depends on the user requirements and the specific environment.
继承关系要求子类要具有基类全部的行为。这里的行为是指落在需求范围内的行为. A需求期望鸟类提供与飞翔有关的行为,即使鸵鸟跟普通的鸟在外观上就是100%的相像,但在A需求范围内,鸵鸟在飞翔这一点上跟其它普通的鸟是不一致的,它没有这个能力,所以,鸵鸟类无法从鸟类派生,鸵鸟不是鸟。B需求期望鸟类提供与羽毛有关的行为,那么鸵鸟在这一点上跟其它普通的鸟一致的。虽然它不会飞,但是这一点不在B需求范围内,所以,它具备了鸟类全部的行为特征,鸵鸟类就能够从鸟类派生,鸵鸟就是鸟。所有派生类的行为功能必须和使用者对其基类的期望保持一致,如果派生类达不到这一点,那么必然违反里氏替换原则。在实际的开发过程中,不正确的派生关系是非常有害的。伴随着软件开发规模的扩大,参与的开发人员也越来越多,每个人都在使用别人提供的组件,也会为别人提供组件。最终,所有人的开发的组件经过层层包装和不断组合,被集成为一个完整的系统。每个开发人员在使用别人的组件时,只需知道组件的对外裸露的接口,那就是它全部行为的集合,至于内部到底是怎么实现的,无法知道,也无须知道。所以,对于使用者而言,它只能通过接口实现自己的预期,如果组件接口提供的行为与使用者的预期不符,错误便产生了。里氏替换原则就是在设计时避免出现派生类与基类不一致的行为。
5 How to use the Richter replacement principle correctly
The purpose of the Richter replacement principle is to ensure the correctness of the inheritance relationship. Do we have to take such a hard look at every succession relationship in the actual project? Do not need, in most cases, according to the "Is-a" to design the inheritance relationship is not a problem, only a few cases, you need to carefully deal with, this kind of situation for a bit of development experience of people, generally will be aware of, there are rules to follow. Most typically, the user's code must contain code that performs the corresponding action according to the subclass type:
//animal type animal: Public class Animal{String name; Public Animal(String name) { This. name = name; } Public void Printname(){Try{System.out.println ("I am a"+ name +"!"); }Catch(Exception Err) {System.out.println ("An error occured!"); }}}//Cat type cat: Public class Cat extends Animal{ Public Cat(String name) {Super(name); } Public void Mew(){Try{System.out.println ("mew~~~"); }Catch(Exception Err) {System.out.println ("An error occured!"); } }}//Dog dogs: Public class Dog extends Animal { Public Dog(String name) {Super(name); } Public void Bark(){Try{System.out.println ("bark~~~"); }Catch(Exception Err) {System.out.println ("An error occured!"); }}}//Test class: Testanimal Public class testanimal { Public void TESTLSP(Animal Animal) {if(AnimalinstanceofCAT) {Cat cat = (cat) animal; Cat.printname (); Cat. Mew (); }if(AnimalinstanceofDog) {Dog dog = (dog) animal; Dog.printname (); Dog. Bark (); } }}
Like this code is obviously not in line with the Richter replacement principle, it gives users a lot of trouble to use, and even can not use, for future maintenance and expansion of great hidden trouble. The key step to realize the opening and closing principle is abstraction, and the inheritance relation between the base class and subclass is an abstract embodiment. Therefore, the Richter substitution principle is a specification for abstraction. Violating the Richter replacement principle means violating the open and closed principle, not necessarily. The Richter replacement principle is an important guarantee for the code to conform to the open and closed principle.
We're familiar with this code, at least in my previous Java and PHP projects. For example, there is a Web page, in order to achieve the customer information to view, add, modify, delete functions, the general server-side corresponding processing class has such a paragraph:
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 }
Everyone is familiar with it, in fact, this is against the Richter replacement principle, the result is that maintainability and scalability will become worse. Some people say: I so use, the effect seems to be good, why pay attention to so much, the realization of demand is the first place. In addition, this kind of writing seems very intuitive, conducive to maintenance. In fact, everyone is in a different environment, the understanding of specific problems, it is inevitable to confine themselves in the field of thinking. For this, I think it should be explained that: as a design principle, people through a lot of project practice, the final refinement of the guiding content. If there is a significant increase in workload and complexity for your project, I think a moderate violation is not too much. Doing anything is a matter of degree, and it is not good to overdo it. In large and medium-sized projects, it is important to pay attention to the idea of software engineering, stress norms and processes, otherwise personnel collaboration and later maintenance will be very difficult. For small projects may be corresponding to simplify a lot, may depend on time, resources, business and other factors, but more from the point of view of software engineering to consider the problem, the robustness of the system, maintainability and other performance indicators of the improvement is very useful. Like a system with a life cycle of only one months, you also consider a lot of principles, unless your head is kicked by a donkey.
The key step to realize the opening and closing principle is abstraction, and the inheritance relation between the base class and subclass is an abstract embodiment. Therefore, the Richter substitution principle is a specification for abstraction. Violating the Richter replacement principle means violating the open and closed principle, not necessarily. The Richter replacement principle is an important guarantee for the code to conform to the open and closed principle.
What kind of revelation does it bring to us by the principle of the Richter replacement?
类的继承原则:如果一个继承类的对象可能会在基类出现的地方出现运行错误,则该子类不应该从该基类继承,或者说,应该重新设计它们之间的关系。动作正确性保证:符合里氏替换原则的类扩展不会给已有的系统引入新的错误。
lsp-Richter Replacement principle