標籤:
第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替換原則