设计模式-里氏替换原则
目录
一家店一开始只卖酒类,只让成年人进入消费。后续增加了奶制品,宣称可以让未成年人进店消费。我们都知道显然是无法营业的,因为这家店扩展业务之后包含了未成年不允许涉及的东西,甚至会导致原先的业务(酒类)无法开展。这就是一个未遵循里氏替换原则的反例。
1. 概述
里氏替换原则主要阐述了有关继承的一些原则,也就是什么时候应该使用继承,什么时候不应该使用继承,以及其中蕴含的原理。里氏替换原是继承复用的基础,它反映了基类与子类之间的关系,是对开闭原则的补充,是对实现抽象化的具体步骤的规范。理解里氏替换原则对程序的健壮性、扩展性、兼容性至关重要。
1.1. 依赖倒置原则的作用
- 里氏替换原则是实现开闭原则的重要方式之一。
- 它克服了继承中重写父类造成的可复用性变差的缺点。
- 它是动作正确性的保证。即类的扩展不会给已有的系统引入新的错误,降低了代码出错的可能性。
- 加强程序健壮性,同时变更时可以做到非常好的兼容性,提高程序维护性、可扩展性,降低需求变更时引入的风险。
1.2. 程序举例
几维鸟不是鸟
package com.skystep.design.unlsp;
public class Bird {
double flySpeed;
public void setSpeed(double speed) {
flySpeed = speed;
}
public double getFlyTime(double distance) {
return (distance / flySpeed);
}
}
package com.skystep.design.unlsp;
class Swallow extends Bird {
}
package com.skystep.design.unlsp;
class BrownKiwi extends Bird {
public void setSpeed(double speed) {
flySpeed = 0;
}
}
package com.skystep.design.unlsp;
public class Client {
public static void main(String[] args) {
Bird swallow1 = new Swallow();
Bird brownKiwi1 = new BrownKiwi();
swallow1.setSpeed(120);
brownKiwi1.setSpeed(120);
System.out.println("如果飞行300公里:");
try {
System.out.println("燕子将飞行" + swallow1.getFlyTime(300) + "小时.");
System.out.println("几维鸟将飞行" + brownKiwi1.getFlyTime(300) + "小时。");
} catch (Exception err) {
System.out.println("发生错误了!");
}
}
}
程序运行错误的原因是:几维鸟类重写了鸟类的 setSpeed(double speed) 方法,这违背了里氏替换原则。子类继承了父类的方法,但不得重写(覆盖)父类的非抽象(已实现)方法。否则大概率会影响原先的业务,且原先调用者无法知道该方法被重写。因为一般情况下,我们认为父类可以使用的地方,替换成子类业务应答不受影响。
1.3. 总结
继承提高代码的重用性,子类拥有父类的方法和属性;提高代码的可扩展性,子类可形似于父类,但异于父类,保留自我的特性;这是继承带给我们的好处,但是如果在编写程序中,对继承不做任何限制,任意继承,子类随意重写父类的方法,很可能会造成非常糟糕的结果,要重构大量的代码。所以我们提倡编写继承类时需要遵循:任何基类可以出现的地方,子类一定可以出现。
2. 实现方法
2.1. 遵循规则
- 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法
- 子类中可以增加自己特有的方法
- 当子类的方法重载父类的方法时,方法的前置条件(即方法的输入参数)要比父类的方法更宽松
- 当子类的方法实现父类的方法时(重写/重载或实现抽象方法),方法的后置条件(即方法的的输出/返回值)要比父类的方法更严格或相等
2.2. 具体实现
2.2.1 子类实现父类的抽象方法但不能覆盖父类的具体方法
package com.skystep.design.unlsp;
public class A {
public void fun(int a, int b) {
System.out.println(a + "+" + b + "=" + (a + b));
}
}
package com.skystep.design.unlsp;
public class B extends A {
@Override
public void fun(int a, int b) {
System.out.println(a + "-" + b + "=" + (a - b));
}
}
package com.skystep.design.unlsp;
public class Demo1 {
public static void main(String[] args) {
System.out.println("父类的运行结果");
A a = new A();
a.fun(1, 2);
//父类存在的地方,可以用子类替代
//子类B替代父类A
System.out.println("子类替代父类后的运行结果");
B b = new B();
b.fun(1, 2);
}
}
显然如果覆盖了父类的具体方法,那么在使用父类的地方换成其子类时便会影响原有的功能特性。
2.2.2. 子类中可以增加自己特有的方法
package com.skystep.design.unlsp.demo2;
public class A {
public void fun(int a, int b) {
System.out.println(a + "+" + b + "=" + (a + b));
}
}
package com.skystep.design.unlsp.demo2;
public class B extends A {
public void newfun() {
System.out.println("这是子类的新方法...");
}
}
package com.skystep.design.unlsp.demo2;
public class Demo2 {
public static void main(String[] args) {
System.out.print("父类的运行结果:");
A a = new A();
a.fun(4, 5);
//父类存在的地方,可以用子类替代
//子类B替代父类A
System.out.print("子类替代父类后的运行结果:");
B b = new B();
b.fun(4, 5);
//子类B的新方法
b.newfun();
}
}
如果在原先的功能P基础上新增功能P1,可以在子类上新增特性,在基类使用的地方替换成新的子类,并且调用新的功能特性以满足新功能要求。当然前提是每个功能符合单一原则。