El principio de sustitución de Liskov (una relación padre-hijo con una relación de amor-odio)

Entre los seis principios de diseño principales, el principio de sustitución de estilo Li es un principio sobre la relación entre las clases principales y las subclases. Este principio estipula que en el desarrollo de software, donde aparece la clase principal, se reemplaza la clase principal con una subclase y, sistemáticamente, la funcionalidad. no debe verse afectado.
El papel principal del principio de sustitución de Liskov es el siguiente.

它克服了继承中重写父类造成的可复用性变差的缺点。
它是动作正确性的保证。即类的扩展不会给已有的系统引入新的错误,降低了代码出错的可能性。
加强程序的健壮性,同时变更时可以做到非常好的兼容性,提高程序的维护性、可扩展性,
降低需求变更时引入的风险。

El principio de sustitución de Liskov define una especificación para una buena relación básica. El principio de sustitución de Liskov es, en términos generales: una subclase puede ampliar la función de la clase principal, pero no puede cambiar la función original de la clase principal. Contiene 4 capas de significados.

1、子类必须完全实现父类的方法
2、子类可以有自己的个性
3、覆盖或者实现父类时的输入参数可以被放大
4、覆写或实现父类方法时输出参数可以被缩小

Primero, la subclase debe implementar completamente los métodos de la clase padre

Cuando hacemos el diseño del sistema, generalmente definimos una interfaz o clase abstracta de acuerdo con los requisitos, y luego codificamos e implementamos la interfaz o la clase abstracta.La clase que llama toma la interfaz o la clase abstracta como un parámetro de función y recibe la interfaz o la clase abstracta. clase abstracta La implementación de la clase abstracta, aquí, ha utilizado el principio de sustitución de Liskov. El principio de sustitución de Liskov estipula que en el método de la clase principal como parámetro, se pasa cualquier subclase y su función no debe verse afectada de ninguna manera.
Tomemos un ejemplo para ilustrar, por ejemplo, todos han jugado CS, podemos usar diferentes armas para pelear con el enemigo y usar un diagrama de clase para describir esta escena: el
inserte la descripción de la imagen aquí
soldado está equipado con un arma y luego dispara con el arma .

el código se muestra a continuación:

Clase abstracta de pistola:

public abstract class AbstractGun {
    
    

    public abstract void shoot();
}

Clase de implementación de pistola:

public class HandGun extends AbstractGun {
    
    
    @Override
    public void shoot() {
    
    
        System.out.println("手枪射击");
    }
}

Clase de implementación de rifle

public class Rifle extends AbstractGun {
    
    
    @Override
    public void shoot() {
    
    
        System.out.println("步枪射击");
    }
}

Clase de soldado:

public class Soldier {
    
    

    private AbstractGun gun;

    void setGun(AbstractGun gun){
    
    
        this.gun = gun;
    }

    void killEnemy(){
    
    
        System.out.println("士兵上战场");
        gun.shoot();
    }

}

Método principal de la clase de juego

public class Game {
    
    
    public static void main(String[] args) {
    
    
        Soldier soldier = new Soldier();
        soldier.setGun(new HandGun());
        soldier.killEnemy();
        soldier.setGun(new Rifle());
        soldier.killEnemy();
    }
}

La estructura de la clase es muy simple. Podemos configurar diferentes armas para soldados y disparar con diferentes armas. Si hay nuevos tipos de armas, podemos definir nuevas armas, siempre y cuando implementemos completamente la clase principal (completar los requisitos de tiro). ), el código no fallará.
Pero, ¿y si el arma que vamos a definir es una pistola de juguete?
Pistolas de juguete:

public class ToyGun extends AbstractGun {
    
    
    
    //玩具枪也是枪,但是玩具枪无法射击呀?
    @Override
    public void shoot() {
    
    
        System.out.println("玩具枪不能发射子弹");
    }
}

En este momento, si se le da una pistola de juguete a un soldado, la pistola de juguete también es un subtipo de pistola, por lo que el soldado recibirá la pistola de juguete y usará la pistola de juguete en el campo de batalla, ¿cuáles serán las consecuencias y el enemigo? recibirá un disparo directo en la cabeza.
¿Qué se debe hacer entonces?
1. A juzgar en la clase Soldado, si es una pistola de juguete, se usará para matar al enemigo y darle un tratamiento especial, pero ¿cuáles serán las consecuencias de esto? Hacerlo significa que todas las funciones de AbstractGun en el programa como los parámetros deben usarse como juguetes El juicio de la pistola, esto es simplemente terrible, obviamente, este método fue rechazado.
2. Desconecte la relación de herencia y establezca una clase principal independiente, como se muestra a continuación.
inserte la descripción de la imagen aquí
Por lo tanto, el principio de sustitución de Liskov nos limita: si la subclase puede implementar completamente el negocio de la clase principal, si no, entonces tenemos que hacer otras consideraciones.

Segundo, las subclases pueden tener sus propias personalidades.

Las subclases, por supuesto, tendrán sus propios comportamientos y características únicas, por lo que, sobre la base del primer artículo, el principio de sustitución de Liskov estipula que donde aparecen las subclases, la clase principal puede no aparecer. En otras palabras, use subclases en función del parámetro , pásele la clase padre y el programa puede salir mal.
Tomemos un arma como ejemplo:
los rifles se dividen en rifles ordinarios y rifles de francotirador. Los rifles comunes, como el AK-47, pueden disparar directamente, mientras que los rifles de francotirador son aquellos con visores, que se pueden apuntar primero y luego disparar. francotirador puede disparar con un rifle de francotirador.
El diagrama de clase es el siguiente: El
inserte la descripción de la imagen aquí
francotirador necesita un rifle de francotirador AUG para disparar a la
clase AUG:

public class AUG extends Rifle {
    
    

    public void zoomOut(){
    
    
        System.out.println("用望远镜观察敌人");
    }
}

Clase de francotirador:

public class Snipper {
    
    
    private AUG aug;


    void setAug(AUG aug){
    
    
        this.aug = aug;
    }

    void killEnemy(){
    
    
        System.out.println("AUG射击。。。。");
    }

}

método principal:

public class Client {
    
    
    public static void main(String[] args) {
    
    
        Snipper snipper = new Snipper();
        snipper.setAug(new AUG());
        snipper.killEnemy();
        snipper.setAug((AUG) new Rifle());
        snipper.killEnemy();
    }
}

Resultados de la operación:
inserte la descripción de la imagen aquí
obviamente, este método solo puede usar subclases. Si la clase principal se usa como parámetro, se informará una excepción de conversión. Desde el principio de sustitución de Liskov, donde hay subclases, la clase principal puede no aparecer.

3. Los parámetros de entrada al anular o implementar la clase principal se pueden ampliar

Cuando la subclase reescribe el método de la clase principal, si el parámetro de la clase principal es HashMap, entonces el parámetro de la subclase debe ser más relajado, como Map o Map, si la clase principal es Map, la subclase solo puede ser Mapa
Ejemplo:
tengo una clase padre y una clase hijo, la clase padre es la clase padre del hijo

public class Father {
    
    

    public Collection doSomething(HashMap map){
    
    
        System.out.println("父类被执行。。。。");
        return map.values();
    }
}

class Son extends Father {
    
    
    public Collection doSomeThing(Map map){
    
    
        System.out.println("子类被执行。。。。");
        return map.values();
    }
}

public class Client {
    
    

    public static void main(String[] args) {
    
    
    //里氏替换原则中,父类出现的地方,子类也可以出现
//        Father f = new Father();
        Son f = new Son();
        HashMap hashMap = new HashMap();
        f.doSomething(hashMap);
        
    }
}

El código anterior define tres clases. En la clase padre, el parámetro de la clase principal es Map, y el parámetro de la subclase es HashMap. Sabemos que Map es la clase principal de HashMap, por lo que el parámetro dado a la clase principal es hashmap, que sigue siendo normal.ejecutar, código anterior, código comentado

//        Father f = new Father();
        Son f = new Son();

No importa cuál se use, el resultado de la ejecución es el mismo,
inserte la descripción de la imagen aquí
porque los parámetros de la subclase son más flexibles que los de la clase principal, por lo que reemplazar la clase principal con la subclase no afectará el resultado.
Si el parámetro de la clase principal es más flexible que la clase secundaria (la clase principal es Map, la clase secundaria es HashMap)

public class Father {
    
    

    public Collection doSomething(Map map){
    
    
        System.out.println("父类被执行。。。。");
        return map.values();
    }
}

class Son extends Father {
    
    
    public Collection doSomeThing(HashMap map){
    
    
        System.out.println("子类被执行。。。。");
        return map.values();
    }
}

public class Client {
    
    

    public static void main(String[] args) {
    
    
    //里氏替换原则中,父类出现的地方,子类也可以出现
//        Father f = new Father();
        Son f = new Son();
        HashMap hashMap = new HashMap();
        f.doSomething(hashMap);
        
    }
}

Luego, cuando la clase principal se reemplaza con una clase secundaria, el resultado de la ejecución es:
inserte la descripción de la imagen aquí
Esto contrasta con el principio de sustitución de Liskov, donde la clase principal se reemplaza con una clase secundaria y el resultado de la ejecución no se contradice.

En cuarto lugar, los parámetros de salida se pueden reducir al anular o implementar el método de la clase principal.

Cuando una subclase anula o implementa un método de la clase principal, los parámetros devueltos por la subclase deben ser coherentes con los parámetros de la clase principal o ser una subclase de los parámetros de la clase principal.

V. Resumen

El propósito del principio de reemplazo de muestreo de Liskov es aumentar la solidez del programa, de modo que el proyecto pueda mantener una muy buena compatibilidad mientras se actualiza la versión Incluso si se agregan subclases, las subclases originales pueden continuar ejecutándose. En el proyecto real, cada subclase corresponde a un negocio diferente, usa la clase principal como parámetro y pasa diferentes subclases para implementar la lógica comercial, ¡es perfecto!

Supongo que te gusta

Origin blog.csdn.net/qq_45171957/article/details/123294726
Recomendado
Clasificación