SOLID重點複習-2.LSP Liskov替換原則

SOLID重點複習-2.LSP Liskov替換原則

最早由Barbara Liskov 由1988年提出:

子型態(subtype)必須要能夠替換它們的基底型態

 

最簡單的範例是:

就是在違反LSP的時候,常常也明顯違反OCP,就是在程式執行階段時,進行型別檢查,如以下

using System;
					
public class Program
{
	public struct Point
	{
		public double x;
		public double y;
	}
	
	public class Shape
	{
		public Shape(string shapeName)
		{
			ShapeName = shapeName;
		}
		
		public string ShapeName;
		
		public static void DrawShape(Shape t)
		{
			if (t.ShapeName == "circle")
			{
				((Circle)t).Draw();
			}
		    if (t.ShapeName == "square")
			{
				(t as Square).Draw();
			}
		}
	}
	
	
	
	public class Circle : Shape
	{
		public Point center;
		public double radius;
		
		public Circle(string typeName):base(typeName)
		{
		}
		
		public void Draw()
		{
			Console.WriteLine("this is circle");
		}
	}
	
	public class Square : Shape
	{
		public Point TopLeft;
		public double side;
		public Square(string typeName):base(typeName)
		{
		}
		
		public void Draw()
		{
			Console.WriteLine("this is square");
		}
	}
	
	
	
	public static void Main()
	{
		//註:這邊更好是用列舉的方式指定Name, 強制約定
		Shape a = new Circle("circle");
		Shape b = new Square("square");
		
		Shape.DrawShape(a);
		Shape.DrawShape(b);
	}
}

很顯然,DrawShape違反了OCP,因為每擴充一種shape的時候,這個函式就要一併作出修正

書中提及很多人肯定這種作法是很糟糕設計,但為什麼會促使程式設計師寫出這種函式呢?

主要可能是對於多型的額外開銷(overhead)認為大得難以忍受,他沒有在shape類別中定義抽象方法,以便於抽象draw行為。

然而在現今電腦發展飛速的時代中,這種多型方法呼叫的開銷都是ns等級。其實不必要對此有過度的觀點

此例中,square類別和circle類別無法替換shape類別,就是違反了LSP, 同時又迫使DrawShape違反了OCP,因此LSP的違反也潛在地違反了OCP

 

關鍵的問題:

考慮以下程式

using System;
					
public class Program
{
	public class Rectangle
	{
		protected double width;
		protected double height;
		public virtual double Height
		{
			get
			{
				return height;
			}
			
			set
			{
				height = value;
			}
		}
	
		public virtual double Width
		{
			get
			{
				return width;
			}
			
			set
			{
				width = value;
			}
		}
		
		public double Area()
		{
			return width*height;
		}
	}
	
	public class Square :Rectangle
	{
		public override double Height
		{
			get
			{
				return base.height;
			}
			
			set
			{
				base.width = value;
				base.height = value;
			}
		}
	
		public override double Width
		{
			get
			{
				return base.width;
			}
			
			set
			{
				base.height = value;
				base.width = value;
			}
		}
	}
	
	
	public static void Main()
	{
		Rectangle r = new Square();
		r.Width = 5; 
		r.Height = 4;
		//對正方向設定5再設定4的邊界,行為意圖比較像是拉動邊的長短而己
		Console.WriteLine("square area is "+r.Area());
		
		
	}
}

雖然square跟rectangle自己都可以正常運作,這種我們稱為子類別的自相容(self-consistent)及結果是正確的,可是這個結論是錯的

跑出來的結果:

square area is 16

但一個自相容的程式未必和他所有的使用端程式相容,考慮以下例子:

Rectangle r = new Square();
    var result = CalculateArea(r);

	public static double CalculateArea(Rectangle r)
	{
		r.Width = 4;
		r.Height = 5;
		
		if (r.Area() != 20)
			throw new Exception("bad area");
		
		return r.Area();
	}

其結果反而是:

Run-time exception (line 89): bad area

 

因此關鍵的問題是,撰寫這個CalculateArea的程式,並不知道其傳進來的類別,改變長的時候會導致寬也被改變掉了

因此此函式作的假設,確因為傳入了square,導致發生了錯誤。因此square與rectangle之間的關係是違反LSP的

 

書中認為或許有人會針對這個函式所存在的問題進行爭論,認為此函式的作者不能假設長寬一定是獨立的。

但問題是此函式的作者不會同意這個論點,因為此命名以rectangle為命名作為參數,確實有一些不變性與真理足以說明。

而Rectangle其中一個不變的性質就是長寬可以獨立。此函式的作者可以斷言這個不變性(檢查)。

反而是square的作者違反了不變性。

而有 趣的是他不是違反square的不變性,而是違反了rectangle的不變性

此例告訴我們

一個模型,如果獨立來看,並不具備真正意義上的有效性,有效性只能以客戶端的程式來表現

解答LSP方針

1.OOD指出Is-A關係是從”行為”來判斷

2.基於契約的設計(DBC),在單元測試時指定契約,後面有具體例子時再討論

簡單摘要至此,足以深化LSP的觀點了,先這樣吧~

 

參考書摘-無瑕的程式碼-敏捷完整篇 物件導向原則、設計模式與C#實踐

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *