前言
- 设计模式其实就是前辈在工作中对思想的一种整理。我们都知道Java是面向对象编程的。面向对象的三大特点就是继承,封装,多态
- 我们今天的主题就是里氏替换原则。他的产生就是解决继承带来的问题
插曲
- 今天一个好朋友问到我他在使用fastjson但是引入另外一个项目,另外一个项目对内部进行了优化,但是并没有兼容自己的项目。

- 针对这种问题我提供了一些自己的见解。我建议他通过双亲机制覆盖掉其他项目的重写的部分。但是转念一想好像又有问题。把别人的覆盖掉被人的功能可能就会受影响的。
回归正题
- 后来我们分析了下引入包的修改处,主要是进行的抽象。其他子类通过继承的方式向fastjson提供小部分的功能。这个被继承的父类负责整合功能。
- 继承的主要特点就是设定规则和契约。虽然他并不会强制要求子类进行全部实现。但是他或多或少的要求不能轻易的改动。因为父类的正题规划而子类只是实现一小部分功能。为什么上面的问题我们不能直接进行子类的定义扩展呢。因为fastjson是别人封装好的。里面并没有提供向外注册的功能所以我们即使继承了父类也无法将子类注册上去。这个问题涉及到fastjson。这里不做展开。我们主题还是回归里氏替换上面。
- 不仅子类受父类的限制,因为是继承父类也会被子类限制。在1.0中父类设计规划好后需求升级了父类的整体规划需要改动。这个时候我们就需要考虑到子类的使用场景。不能仅仅将父类的规划进行升级。这就是两个对象增加了耦合性
- 那么关于继承的使用我们就需要提出一种约定来避免上面问题的产生。这就是里氏替换原则

里氏替换
- T2继承T1,在P程序中T2可以完全替代T1即为里氏替换原则。
- 那么对于T2有需要不能重写T1中已经实现好的功能。比如T1中compute方法实现加法的功能而T2重写了compute变成减法的功能。那就不能进行替换了。
- 但是实际开发中我们不可能不重写父类的方法。如果又想里氏替换又想重写,那么我们只能继续向上再抽离一层出来了。我们只需要程序中使用B即可。这样B和T1或者说B和T2之间仍然是里氏替换

- 通过上面的优化方式来改进,我们就不会发生子类无法替换父类的情况了。因为我们将父类和子类的关系进行了转移了。
常规开发
public class LiReplace {
public static void main(String[] args) {
Math math = new Math();
System.out.println("3+4="+math.add(3,4));
ZMath zMath = new ZMath();
System.out.println("3+4="+zMath.add(3,4));
System.out.println("13-4="+zMath.sub(13,4));
}
}
class Math{
public Integer add(int a, int b) {
return a+b;
}
}
class ZMath extends Math {
@Override
public Integer add(int a, int b) {
return a-b;
}
public Integer sub(int a, int b) {
return a-b;
}
}
- 像上面这种写法是很常见的。但是这段代码就和里氏替换原则想违背了。因为作为Math的子类,ZMath是无法完全替代Math的。就是因为ZMath重写了Math的add方法。
- 有的读者可能会说这个简单啊。不重写不就行了吗。这里强调一下为了举例这里代码方法名叫add假如方法是其他名称。而ZMath又是必须重写ad d方法的情况。那么我们就不能去掉这段重写。
- 如果我们必须重写那就冲破了里氏替换原则了。里氏替换的冲破影响的就是使用者。对于使用者来说我在使用子类替换父类的时候居然达不到父类的功效。最明显的是现在我有一个抽象类并有一个start方法和待实现的end方法。当我实现了一个子类后。在抽象类中start的方法是将数据写入到数据库中。但是因为我实现的这个子类不仅实现了end方法了还实现了start方法了 。那么这个时候子类必然无法达成父类的功能。对于使用者了来说就无法理解这两个类之间的关联与存在的意义了。
public class LiReplace {
public static void main(String[] args) {
Math math = new Math();
System.out.println("3+4="+math.add(3,4));
ZMath zMath = new ZMath();
System.out.println("3+4="+zMath.addOrSub(3,4));
System.out.println("13-4="+zMath.sub(13,4));
}
}
class Base {
public Integer compute(int a, int b) {
return a*b;
}
}
class Math extends Base{
public Integer add(int a, int b) {
return a+b;
}
}
class ZMath extends Base {
public Integer addOrSub(int a, int b) {
return a-b;
}
public Integer sub(int a, int b) {
return a-b;
}
}
- 上述就是将父类进行提升。这样就解决了里氏原则的问题。Math是可以替换Base类.因为并没有重写父类的东西。而对于Math和ZMath各自有各自的东西实现,两者并没有实际的关联关系,所以对于使用者来说他们并不会认为两者有任何的关联。也就不会造成困扰了。
继续升级
- 仔细观察下方案一,针对里氏替换问题感觉是解决了,但是又感觉没解决。为什么会有这种感觉呢?
- 首先我们产生里氏原则冲突时因为ZMath继承了Math类并重写了里面的方法。而方案一的确是解决了里氏原则问题。但是使得ZMath和Math完全是两个独立的东西了。那为什么我们不从一开始就不适用两个类的继承关系呢?从一开始ZMath就不继承Math问题自然就不存在
- 所以综上所述,方案一并不是解决问题而是在逃避问题。
- 但是我也说了方案一也算是解决了里氏冲突问题。因为的的确确里氏原则不冲突。接下来我们就是解决ZMath和Math类关系的问题
- ZMath的功能又依赖于Math的话,我们可以通过依赖,聚合,组合的方式来进行解决。说白了就是ZMath中通过属性的方式引入Math类
public class LiReplace {
public static void main(String[] args) {
Math math = new Math();
System.out.println("3+4="+math.compute(3,4));
ZMath zMath = new ZMath();
System.out.println("3+4="+zMath.compute(3,4));
System.out.println("13-4="+zMath.sub(13,4));
}
}
class Base {
public Integer compute(int a, int b) {
return a*b;
}
}
class Math extends Base{
@Override
public Integer compute(int a, int b) {
return a+b;
}
}
class ZMath extends Base {
private Math math = new Math();
@Override
public Integer compute(int a, int b) {
return a-b;
}
public Integer sub(int a, int b) {
return math.compute(a,b);
}
}
总结
- 里氏替换发生在继承的关系上。子类中尽量不要出现重写的方法。
- 如果发生重写,我们就需要将父类子类的关系进行转移,往往是将关系上移
- 最后我们通过依赖,聚合,组合的方式来解决两个类之间的关系调用问题