敏捷式軟體開發 (Agile Software Development):原則、模式與實踐——第10章 LSP:Liskov替換原則

來源:互聯網
上載者:User

標籤:

第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 = 5;        r.Height = 4;        if (r.Area() != 20)            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的含義過於寬泛以至於不能作為子類型的定義。子類型的正確定義是可替換的。

 

 

摘自:《敏捷式軟體開發 (Agile Software Development):原則、模式與實踐(C#版)》Robert C.Martin    Micah Martin 著

轉載請註明出處:

JesseLZJ
出處:http://jesselzj.cnblogs.com

敏捷式軟體開發 (Agile Software Development):原則、模式與實踐——第10章 LSP:Liskov替換原則

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.