前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >里氏替换原则(LSP)

里氏替换原则(LSP)

作者头像
河边一枝柳
发布2022-06-21 15:35:00
5980
发布2022-06-21 15:35:00
举报

里氏替换原则(英文名为Liskov substitution principle,简称LSP)是由Barbara Liskov在1988年提出的,在Robert C. Martin提出的SOLID软件设计原则中的第三个字母L

挑出一个相对比较容易明白的定义:

if S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program.

简单来说,就是在使用基类的地方,如果全部换成子类的对象,也并不会产生不期望的结果。

面向对象有三个基本特性:封装、继承和多态。本人认为,里氏替换原则是在指导工程师更好地去实现继承。继承可以让子类获得父类的方法和属性,那么子类也就和父类有一定的耦合性,比如一个父类的方法bool func(), 子类重写父类这个方法或者调用者调用继承于父类的这个方法产生了与这个父类方法不同的期望,我们可以称之为,不适合的继承。里氏替换原则,从语义上要保证不会因为继承而发生变化。

其实这样的定义还是很抽象的,我想应该大多数程序员在写代码中,继承一个类的时候,脑海中应该不是浮现出上述的原则。但原则应该都是基于工程实践反馈,抽象出来的具有指导性的方法。所以本文尽量通过一些例子来和大家探讨一下这个原则。

子类对象替换父类对象

我们在编程的时候,经常都会使用到队列,在做SaaS开发的时候,如果你在AWS平台上开发则可能使用SQS, 如果你在Azure平台上开发可能会使用Service Bus Queue。假设你现在AWS平台开发,但是你在做实现的时候,也需要抽象出队列的接口给调用者使用,因为说不定某一天要切换到Azure平台,这样的抽象接口有利于你进行扩展,在扩展到另一种队列的时候,调用者改动的代码对产品的影响会很小。如下图所示:

不过这不是里氏替换原则指导我们的,而是未来笔者也准备写一篇文章的依赖导致原则指导我们去这么做的。里式替换原则主要讲的是以下这个场景, 那么调用者可能有如下实现代码,这里子类对象替换父类出现的地方,可以从静态角度和动态角度来看:

  • 静态角度: 对于调用者来说,在编写的时候是面向QueueAbstract, 程序的后续操作都是基于父类QueueAbstract的接口方法。
  • 动态角度: 在运行时实际上因为多态原理,实际调用的是子类QueueSQS的方法GetTask,可以认为子类对象在动态上替换了父类

上述例子,目前看是没有违背里氏替换原则,也是我们常见的使用场景。但是这似乎还没有解决一个问题:那就是怎么才叫做违背了里式替换原则,违背了原有的语义呢?

语义的变化

继续以上述例子为例,队列的调用者是面向QueueAbstract去编程的,那么在编程的时候一定要搞清楚一个接口的语义,比如这里的GetTask,语义我们从参数,返回值,行为等几个方面定义,因为我们就以返回值为例: 一般队列都会设定一个指定时间内拿不到数据,函数也会返回,可以在初始化的时候进行配置。假设我们定义: 拿不到数据,GetTask返回nullptr,而不是抛出异常。

但是如果我们继承实现的时候,实现如下, 如果QueueSQS中实现的方法GetTask没有数据则抛出异常,那么和父类的语义就不同了。调用者认为只需要通过判断返回是否为nullptr来决定是否获取到数据,调用者是面向基类编程的,不会去捕获NoDataException这个异常,从而有可能导致程序崩溃。

代码语言:javascript
复制
class QueueSQS : public QueueAbstract {
  std::shared_ptr<Task> GetTask()
  {
    //Get task from SQS
    //.....
    //
    if (!bContainsTaskData)
      throw NoDataException("No Data");
    return task;
  }
};

以上只是返回值的例子,但是只要是其继承后的方法,对于返回值、抛出的异常以及内部属性的变化,都应该属于父类定义的行为的子集。

结束语

当思考这些抽象原则的时候,实话说笔者的大脑也是有些难熬的。写下这些,一是为了能够将自己的想法输出加强印象,也希望让看到的同行一起探讨。

最后留个网上最常见的讨论的例子,这个违背了里氏替换原则吗?:

代码样例:

代码语言:javascript
复制
class Rectangle {
public:
   virtual void SetLength(int size) 
{
     m_Length = size;
   }
   virtual void SetHeight(int size) 
{
     m_Height = size;
   }
protected:
   int m_Length = 0;
   int m_Height = 0;
};

class Square : public Rectangle
{
protected:
   void SetSide(int size)
{
     Rectangle::SetHeight(size);
     Rectangle::SetLength(size);
   }
public:
   virtual void SetLength(int size) 
{
     SetSide(size);
   }
   virtual void SetHeight(int size)
{
     SetSide(size);
   }
}
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2022-05-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 一个程序员的修炼之路 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 子类对象替换父类对象
  • 语义的变化
  • 结束语
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档