读懂 SOLID 的「里氏替换」原则

这是理解 SOLID原则,关于 里氏替换原则为何提倡咱们面向抽象层编程而不是具体实现层,以及为何这样可使代码更具维护性和复用性。

什么是里氏替换原则

Objects should be replaceable with instances of their subtypes without altering the correctness of that program.

某个对象实例的子类实例应当能够在不影响程序正确性的基础上替换它们。javascript

这句话的意思是说,当咱们在传递一个父抽象的子类型时,你须要保证你不会修改任何关于这个父抽象的行为和状态语义。前端

若是你不遵循里氏替换原则,那么你可能会面临如下问题:java

  • 类继承会变得很混乱,所以奇怪的行为会发生
  • 对于父类的单元测试对于子类是无效的,所以会下降代码的可测试性和验证程度

一般打破这条原则的状况发生在修改父类中在其余方法中使用的,与当前子类无关联的内部或者私有变量。这一般算得上是一种对于类自己的一次潜在攻击,并且这种攻击多是你在不经意间本身发起的,并且不只在子类中。程序员

反面例子

让咱们经过一个反面例子来演示这种修改行为和它所产生的后果。好比,咱们有一个关于Store的抽象类和它的实现类BasicStore,这个类会储存一些消息在内存中,直到储存的个数超过每一个上限。客户端代码的实现也很简单明了,它指望经过调用retrieveMessages就能够获取到全部储存的消息。typescript

代码以下:编程

interface Store {
    store(message: string);
    retrieveMessages(): string[];
}

const STORE_LIMIT = 5;

class BasicStore implements Store {
   protected stash: string[] = [];
   protected storeLimit: number = STORE_LIMIT;
  
   store(message: string) {
     if (this.storeLimit === this.stash.length) {
         this.makeMoreRoomForStore();
      }
      this.stash.push(message);
    }
  
    retrieveMessages(): string[] {
      return this.stash;
    }

    makeMoreRoomForStore(): void {
       this.storeLimit += 5;
    }
}

以后经过继承BasicStore,咱们又建立了一个新的RotatingStore实现类,以下:后端

class RotatingStore extends BasicStore {
    makeMoreRoomForStore() {
        this.stash = this.stash.slice(1);
    }
}

注意RotatingStore中覆盖父类makeMoreRoomForStore方法的代码以及它是如何隐蔽地改变了父类BasicStore关于stash的状态语义的。它不只修改了stash变量,还销毁了在程序进程中已储存的消息已为未来的消息提供额外的空间。前端框架

在使用RotatingStore的过程当中,咱们会遇到一些奇怪的现象,这正式因为RotatingStore自己产生的,以下:框架

const st: Store = new RotatingStore()

st.store("hello")
st.store("world")
st.store("how")
st.store("are")
st.store("you")
st.store("today")
st.store("sir?")

st.retrieveMessages() // 一些消息丢失了

一些消息会无端消失,当前这个类的表现逻辑与全部消息都可以被取出的基本需求不一致。编程语言

如何实践里氏替换原则

为了不这种奇怪现象的发生,里氏替换原则推荐咱们经过在子类中调用父类的公有方法来获取一些内部状态变量,而不是直接使用它。这样咱们就能够保证父类抽象中正确的状态语义,从而避免了反作用和非法的状态转变。

它也推荐咱们应当尽量的使基本抽象保持简单和最小化,由于对于子类来讲,有助于提供父类的扩展性。若是一个父类是比较复杂的,那么子类在覆盖它的时候,在不影响父类状态语义的状况下进行扩展绝非易事。

对于内部系统作可行的后置条件检查也是一个不错的方式,这种检查一般会验证是否子类会搅乱一些关键代码的运行路径(译者注:也能够理解为状态语义),可是我自己对这个实践并无太多的经验,因此没法给予具体的例子。

代码评论也能够必定程度上给予好的帮助。当你在开发一些你可能无心间作出一些对已有系统的破坏,可是你的同事可能会很容易地发现这些(当局者迷旁观者清)。软件设计保持一致性是一件十分重要的事情,所以应当尽早、尽量多地查明那些对对象继承链做出潜在修改的代码。

最后,在单一职责原则中,咱们曾说起,考虑使用组合模式来替换继承模式

总结

正如你所看到的,在开发软件时,咱们每每须要额外花一些努力和精力来使它变得更好。将这些原则牢记于心,理解它们所存在的意义以及它们想要解决的问题,这样会使你的工做变得更加容易、更具条理性,可是同时记住,这并非一件容易的事,相反,你应当在构思软件时,花至关多的事件思考如何更好地实践这些原则。

试着让本身设计的软件系统具有可适应性,这种适应性能够抵御各类不利的变化以及潜在的错误,这样天然而然地可使你少加班和早回家(译者注:看来加班是每一个程序员都要面临的问题啊)

译者注

这是SOLID原则中我所接触和了解较少的一个原则,但通过仔细思考后,发现其实咱们仍是常常会在实际工做中运用它的。

在许多面向相对的编程语言中,关于对象的继承机制中,都会提供一些内部变量和状态的修饰符,好比public(公有)protect(保护)private(私有),关于这些修饰符自己的异同这里再也不赘述,我想说的是,这些修饰符存在必然有它存在的意义,必定要在实际工做中,使用它们。以前作java后端时,常常在公司的项目的历史代码中发现,不多使用protectprivate对类内部的方法和变量作约束,可见当时的编写者并无对类自己的职能有一个清晰的认识,又或者是随着时间一步步迭代出来的结果。

那么问题来了,一些静态语言有这些修饰符,可是像javascript这种鸭子类型语言怎么办呢?其实没有必要担忧,最先开始学前端的时候,这个问题我就问过本身无数次,javascript虽然没有这些修饰符,可是咱们能够经过别的方式来达到相似的效果,或者使用typescript

除了在编程语言层面,在前端实际工做中,你可能会听到一个叫做immutable的概念,这个概念我认为也是里氏替换原则的一直延伸。由于当前的前端框架通常提倡的理念均是f(state) => view,即数据状态表明视图,而数据状态自己因为javascript动态语言的特性,很容易会在不经意间被修改,一旦存在这种修改,视图中便会产生一些意想不到的问题,所以immutable函数式的概念才会在前段时间火起来。

写在最后

通过这五篇文章,咱们来分别总结一下这五条基本原则以及它们带来的好处:

  • 单一职责原则:提升代码实现层的内聚度,下降实现单元彼此之间的耦合度
  • 开闭原则:提升代码实现层的可扩展性,提升面临改变的可适应性,下降修改代码的冗余度
  • 里氏替换原则:提升代码抽象层的可维护性,提升实现层代码与抽象层的一致性
  • 接口隔离原则:提升代码抽象层的内聚度,下降代码实现层与抽象层的耦合度,下降代码实现层的冗余度
  • 依赖倒置原则:下降代码实现层由依赖关系产生的耦合度,提升代码实现层的可测试性

能够注意到我这里刻意使用了下降/提升 + 实现层/抽象层 + 特性/程度(耦合度、内聚度、扩展性、冗余度、可维护性,可测试性)这样的句式,之因此这么作是由于在软件工做中,咱们理想中的软件应当具有的特色是, 高内聚、低耦合、可扩展、少冗余、可维护、易于测试,而这五个原则也按正确的方向,将咱们的软件系统向咱们理想中的标准推动。

为了便于对比,特别绘制了下面的表格,但愿你们从真正意义上作到将这些原则牢记于心,并付诸于行。

原则 耦合度 内聚度 扩展性 冗余度 维护性 测试性 适应性 一致性
单一职责原则 - + o o + + o o
开闭原则 o o + - + o + o
里氏替换原则 - o o o + o o +
接口隔离原则 - + o - o o + o
依赖倒置原则 - o o - o + + o

Note: +表明增长, -表明下降, o表明持平

关注公众号 全栈101,只谈技术,不谈人生

clipboard.png

相关文章
相关标签/搜索