第10章 LSP:Liskov替换原则   

  Liskov替换原则:子类型(subtype)必须能够替换掉它们的基类型(base type)。

10.1 违反LSP的情形

10.1.1 简单例子

  对LSP的违反导致了OCP的违反:

struct Point { double x, y;}
public enum ShapeType { square, circle };
public class Shape
{
private ShapeType type;
public Shape(ShapeType t) { type = t; }
public static void DrawShape(Shape s)
{
if (s.type == ShapeType.square)
(s as Square).Draw();
else if (s.type == ShapeType.circle)
(s as Circle).Draw();
}
}
public class Circle : Shape
{
private Point center;
private double radius;
public Circle() : base(ShapeType.circle) { }
public void Draw() {/* draws the circle */}
}
public class Square : Shape
{
private Point topLeft;
private double side;
public Square() : base(ShapeType.square) { }
public void Draw() {/* draws the square */}
}

  很显然DrawShape函数违反了OCP。它必须知道Shape类每个可能的派生类,并且每次创建一个Shape类派生出的新类时都必须要更改它。

10.1.2 更微妙的违反情形

  下面是一个Rectangle类型:

public class Rectangle
{
private Point topLeft;
private double width;
private double height;
public double Width
{
get { return width; }
set { width = value; }
}
public double Height
{
get { return height; }
set { height = value; }
}
}

  某一天,用户要求添加正方形的功能。

  我们经常说继承是IS-A(是一个)关系。从一般意义上讲,一个正方形就是一个矩形。因此把Square类视为从Rectangle类派生是合乎逻辑的。不过,这种想法会带来一些微妙但几位值得重视的问题。一般来说,这些问题是很难遇见的,直到我们编写代码时才会发现。

  Square类并不同时需要height和width。但是Square仍会从Rectangle中继承它们。显然这是浪费。假设我们不十分关心内存效率。写出如下自相容的Rectangle类和Square类代码:

public class Rectangle
{
private Point topLeft;
private double width;
private double height;
public virtual double Width
{
get { return width; }
set { width = value; }
}
public virtual double Height
{
get { return height; }
set { height = value; }
}
}
public class Square : Rectangle
{
public override double Width
{
set
{
base.Width = value;
base.Height = value;
}
}
public override double Height
{
set
{
base.Height = value;
base.Width = value;
}
}
}

真正的问题

  现在Square和Rectangle看起来都能够工作。这样看起来该设计似乎是自相容的、正确的。可是,这个结论是错误的。一个自相容的设计未必就和所有的用户程序相容。考虑如下函数:

    void g(Rectangle r)
{
r.Width = ;
r.Height = ;
if (r.Area() != )
throw new Exception("Bad area!");
}

  对于Rectangle来说,此函数运行正确,但是,如果传递进来的是Square对象就会抛出异常。所有,真正的问题是:函数g的编写者假设改变Rectangle的常不会导致宽的改变。

  显然,改变一个长方形的宽不会影响他的长是的假设是合理的!然而,并不是所有作为Rectangle传递的对象都满足这个假设。函数g对于Square、Rectangle层次结构来说是脆弱的。对于g来说,Square不能替换Rectangle,因此Square和Rectangle之间的关系是违反LSP的。

有效性并非本质属性

  一个模型,如果孤立的看,并不具有真正意义上的有效性。模型的有效性只能通过它的客户程序来表现。因此,像其他原则一样,只预测那些最明显的对于LSP的违反的情况而推迟所有其他的预测,直到出现相关的脆弱性的臭味时,才去处理它们。

ISA是关于行为的

  OOD中IS-A关系是就行为方式而言的,行为方式是可以进行合理假设的,是客户程序所依赖的。

10.2 用提取公共部分的方法代替继承

查看如下代码:

public class Line
{
private Point p1;
private Point p2;
public Line(Point p1, Point p2) { this.p1 = p1; this.p2 = p2; }
public Point P1 { get { return p1; } }
public Point P2 { get { return p2; } }
public double Slope { get {/*code*/} }
public double YIntercept { get {/*code*/} }
public virtual bool IsOn(Point p) {/*code*/}
} public class LineSegment : Line
{
public LineSegment(Point p1, Point p2) : base(p1, p2) { }
public double Length() { get {/*code*/} }
public override bool IsOn(Point p) {/*code*/}
}

  初看,会觉得它们之间自然有继承关系。但是,这两个类还是以微妙的方式违反了LSP。

  Line的使用者可以期望和该Line具有线性线性对应关系的所有点都在该Line上。例如,由YIntercept属性返回的点就是线和轴的交点。由于这个点和线具有线性对应关系,所以Line的使用者可以期望IsOn(YIntercept())==true。然而,对于许多LineSegment的实例,这条声明会失效。

  一个简单的方案可以解决Line和LineSegment的问题,该方案也阐明了一个OOD的重要工具。如果我们可以同时具有Line类和LineSegment类的访问权限,那么可以把这两个类的公共部分提出来一个抽象基类。如下:

public abstract class LinearObject
{
private Point p1;
private Point p2;
public LinearObject(Point p1, Point p2)
{ this.p1 = p1; this.p2 = p2; }
public Point P1 { get { return p1; } }
public Point P2 { get { return p2; } }
public double Slope { get {/*code*/} }
public double YIntercept { get {/*code*/} }
public virtual bool IsOn(Point p) {/*code*/}
} public class Line : LinearObject
{
public Line(Point p1, Point p2) : base(p1, p2) { }
public override bool IsOn(Point p) {/*code*/}
} public class LineSegment : LinearObject
{
public LineSegment(Point p1, Point p2) : base(p1, p2) { }
public double GetLength() {/*code*/}
public override bool IsOn(Point p) {/*code*/}
}

  提取公共部分是一个有效的工具。如果两个类中有一些公共的特性,那么很可能稍后出现的其他类也会要这些特性。例如Ray类:

public class Ray : LinearObject
{
public Ray(Point p1, Point p2) : base(p1, p2) {/*code*/}
public override bool IsOn(Point p) {/*code*/}
}

10.3 启发式规则和习惯用法

  完成的功能少于基类的派生类通常是不能替换其类的,因此就违反了LSP。

  查看如下代码:

public class Base
{
public virtual void f() {/*some code*/}
}
public class Derived : Base
{
public override void f() { }
}

  在Base中实现了函数f。不过,在Derived中,函数f是退化的。也许,Derived的编程者认为函数f在Derived中没有用处。遗憾的是,Base的使用者不知道他们不应该调用f,因此就出现了一个替换违规。

  在退化类中存在退化函数并不总是表示违反了LSP,但是当存在这种情况时,还是值得注意一下的。

10.4 结论  
  OCP是OOD中很多说法的核心。LSP是使OCP成为可能的主要原因之一。
  术语IS-A的含义过于宽泛以至于不能作为子类型的定义。子类型的正确定义是可替换的。

摘自:《敏捷软件开发:原则、模式与实践(C#版)》Robert C.Martin    Micah Martin 著

转载请注明出处:

作者:JesseLZJ
出处:http://jesselzj.cnblogs.com

敏捷软件开发:原则、模式与实践——第10章 LSP:Liskov替换原则的更多相关文章

  1. 《敏捷软件开发-原则、方法与实践》-Robert C. Martin读书笔记(转)

    Review of Agile Software Development: Principles, Patterns, and Practices 本书主要包含4部分内容,这些内容对于今天的软件工程师 ...

  2. 敏捷软件开发_实例2<四>

    敏捷软件开发_实例2 上一章中对薪水支付案例的用例和类做了详细的阐述,在本篇会介绍薪水支付案例包的划分和数据库,UI的设计. 包的划分 一个错误包的划分 为什么这个包是错误的: 如果对classifi ...

  3. 敏捷软件开发:原则、模式与实践——第14章 使用UML

    第14章 使用UML 在探索UML的细节之前,我们应该先讲讲何时以及为何使用它.UML的误用和滥用已经对软件项目造成了太多的危害. 14.1 为什么建模 建模就是为了弄清楚某些东西是否可行.当模型比要 ...

  4. 敏捷软件开发:原则、模式与实践——第12章 ISP:接口隔离原则

    第12章 ISP:接口隔离原则 不应该强迫客户程序依赖并未使用的方法. 这个原则用来处理“胖”接口所存在的缺点.如果类的接口不是内敛的,就表示该类具有“胖”接口.换句话说,类的“胖”接口可以分解成多组 ...

  5. 敏捷软件开发:原则、模式与实践——第8章 SRP:单一职责原则

    第8章 SRP:单一职责原则 一个类应该只有一个发生变化的原因. 8.1 定义职责 在SRP中我们把职责定义为变化的原因.如果你想到多于一个的动机去改变一个类,那么这个类就具有多于一个的职责.同时,我 ...

  6. 【Scrum】-NO.40.EBook.1.Scrum.1.001-【敏捷软件开发:原则、模式与实践】- Scrum

    1.0.0 Summary Tittle:[Scrum]-NO.40.EBook.1.Scrum.1.001-[敏捷软件开发:原则.模式与实践]- Scrum Style:DesignPattern ...

  7. 敏捷软件开发:原则、模式与实践——第13章 写给C#程序员的UML概述

    第13章 写给C#程序员的UML概述 UML包含3类主要的图示.静态图(static diagram)描述了类.对象.数据结构以及它们之间的关系,藉此表现出了软件元素间那些不变的逻辑结构.动态图(dy ...

  8. 敏捷软件开发:原则、模式与实践——第11章 DIP:依赖倒置原则

    第11章 DIP:依赖倒置原则 DIP:依赖倒置原则: a.高层模块不应该依赖于低层模块.二者都应该依赖于抽象. b.抽象不应该依赖于细节.细节应该依赖于抽象. 11.1 层次化 下图展示了一个简单的 ...

  9. 敏捷软件开发:原则、模式与实践——第9章 OCP:开放-封闭原则

    第9章 OCP:开放-封闭原则 软件实体(类.模块.函数等)应该是可以扩展的,但是不可修改. 9.1 OCP概述 遵循开放-封闭原则设计出的模块具有两个主要特征: (1)对于扩展是开放的(open f ...

随机推荐

  1. 【转载】如何使用 gnome-screenshot 截图命令

    转载自:http://www.linuxeden.com/html/softuse/20130726/141753.html 日常工作中,我们经常需要截取屏幕状态来给其他人看.一般情况下,我们能最直接 ...

  2. LeetCode - 44. Wildcard Matching

    44. Wildcard Matching Problem's Link --------------------------------------------------------------- ...

  3. IOS 之 PJSIP 笔记(一) 编译多平台支持的静态库

    好久没有写博客了,这也算是我步入新工作后的第一篇技术博文吧.在进入新公司前,早就有了技术层进入下一个迭代的准备,但很多事情是意想不到的,就像我以 C# 程序员的身份面试入职的,而今却是一个全职的 IO ...

  4. C语言中有bool类型吗?

    之前一直都没有注意到,最近在用C语言写DSP算法时,偶然间发现我函数中定义的bool类型的变量在VC6.0(我主要用它来检查一下语法错误)中编译居然报错了,说是bool类型没有定义.用了这么久的C和C ...

  5. 不可或缺 Windows Native (9) - C 语言: 动态分配内存,链表,位域

    [源码下载] 不可或缺 Windows Native (9) - C 语言: 动态分配内存,链表,位域 作者:webabcd 介绍不可或缺 Windows Native 之 C 语言 动态分配内存 链 ...

  6. sqldbhelper

    using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Data. ...

  7. R语言-妹子被追后的选择分析

    前提假设 妹子们一生中可以遇到100个追求者,追求者的优秀程度符合正态分布: 每个妹子都具备判断并比较追求者优秀程度的能力: 接受或拒绝一个追求者后永远无法后悔. 那么,问题来了 当遇到追求者时,如何 ...

  8. 使用PHP搭建书虫网站

    年前开始了一个新项目,书虫项目的启动,项目组里面我是负责搭建网站的.以前听说过PHP的大名,就是没怎么看过,只能从一无所知开始了. 来自PHP手册的介绍:PHP, 即“PHP: Hypertext P ...

  9. .NET Core Runtime IDentifier (RID) catalog

    转载至:https://docs.microsoft.com/zh-cn/dotnet/articles/core/rid-catalog What are RIDs? RID is short fo ...

  10. [Xamarin.Android] Support Library Tips

    [Xamarin.Android] Support Library Tips Support Library支持内容 Xamarin Support Library每个版本支持.那些组件,可以参考这份 ...