第10章 Liskov 替换原则

1. 目的
OCP原则背后的主要机制是抽象和多态。支持抽象和多态的关键机制之一是继承。只有良好的继承,才能使设计符合OCP。那么,什么才是良好的继承?LSP原则提供了一个判断标准。

 

2. 什么是LSP
Liskov Substitution Principle。Liskov是提出这个原则的人的名字。

 

LSP的定义是这样的:子类型必须能够替换掉它们的基类型。

 

替换性质是这样的:若对每个类型S的对象o1,都存在一个类型T的对象o2,使得在所有针对T编写的程序P中,用o1替换o2后,程序P行为功能不变,则S是T的子类型。

 

例如:有类B继承类A。
 


对于任意函数f(A &a),以类A的引用为参数。如果以类B引用做参数传给f,f的动作结果仍然与预期一致,则B可以替换A。

 

3. 一个明显的例子
 


 
 
void Draw(const Shape &shape)
{
 switch(shape.m_iType)
 {
 case CIRCLE:
  static_cast<const Circle&>(shape).Draw();
  break;
 case SQUARE:
  static_cast<const Square&>(shape).Draw();
  break;
 default:
  break;
 }
}

    这个继承关系是不符合LSP的。如果增加了新的Shape子类,则不能替换Shape,因为Draw函数中需要判断具体的子类类型,必须添加代码才行。同时,这个继承关系也不符合OCP原则。

    我们可以这样修改:
 

 
4. 一个隐晦的例子
上面的例子是比较明显的,很容易发现。还有比较微妙的违规:
 

 
因为Square的长和宽是相等的,所以SetWidth和SetHeight这样实现:


void Square::SetWidth(int width)
{
 Rectangle::SetWidth(width);
 Rectangle::SetHeight(width);
}

void Square::SetHeight(int height)
{
 Rectangle::SetWidth(height);
 Rectangle::SetHeight(height);
}

 

现在有这样一个函数f:
bool f(Rectangle &rect, int width, int height)
{
 rect.SetWidth(wiedht);
 rect.SetHeight(height);
 int area = rect.GetWidth() * rect.GetHeight();
 if (width * height == area)
 {
  return true;
 }
 else
 {
  return false;
 }
}


    函数f设定了Rectangle的长和宽,并期望area是设定的长和宽之积。如果此处传入一个Square引用做参数,则f可能返回false,这和f作者的期望不一致。所以,Square不能替换Rectangle,违反了LSP。

 

 

 

    如果只是观察Rectangle和Square,它们的继承关系,以及SetWidth和SetHeight的实现都是合理的,但是为什么从函数f的角度看就不合理了呢?

    记住:考察一个设计是否合理,要放到具体的环境中去,单独的考察一个设计是没有意义的。

    这里,Rectangle和Square违反LSP的原因,是因为Square的作者和函数f的作者对于Width和Height能否独立变化有歧义。怎么避免这种歧义?

 

5. 基于契约设计(Design By Contract,DBC)
    使用DBC,类的编写者显示地规定针对该类的契约。客户代码的编写者可以通过该契约获悉可以以来的行为方式。契约是通过为每个方法声明的前置条件和后置条件来指定的。要使用一个方法,前置条件必须为真。方法执行完毕,要保证后置条件为真。

基于契约设计的规则是:派生类的前置条件与基类相等或更弱,后置条件与基类相等或更强。
    更弱的前置条件,保证在能使用基类的地方也能使用派生类;更强的后置条件,保证派生类的运行结果在基类的运行结果范围之内。

    可惜的是,C++和Java并没有支持前置条件和后置条件的机制。

 

6. 在单元测试中指定契约
    可以通过编写单元测试的方式来指定契约。客户查看这些单元测试,这样就知道他们能对这个类做什么样的假设。

 

7. 更强的抽象
    f的作者之所以会假设Rectangle的长和宽可以单独变化,是因为他们看到了Rectangle的SetWidth和SetHeight实现。如果SetWidth和SetHeight是纯虚的,就没有办法对这两个函数做假设了。我们可以做一个更强的抽象:
 

 
8. 结论
    我们经常通过继承和多态实现可扩展性。但是具体是否有可扩展性,需要运用LSP原则去判断。使用基类的地方,如果给它一个派生类,代码是否依然正常运行。如果答案是Yes,那么就是符合LSP的,是可扩展的;否则,就违反了LSP,需要进行重构。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值