大家好,我是Ant,今天我们来学习的是设计原则之一的里氏替换原则(Liskov Substitution principle) 。
什么是里氏替换原则?
里氏替换原则是面向对象设计的基本原则之一,它是对继承复用的一个补充。
里氏替换原则的定义是:如果对于一个对象o1,都有对象o2,使得以o2替换o1,程序的行为没有发生变化,那么o2是o1的子类型。
简单来说,就是一个父类可以被它的子类所替换,而不会影响业务逻辑和正确性。
同时根据上面的定义,我们可以扩展一下:如果子类不能完整的实现父类的业务, 那么建议断开父子关系, 采用依赖、聚集、组合等方式代替继承。
为什么使用里氏替换原则?
里氏替换原则主要是来约束继承的复用的。
那么为什么要使用呢?假设一下:
在Order
中有一个方法orderSortByAmount
,这个方法的功能是对订单根据金额进行排序, 而在OrderExt
中将orderSortByAmoount
改成了根据订单的创建时间排序。那么使用的时候,既不报错,结果又不对,是不是就傻眼了。
所以,我们需要使用里氏替换原则来约束继承的复用。
违反里氏替换原则的情况
一般来讲,我们写的继承代码基本不会违背里氏替换原则,那么什么情况下会违背呢?
-
第一种就是上面提到的,彻底修改了父类的业务逻辑。
-
第二种是修改了父类的接口权限。不过这种情况IDEA编译器自动给避免了。
-
第三种是子类违反了父类要求的入参、出参、异常等。
-
第四种是子类违背了父类要求的特殊规则。
- 比如父类要求不能出现负的金额,但子类却允许出现负的金额。
示例
在上一篇开闭原则中,我们有以下的示例,当时提到了这样修改违背了里氏替换原则。
scss
public class GoodsDiscounts extends Goods {
GoodsDiscounts(String name, Double price) {
super(name, price);
}
public Double getPrice() {
super.getPrice() * 0.6;
}
public Double getOriginPrice() {
super.getPrice();
}
}
那么为什么说它违背了呢? 是否还记得,我们有这么一个方法:
arduino
public static Double getTotalPrice(Goods goods, int num) {
return goods.getPrice() * num;
}
这么看其实并没有违背吖?那么为什么说它违背呢? 如果我们将getTotalPrice
定义为获取总价格,然后有getPayPrice()
获取实际支付价格,代码如下:
arduino
public static Double getPayPrice(Goods goods, int num) {
return goods.getPrice() * num;
}
那么将GoodsDiscounts
传入getTotalPrice
中是否还能得到正确的结果呢? 明显我们得不到正确的结果,那么我们将GoodsDiscounts
代码修改为如下:
scss
public class GoodsDiscounts extends Goods {
GoodsDiscounts(String name, Double price) {
super(name, price);
}
public Double getDiscountsPrice() {
super.getPrice() * 0.6;
}
}
这样就没有修改getPrice()
了,也没修改父类的业务逻辑,现在总该符合了吧?
其实还不符合,我们在getPayPrice()
中就拿不到想要的结果了,要么就得修改getPayPrice
方法,而这又不符合开闭原则。
那么我们就没有两全其美的方法吗? 其实在上面的示例我们可以感觉到GoodsDicounts
似乎就不适合继承了。
这时候我们可以使用策略模式 来进行重构:
首先建立一个策略, 先创建一个价格策略接口类,然后实现对应的策略:
java
/**
* 价格策略
*/
public interface PriceStrategy{
Double calculatePrice(Double price, int num);
}
/**
* 原价计算策略
*/
public PriceCalculatorStrategy implements PriceStrategy{
@Override
public Double calculatePrice(Double price, int num) {
return price * num;
}
}
/**
* 折扣计算策略
*/
public DiscountsPriceCalculatorStrategy implements PriceStrategy{
private double discounts = 1.0;
DiscountsPriceCalculator(double discounts) {
this.discounts = discounts;
}
@Override
public Double calculatePrice(Double price, int num) {
return price * num;
}
}
其次将GoodsTest
修改为:
csharp
public class GoodsTest{
public static void main(String[] args) {
Goods goods = new Goods("苹果", 5.0);
int num = 5;
System.out.println("买了" + num + goods.getName() + ", " +
"总价:" + calculatePrice(goods, num,
new PriceCalculatorStrategy()));
// 如果要知道折扣实付金额
System.out.println("打折,只需要付" + calculatePrice(goods, num,
new DiscountsPriceCalculatorStrategy(0.6)));
}
/**
* 计算价格,理论上应该是策略的调用类,这里简化为方法
*/
public static Double calculatePrice(Goods goods, int num,
PriceStrategy strategy) {
return strategy.calculatePrice(goods.getPrice(), num);
}
}
这样我们就将GoodsDiscounts
的业务逻辑和计算逻辑分离了,并且将计算逻辑抽象出来了,这样就符合了里氏替换原则。 之后再增加什么计算规则,都可以增加对应的策略。
可能上面这样改,我们可能会产生一个疑问:这不是修改了getTotalPrice()
和getPayPrice()
吗?
其实有时我们不能要求一开始就设计的很完美,设计要适合,比如一开始想到了价格会出现不同的算法,那么就可以使用策略模式, 但有时可能很久都不会修改价格,那么就没必要设计,不能因为未来很低的可能性,造成过度设计。
如果一开始以为出现修改价格的可能性很低,但后来就是出现了,那么就可以先扩展,扩展不了就重构,重构不算修改。
结语
里氏替换原则是面向对象设计的基本原则之一,它主要是用来约束继承。
同时我们要注意,设计要适度,根据经验判断大概率会出现的,那么不用想,直接上设计模式(而且大概率也用过对应的设计模式), 如果小概率才出现,而且可能影响进度,造成代码冗余,那就没必要设计。
接下来我们将学习单一职责原则(Single Responsibility Principle) ,敬请期待。