02.设计原则之里氏替换原则

115 阅读5分钟

里氏替换原则

定义

Liskov Substitution Principle
里氏替换原则:一种面向对象编程的设计原则,要求子类对象能够替换其父类对象,且不影响程序的正确性。

在该定义中需要抓住两个重点:

  1. 子类必须能够替换他们的基类,意味着子类必须保持与父类行为兼容,在重写一个方法时,你要对基类行为进行扩展,而不是将其完全替换
  2. 继承表达类型抽象

上述解释还是概念比较强,我们来一个形象的比喻,里氏替换原则可以理解为“青出于蓝而胜于蓝”,子类完美继承父类接口设计的约定,并做了增强 而不是改变父类的设计初衷。

多态

定义

父类的引用可以指向子类的对象

笔者刚接触里氏替换原则的时候,就有点与多态区分不开。 多态 是面向对象编程的一大特性,也是面向对象编程语言的一种语法。它是一种代码实现的思路。 里氏替换原则 是一种针对子类和父类关系的设计原则,用来指导继承关系中的子类该如何设计,子类的设计要保证在替换父类的时候,不改变原有程序的逻辑及不破坏 原有程序的正确性。

public class ListDemo {
    public static void main(String[] args) {
        // 父类的引用指向子类的对象
        List arrayList = new ArrayList();
        List linkedList = new LinkedList();
        addV(arrayList,123);
        addV(linkedList,123);

    }

    /***
     * addV方法第一个参数为List 是一个接口,支持传入任何List接口的子类。这家就是多态
     * @param list
     * @param val
     */
    public static void addV(List list, Object val) {
        list.add(val);
    }
}

分析

我们还是以JDK的集合ArrayList、LinkedList和AbstractSequentialList源码为例,来分析一下里氏替换原则

  1. ArrayList类结构
public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{...}
  1. LinkedList类结构

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{...}
  1. AbstractSequentialList类结构
public abstract class AbstractSequentialList<E> extends AbstractList<E> {...}

我们可以看到,ArrayList和LinkedList都属于List接口的子类, 都属于AbstractList抽象类的子类(虽然LinkedList中间还有一个AbstractSequentialList的父类, 但在整个继承链上依然是AbstractList的子类)。现在我们基于ArrayList和LinkedList书写以下代码来展示“替换”的精妙之处。

public class ListDemo {
    public static void main(String[] args) {
        // 父类的引用指向子类的对象
        List arrayList = new ArrayList();
        List linkedList = new LinkedList();
        addV(arrayList,123);
        addV(linkedList,123);

    }

    /***
     * addV方法第一个参数为List 类型,父类类型,支持传入任何List接口的子类ArrayList和LinkedList。此处体现里氏替换原则
     * @param list
     * @param val
     */
    public static void addV(List list, Object val) {
        list.add(val);
    }
}

然而,我们都知道ArrayList和LinkedList的区别,LinkedList中有自己特有的属性和方法,比如LinkedList中有addFirst而ArrayList则没有 因此也从侧面印证了里氏替换原则的核心,子类并不改变父类的设计的初衷,子类只是完美继承父类的设计初衷并做增强。

哪些代码会明显违背里氏替换原则

  1. 子类违背父类声明要实现的功能 比如:父类中提供一个方法是按照创建时间排序,子类重写后,按照更新时间排序,这就违背了里氏替换原则
  2. 子类违背父类对输入、输出、异常的约定 比如:父类中一个函数的约定是运行出错返回null,获取数据为空时返回空集合,子类重写函数之后,运行出错返回异常,获取不到数据返回null。这就违背了里氏替换
  3. 子类违背父类注释中所罗列的任何特殊说明 比如:父类中定义提现函数,注释中这么写的"用户提现金额不能超过账户余额",而子类重写后,针对VIP账户实现了透支提现功能 也就是说提现金额可以大于账户余额。这就不符合里氏替换原则

【例子1】违反替换原则的文档类层次结构例子

Document接口

public interface Document {

    public void open();

    public void save();

}

ReadOnlyDocument实现 Document子类

// 只读文件中的save调用会抛出异常,然而在父类的save方法中则没有此限制,违反了父类的约定,即违反了里氏替换原则
public class ReadOnlyDocument implements Document {

    @Override
    public void open() {

    }
    
    @Override
    public void save() {
        throw new RuntimeException("无法保存");
    }
}

Project类

// 此处代码也违反了开闭原则,因为客户端代码将依赖于具体的文档类。如果你引入了新的文档子类,则需要修改客户端代码才能对其进行支持。
public class Project {

    private List<Document> documentList;

    public void openAll() {
        for (Document doc:documentList) {
            doc.open();
        }
    }

    public void saveAll() {
        for (Document doc:documentList) {
            if (doc instanceof ReadOnlyDocument) {
            } else {
                doc.save();
            }
        }
    }
}

上述问题如何解决? 当把只读文档类作为层次结构中的基类后,这个问题得到了解决。

你可以通过重新设计类层次结构来解决这个问题:一个子类必须扩展其超类的行为,因此只读文档变成了层次结构中的基类。 可写文件现在变成了子类,对基类进行扩展并添加了保存行为。 Document接口

// 把只读文档类作为层次结构中的基类
public interface Document {

    public void open();

}

WritableDocument实现 Document子类

public class WritableDocument implements Document {
    
    @Override
    public void open() {
        
    }
    
    public void save() {
        
    }
}

Project类

public class Project {

    private List<WritableDocument> writableList;
    
    private List<Document> allList;

    public void openAll() {
        for (Document doc:allList) {
            doc.open();
        }
    }

    public void saveAll() {
        for (Document doc:writableList) {
            doc.save(); 
        }
    }
}

总结

通过上面的学习和分析,我们再来看一下里氏替换更具有意义的定义:按照协议来设计,子类在设计的时候,要遵守父类的行为约定/协议 。父类定义了函数的行为约定,那子类可以改变函数的内部实现逻辑,但不能改变函数原有的行为约定。行为约定包括: 函数声明要实现的功能;对输入、输出、异常的约定;注释中所罗列的特殊说明。