Liskov Substitution Principle

 

Six Principles of Design Patterns (2): The Liskov Substitution Principle (reproduced)

 

We all know that object-oriented has three major characteristics: encapsulation, inheritance, and polymorphism. Therefore, in the actual development process, after the subclass inherits the parent class, according to the characteristics of polymorphism, it may be convenient for the moment, and the method of the parent class is often arbitrarily rewritten, so this method will greatly increase the probability of code problems. For example, the following scenario: class C implements a function F1. Now it is necessary to modify and expand the function F1, and expand the function F1 to F, wherein F is composed of the original function F1 and the new function F2. The new function F is completed by the subclass C1 of the class C, and the subclass C1 may cause the original function F1 of the class C to malfunction while completing the function F. This is where the Liskov substitution principle comes into play.


What is the Liskov Substitution Principle
  The single responsibility principle mentioned earlier is easy to understand literally, but the Liskov Substitution Principle is a bit confusing. After checking the information, I found that this principle was first proposed in 1988 by a woman surnamed Liskov from the Massachusetts Institute of Technology.

  English abbreviation: LSP (Liskov Substitution Principle).
  Strict definition: If for every object o1 of type T1, there is an object o2 of type T2, so that all programs P defined by T1 have no change in the behavior of program P when all objects o1 are replaced by o2, Then type T2 is a subtype of type T1.
  Popular definition: All references to the base class must be able to use the object of its subclass transparently.
  A more popular definition: a subclass can extend the functions of the parent class, but cannot change the original function of the parent class.


The Liskov substitution principle includes the following four meanings

    . Subclasses can implement the abstract methods of the parent class, but cannot override the non-abstract methods of the parent class.
    Subclasses can add their own unique methods.
    When a subclass overrides or implements a method of the superclass, the preconditions of the method (that is, the method's formal parameters) are more relaxed than the input parameters of the superclass method.
    When the method of the subclass implements the abstract method of the superclass, the postconditions of the method (that is, the return value of the method) are stricter than those of the superclass.

Now we can explain the meanings of the above four layers one by one:

 

Subclasses can implement abstract methods of superclass, but cannot override non-abstract methods of superclass

  When we do system design, we often design interfaces or abstract classes, and then subclasses implement abstract methods. The Liskov substitution principle is actually used here. It is easy to understand that a subclass can implement the abstract method of the parent class. In fact, the subclass must also fully implement the abstract method of the parent class, even if an empty method is written, otherwise it will compile an error.

   The key point of the Liskov Substitution Principle is that non-abstract methods of parent classes cannot be overridden. All the methods that have been implemented in the parent class are actually setting a series of specifications and contracts. Although it does not mandate that all subclasses must comply with these specifications, if the subclass arbitrarily modifies these non-abstract methods, it will be Will destroy the entire inheritance system. The Liskov Substitution Principle expresses this meaning.

   In the object-oriented design thought, inheritance brings great convenience to the system design, but it also brings some potential risks. Just like the scene mentioned at the beginning, it is best to follow the Liskov substitution principle in that case. When class C1 inherits class C, you can add new methods to complete new functions, and try not to override the methods of parent class C. Otherwise, it may bring unforeseen risks, such as the following simple example to restore the opening scene:

 

public class C {
    public int func(int a, int b){
        return a+b;
    }
}
 
class C1 extends C{
    @Override
    public int func(int a, int b) {
        return a-b;
    }
}
 
class Client{
    public static void main(String[] args) {
        C c = new C1();
        System.out.println("2+1=" + c.func(2, 1));
    }
}

// Running result: 2+1=1

 The above results are obviously wrong. Class C1 inherits C, and later needs to add new functions. Class C1 does not write a new method, but directly rewrites the func method of the parent class C, which violates the Liskov substitution principle. The place where the parent class is referenced cannot be used transparently. An object of the class, resulting in an error in the running result.

 

Subclasses can add their own unique methods

  While inheriting the properties and methods of the parent class, each subclass can also have its own personality, extending its own functions on the basis of the parent class. As mentioned earlier, when the function is extended, the subclass should try not to rewrite the method of the parent class, but write another method, so the above code is changed to conform to the Liskov substitution principle, the code is as follows:

 

 

public class C {
    public int func(int a, int b){
        return a+b;
    }
}
 
class C1 extends C{
    public int func2(int a, int b) {
        return a-b;
    }
}
 
class Client{
    public static void main(String[] args) {
        C1 c = new C1();
        System.out.println("2-1=" + c.func2(2, 1));
    }
}

//Running result: 2-1=1

 

When a subclass overrides or implements a method of the parent class, the preconditions of the method (that is, the formal parameters of the method) are more relaxed than the input parameters of the parent class method

code example

 

public class Father {
    public void func(HashMap m){
        System.out.println("Execute parent class...");
    }
}
 

class Son extends Father{
    public void func(Map m){//The formal parameter of the method is looser than that of the parent class
        System.out.println("Execute subclass...");
    }
}
 
class Client{
    public static void main(String[] args) {
        Father f = new Son();//Where the base class is referenced, the object of its subclass can be used transparently.
        HashMap h = new HashMap();
        f.func(h);
    }
}

//Run result: execute parent class...

 Note that the @Override annotation cannot be added in front of the func method of the Son class, because otherwise, the compilation prompt will report an error, because this is not an override (Override), but an overload (Overload), because the input parameters of the method are different. The difference between overriding and overloading has been explained in the article Java Object-Oriented Detailed Explanation , and will not be repeated here.

 

When the method of the subclass implements the abstract method of the superclass, the postconditions of the method (that is, the return value of the method) are stricter than those of the superclass

Code example:

public abstract class Father {
    public abstract Map func();
}
 
class Son extends Father{
     
    @Override
    public HashMap func(){//方法的返回值比父类的更严格
        HashMap h = new HashMap();
        h.put("h", "执行子类...");
        return h;
    }
}
 
class Client{
    public static void main(String[] args) {
        Father f = new Son();//引用基类的地方能透明地使用其子类的对象。
        System.out.println(f.func());
    }
}

//执行结果:{h=执行子类...}

 总结

  继承作为面向对象三大特性之一,在给程序设计带来巨大便利的同时,也带来了一些弊端,它增加了对象之间的耦合性。因此在系统设计时,遵循里氏替换原则,尽量避免子类重写父类的方法,可以有效降低代码出错的可能性。

 

 

Guess you like

Origin http://10.200.1.11:23101/article/api/json?id=326692981&siteId=291194637