分類: 物件導向設計模式摘要

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#實踐

SOLID重點複習-1.OCP開放-封閉法則

SOLID重點複習-1.OCP開放-封閉法則

提出者:Betrand Meyer在19888年提出

OCP(The Open-Closed Principle):開放-封閉原則

如果程式的區塊一旦變動,就會產生一連串的反應,導致相關使用到的模組都必須更動,那麼這個程式碼就具備bad smell

OCP建議我們應該進行重構

 

如果OCP原則應用正確的話,那麼,以後再進行”同樣”的需求變動時,就只需要增加新的程式碼,而不用再調整”已經”正常運行的程式碼

其特徵為2:

1.對於擴展是開放的(open for extension)

2.對修改是封閉的(closed for modification)

如何可以「不變動模組原始碼但改變行為呢?」,OOP讓他能做到的機制就是抽象(Abstract)

建立可以固定能”描述”可能行為的抽象體作為抽象基底類別

 

模組針對抽象體進行操作。透過抽象體的衍生,就可以擴展此模組的行為

不遵循OCP的簡單設計的程式如下,Client直接使用了ServerImplementA的類別方法。

using System;
					
public class Program
{
	public class Client
	{
		public void run()
		{
			var server = new ServerImplementA();
			server.Start();
		}
	}
	
	public class ServerImplementA
	{
		public ServerImplementA()
		{
			
		}
		public void Start()
		{
			Console.Write("ServerImplement A Running");
		}
	}
	
	
	public static void Main()
	{
		new Client().run();
	}
}

註:這種直接透過實作達成目的,雖然沒有滿足ocp的要求,不是oop的設計原則,但是並不一定是不好的哦。

因為能解決問題的程式就是好程式,若這樣的程式需求變動可能性低或目的性明確的話,或許這種程序式的程式設計風格(Procedural programming Style)反而意圖更明確好懂呢。

此例中:Client使用了Implement A的類別,這是一種既不開放又不封閉的Client

 

若遵循OCP來做設計,使用策略模式(Strategy)可以重構他

using System;
					
public class Program
{
	public class Client
	{
		public void run(string type)
		{
			ClientInterface client = null;
			if (type=="A")
			  client = new ServerImplementA();
			if (type=="B")
			  client = new ServerImplementB();
			
			if (client!=null)
				client.Start();
		}
	}
	
	
	interface ClientInterface
	{
		void Start();
	}
	
	
	public class ServerImplementA:ClientInterface
	{
		public ServerImplementA()
		{
		}
		
		public void Start()
		{
			Console.WriteLine("ServerImplement A Running");
		}
	}
	
	public class ServerImplementB:ClientInterface
	{
		public ServerImplementB()
		{
		}
		
		public void Start()
		{
			Console.WriteLine("ServerImplement B Running");
		}
	}
	
	
	public static void Main()
	{
		Client client = new Client();
		client.run("A");
		client.run("B");
	}
}

我們透過封裝Client的固定行為”Run”,拉到抽象體(策略模式的抽象載體為interface)

註:書中的案例發生了Client若使用了Server的程式時,抽象體為什麼要叫ClientInterface呢?

作者提到:抽象類別和它們的客戶的關係要比實作它們的類別關係更密切一點

 

那麼,當我們要擴充B的實作的時候,只需要針對B去”增加”實作,然後Client的使用端,不用再去改到使用行為,當然策略模式使用interface來解決抽象體

那麼簽章異動時(例如方法,參數),那麼仍然是會面對到策略模式帶來的副作用!

 

另一種樣版方法模式(Template Method)

using System;
					
public class Program
{
	public class Client
	{
		public void run(string type)
		{
			ServerInterface client = null;
			//封閉的地方
			if (type=="A")
			  client = new ServerImplementA();
			//開放的地方
			if (type=="B")
			  client = new ServerImplementB();
		}
	}
	
	
	public abstract class ServerInterface
	{
		public ServerInterface()
		{
			this.Start();
		}
		
		public abstract void Start();
	}
	
	//不變的地方
	public class ServerImplementA:ServerInterface
	{
		public ServerImplementA()
		{
		}
		
		public override void Start()
		{
			Console.WriteLine("ServerImplement A Running");
		}
	}
	
	//開放的地方
	public class ServerImplementB:ServerInterface
	{
		public ServerImplementB()
		{
		}
		
		public override void Start()
		{
			Console.WriteLine("ServerImplement B Running");
		}
	}
	
	
	public static void Main()
	{
		//不變的地方
		Client client = new Client();
		
		//開放的地方
		client.run("A");
		client.run("B");
	}
}

透過抽象類別的實作,其實針對使用端來說,差不多差不多,都是為了抽象化,讓Client使用上足以一致性

以我抽象類別實作的好處是你可以將部分共同的實作封裝在抽象類別中!讓重覆的行為更被集中!以上面的例子,就是將Start的 行為抽到Constructor去

而抽象體使用介面或是抽象類別來達成哪個好?以結果論則是視使用上是否足夠抽象來決定

 

這兩個模式是滿足OCP最常用的方法

 

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