謹記:設計嚴謹的軟件重要的標准就是需要經的起測試,一個程序好不好被測試,測試發現問題能不能被良好的修復,程序狀況能否被監控,這都有賴於對抽象類和接口的正確使用。
接口和抽象類,是高階面向對象設計的起點。想要學習設計模式,必須有着對抽象類和接口的良好認知,和SOLID的認知,並在日常工作中正確的使用他們。
先簡述一下SOLID的特指的五種原則,優秀的設計模式,都是參考於這五種原則的實現;
SOLID:
SRP: Single Responsibility Principle 單一職責原則
OCP: Open Closed Principle 開閉原則
LSP: Liskov Substitution Principle 里氏替換原則
ISP: Interface Segregation Principle 接口隔離原則
DIP: Dependency Inversion Principle 依賴反轉原則
什么是接口和抽象類:
- 接口和抽象類都是 “軟件工程產物”
- 具體類——抽象類——接口:越來越抽象,內部實現的東西越來越少
- 抽象類是未完全實現邏輯的類(可以有字段和非public成員,他們代表了 “具體邏輯”)
- 抽象類為復用而生:專門作為基類來使用,也具有解耦的功能。
- 抽象類封裝的是確定的,開放的是不確定的,推遲到合適的子類中去實現。
- 接口是完全未實現邏輯的 “類”,(“純虛類”;只有函數成員;成員默認為public且不能為private) 但在新的C#版本中,接口也能擁有屬性,索引,事件,和方法的默認實現。
- 接口為解耦而生:“高內聚,底耦合”,方便單元測試。
- 接口是一個 “協約”,它規定你必須有什么。
- 它們都不能實例化,只能用來聲明變量,引用具體類的實例。
為做基類而生的 “抽象類”與 “開放/關閉原則”: (所有的規則都是為了更好的協作)
我們應該封裝那些不變的,穩定的,確定的而把那些不確定的,有可能改變的成員聲明為抽象成員,並且留給子類去實現。
錯誤的功能封裝實現:違反了開閉原則,每當新增功能就會新增代碼

class Program { static void Main(string[] args) { Vehicle vehicle = new Car(); vehicle.Run("car"); } } class Vehicle { public void Stop() { Console.WriteLine("stopped!"); } public void Fill() { Console.WriteLine("Pay and fill..."); } public void Run(string type) { //這時候又來一輛車 我們又得加代碼了 破壞了 開閉原則 switch (type) { case "car": Console.WriteLine("car is running..."); break; case "truck": Console.WriteLine("truck is running..."); break; default: break; } } } class Car : Vehicle { public void Run() { Console.WriteLine("car is running..."); } } class Truck : Vehicle { public void Run() { Console.WriteLine("truck is running..."); } }
利用多態的機制優化上述代碼:

class Program { static void Main(string[] args) { Vehicle vehicle = new Car(); vehicle.Run(); } } class Vehicle { public void Stop() { Console.WriteLine("stopped!"); } public void Fill() { Console.WriteLine("Pay and fill..."); } public virtual void Run() { Console.WriteLine("vehicle is running..."); } } class Car : Vehicle { public override void Run() { Console.WriteLine("car is running..."); } } class Truck : Vehicle { public override void Run() { Console.WriteLine("truck is running..."); } }
我們可以發現vehicle的Run方法,我們基本上很少能用到,那我們去掉它的方法實現,一個虛方法卻沒有函數實現,這不就是純虛方法了嗎? 在C#中,純虛方法的替代類型是abstract。
在現在這個方法中,它封裝了確定的方法,開放了不確定的方法,符合了抽象類的設計,也遵循了開閉/原則。

class Program { static void Main(string[] args) { Vehicle vehicle = new Car(); vehicle.Run(); } } //封裝出了確定的方法 開放了不確定的方法 abstract class Vehicle { public void Stop() { Console.WriteLine("stopped!"); } public void Fill() { Console.WriteLine("Pay and fill..."); } public abstract void Run(); } class Car : Vehicle { public override void Run() { Console.WriteLine("car is running..."); } } class Truck : Vehicle { public override void Run() { Console.WriteLine("truck is running..."); } }
這個時候我們如果想要新增一個類型的汽車,只需要繼承並實現它的抽象方法即可,這更符合開閉/原則。
現在我們讓VehicleBase做為純抽象類,這在C#中是一種常見的模式,我們可以分“代”的來完成開放的不確定方法,讓方法慢慢變得清晰和確定。

class Program { static void Main(string[] args) { Vehicle vehicle = new Car(); vehicle.Run(); } } abstract class VehicleBase { public abstract void Stop(); public abstract void Fill(); public abstract void Run(); } abstract class Vehicle : VehicleBase { public override void Fill() { Console.WriteLine("pay and fill..."); } public override void Stop() { Console.WriteLine("stopped!"); } } class Car : Vehicle { public override void Run() { Console.WriteLine("car is running..."); } } class Truck : Vehicle { public override void Run() { Console.WriteLine("truck is running..."); } }
讓我們在思考下去,純抽象類,不就是接口的默認實現模式嗎,我們將純抽象類改成接口。

class Program { static void Main(string[] args) { Vehicle vehicle = new Car(); vehicle.Run(); } } interface IVehicle { void Stop(); void Fill(); void Run(); } abstract class Vehicle : IVehicle { public void Fill() { Console.WriteLine("pay and fill..."); } public void Stop() { Console.WriteLine("stopped!"); } abstract public void Run(); } class Car : Vehicle { public override void Run() { Console.WriteLine("car is running..."); } } class Truck : Vehicle { public override void Run() { Console.WriteLine("truck is running..."); } }
這是不是就熟悉多了,這是一種良好的設計方法了。
接口是一種契約
他不僅僅約束提供服務方應如何去實現這個服務,例如需要返回一個什么樣的結果。也約束了消費方如何去消費這個服務,例如你應該提供怎么樣的操作。
現在沒有接口,我們要實現來自於兩個消費者的需求,這兩個需求的內部邏輯都是一樣的,給一串數字求和,求平均數,但是因為他們的產品不同,所以入參不同。

static void Main(string[] args) { ArrayList nums1 = new ArrayList() { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; int[] nums2 = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; Console.WriteLine(Sum(nums1)); Console.WriteLine(Avg(nums1)); Console.WriteLine(Sum(nums2)); Console.WriteLine(Avg(nums2)); } static int Sum(int[] nums) { int result = 0; foreach (int x in nums) result += x; return result; } static int Avg(int[] nums) { int result = 0; foreach (int x in nums) result += x; return result / nums.Length; } static int Sum(ArrayList nums) { int result = 0; foreach (int x in nums) result += x; return result; } static int Avg(ArrayList nums) { int result = 0; foreach (int x in nums) result += x; return result / nums.Count; }
但我們程序員觀察了內部的邏輯,發現這兩個參數在內部都使用了foreach,突然靈光一閃,能用foreach實現迭代不就證明他們都實現了IEnumerable接口嗎,於是優化了代碼:

static void Main(string[] args) { ArrayList nums1 = new ArrayList() { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; int[] nums2 = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; Console.WriteLine(Sum(nums1)); Console.WriteLine(Avg(nums1)); Console.WriteLine(Sum(nums2)); Console.WriteLine(Avg(nums2)); } static int Sum(IEnumerable nums) { int result = 0; foreach (int x in nums) result += x; return result; } static int Avg(IEnumerable nums) { int result = 0; int count = 0; foreach (int x in nums) { result += x; count++; } return result / count; }
沒錯,這其實也是多態的一種實現。
接口的解耦
緊耦合:一件需要警惕的事情,現在有這樣一個場景,你負責開發了一個引擎功能,在其他團隊負責的小車功能中,他們需要你的引擎功能作為基礎,想想看這個時候如果你的功能出了錯誤,他們的工作也就白做了,不僅如此,他們工作的進度,也取決於你的開發進度。
我們使用接口來讓耦合變得更松弛,現在我們都在使用手機,但是手機的品牌有很多供我們選擇,但他們都為我們提供了相同的服務,這些服務便可以被接口所決定和約束,你必須要有打電話,接電話,發送短信的功能等等,你才能稱的上是手機。並且我們使用手機不可能只使用這一個手機,我們可能會使用不同的手機,而換手機這個操作也不能讓我們受到影響。

static void Main(string[] args) { var userPhone = new UserPhone(new Xiaomi()); userPhone.Use(); } class UserPhone { private IPhone _phone; public UserPhone(IPhone phone) { _phone = phone; } public void Use() { _phone.Send(); _phone.Call(); _phone.Recived(); } } interface IPhone { void Call(); void Send(); void Recived(); } class Huawei : IPhone { public void Call() { Console.WriteLine("Huawei call"); } public void Recived() { Console.WriteLine("Huawei Recived"); } public void Send() { Console.WriteLine("Huawei Send"); } } class Xiaomi : IPhone { public void Call() { Console.WriteLine("Xiaomi call"); } public void Recived() { Console.WriteLine("Xiaomi Recived"); } public void Send() { Console.WriteLine("Xiaomi Send"); } }
依賴反轉原則
我們在日常編程中,我們慣性的自頂向下的思維,會將大問題分解成一個個小的問題,可能會產生一層層的耦合。
在這個例子中,耦合度很高,driver只能駕駛Car,而不能駕駛其他汽車。
引入IVehicle接口,將車的類型於Driver解耦,現在Diver可以利用多態來掌控不同類型的汽車。注意箭頭的指向!現在依賴反轉了。
引入抽象類,作為Driver們的基類,它依賴了IVehicle,現在組合方式成了 2 X 2,Driver們和Car耦合大大降低了。
接口提供的易測試性
現在有一個小例子,廠家生產了電風扇,電風扇不同的電流對應了電風扇不同的狀態。我們試着來一步步優化並測試這個方法。

class Program { static void Main(string[] args) { DeskFan fan = new DeskFan(new PowerSupply()); fan.Work(); } class PowerSupply { public int GetPower() => 100; } class DeskFan { private PowerSupply _powerSupply; public DeskFan(PowerSupply powerSupply) { _powerSupply = powerSupply; } public void Work() { int power = _powerSupply.GetPower(); if (power <= 0) { Console.WriteLine("Don't Work"); } else if (power < 100) { Console.WriteLine("Low"); } else if(power < 200) { Console.WriteLine("Work Fine"); } else { Console.WriteLine("Warning"); } } } }
引入接口解耦合:

class Program { static void Main(string[] args) { DeskFan fan = new DeskFan(new PowerSupply()); fan.Work(); } interface IPowerSupply { int GetPower(); } class PowerSupply : IPowerSupply { public int GetPower() => 100; } class DeskFan { private IPowerSupply _powerSupply; public DeskFan(IPowerSupply powerSupply) { _powerSupply = powerSupply; } public void Work() { int power = _powerSupply.GetPower(); if (power <= 0) { Console.WriteLine("Don't Work"); } else if (power < 100) { Console.WriteLine("Low"); } else if(power < 200) { Console.WriteLine("Work Fine"); } else { Console.WriteLine("Warning"); } } } }
接下來我們寫了一個xUnit測試項目,對現有接口進行測試。

public class DeskFanTest { [Fact] public void PowerSupplyThenZero_Ok() { var fan = new DeskFan(new PowerSupplyThenZero()); var expected = "Work Fine"; var actual = fan.Work(); Assert.Equal(expected, actual); } [Fact] public void PowerSupplylessThen200_Bad() { var fan = new DeskFan(new PowerSupplyThen200()); var expected = "Work Fine"; var actual = fan.Work(); Assert.Equal(expected, actual); } } class PowerSupplyThenZero : IPowerSupply { public int GetPower() { return 140; } } class PowerSupplyThen200 : IPowerSupply { public int GetPower() { return 210; } } }
但我們這個測試項目其實還是有問題的,我們的測試用例都需要一個個來創建,比較麻煩,我們這里引入Mock。

public class DeskFanTest { [Fact] public void PowerSupplyThenZero_Ok() { var mockPower = new Mock<IPowerSupply>(); mockPower.Setup(s=>s.GetPower()).Returns(100); var fan = new DeskFan(mockPower.Object); var expected = "Work Fine"; var actual = fan.Work(); Assert.Equal(expected, actual); } [Fact] public void PowerSupplylessThen200_Bad() { var mockPower = new Mock<IPowerSupply>(); mockPower.Setup(s => s.GetPower()).Returns(300); var fan = new DeskFan(mockPower.Object); var expected = "Warning"; var actual = fan.Work(); Assert.Equal(expected, actual); } }
具有良好的測試性對於一個持續集成的環境也是有很大幫助的,每當我們 check in 代碼時,持續集成工具都會先 run一遍測試項目,如果這一次的代碼 check in 使之前的測試項目不能通過了,那這一次 check in 則是失敗的。
接口的D/I原則
我們說到接口是服務提供方和服務消費方之間的協議,那現在服務提供方提供了消費方在規定協議中的功能接口,但消費方對功能的需求的軟性需求,則有可能服務提供方還多提供了服務,但這些服務可能永遠都沒有被用到,那么這個接口就太胖了,造成這種原因可能是一個接口中包含了多個其他場景的功能,那么它的設計違反了接口隔離原則的。而實現了這個接口的類也違反了單一職責原則。
以下的這個例子:聲明了兩個接口IVehicle和ITank,但是呢這兩個接口中都封裝了相同的Run方法,並且ITank的Fire方法也未曾被使用過(並且Fire應該是屬於武器部分),還有呢就是我們如果想讓Driver開坦克,就必須要該代碼了。所以這個接口可以被重新隔離。

public class Program { public static void Main(string[] args) { Driver driver= new Driver(new Car()); driver.Run(); } class Driver { private readonly IVehicle _vehicle; public Driver(IVehicle vehicle) { _vehicle = vehicle; } public void Run() { _vehicle.Run(); } } interface IVehicle { void Run(); } class Car : IVehicle { public void Run() { Console.WriteLine("car is running..."); } } class Truck : IVehicle { public void Run() { Console.WriteLine("truck is running..."); } } interface ITank { void Run(); void Fire(); } class SmallTank : ITank { public void Fire() { Console.WriteLine("small boom!!"); } public void Run() { Console.WriteLine("small tank is running..."); } } class BigTank : ITank { public void Fire() { Console.WriteLine("big boom!!"); } public void Run() { Console.WriteLine("big tank is running..."); } } }
我們引入IWeapon接口來隔離Fire和Run,並且讓ITank也遵守IVehicle的規定,那么Driver就能把Tank當Car開了。

public class Program { public static void Main(string[] args) { Driver tankDriver= new Driver(new BigTank()); tankDriver.Run(); Driver carDriver = new Driver(new Car()); carDriver.Run(); } class Driver { private readonly IVehicle _vehicle; public Driver(IVehicle vehicle) { _vehicle = vehicle; } public void Run() { _vehicle.Run(); } } interface IVehicle { void Run(); } class Car : IVehicle { public void Run() { Console.WriteLine("car is running..."); } } class Truck : IVehicle { public void Run() { Console.WriteLine("truck is running..."); } } interface IWeapon { void Fire(); } interface ITank : IVehicle, IWeapon { } class SmallTank : ITank { public void Fire() { Console.WriteLine("small boom!!"); } public void Run() { Console.WriteLine("small tank is running..."); } } class BigTank : ITank { public void Fire() { Console.WriteLine("big boom!!"); } public void Run() { Console.WriteLine("big tank is running..."); } } }
我們用另一個例子來理解 “合適的” 接口,還記得上面我們數組元素相加的例子嗎,我們想要的功能其實就是對int[]類型的元素進行迭代累加而已,不需要其他的功能,所以我們使用的不是IList或者ICollection這些 “胖接口” 因為這些里面的功能我們都用不到,所以我們提供IEnumerable接口即可。這更符合接口隔離原則,太胖的接口還會使得阻礙我們服務的提供。

public class Program { public static void Main(string[] args) { int[] nums = new int[] { 1, 2, 3, 4, 5 }; var roc = new ReadOnlyCollection(nums); var result1 = Sum(nums); Console.WriteLine(result1); // 15 //會出錯 因為ICollection是一個胖接口 //我們只需要使用迭代功能即可 胖接口擋住了我們合格的服務 var result2 = Sum(roc); var result3 = Sum1(roc); Console.WriteLine(result3); //15 } static int Sum(ICollection collection) { int result = 0; foreach (var item in collection) { result += (int)item; } return result; } static int Sum1(IEnumerable collection) { int result = 0; foreach (var item in collection) { result += (int)item; } return result; } } class ReadOnlyCollection : IEnumerable { private readonly int[] _array; public ReadOnlyCollection(int[] array) { _array = array; } public IEnumerator GetEnumerator() { return new Enumerator(this); } public class Enumerator : IEnumerator { private readonly ReadOnlyCollection _readOnlyCollection; private int _head; public Enumerator(ReadOnlyCollection readOnlyCollection) { _readOnlyCollection = readOnlyCollection; _head = -1; } public bool MoveNext() { return ++_head < _readOnlyCollection._array.Length; } public void Reset() { _head = -1; } public object Current { get { object o = _readOnlyCollection._array[_head]; return o; } } } }
對接口的進一步隔離還可以使用顯示接口:現在有一個這樣的例子,有一個男孩其實是一個超級英雄,他白天在學校里當學生,晚上懲奸除惡還不想被人知道。

public class Program { public static void Main(string[] args) { //student只能訪問到learn方法 var student = new Boy(); student.Learn(); //想要調用KillBadGuy 你需要讓他變成英雄 IHero hero = student; hero.KillBadGuy(); } public interface IStudent { void Learn(); } public interface IHero { void KillBadGuy(); } public class Boy : IStudent, IHero { public void Learn() { Console.WriteLine("I am a Student Keep learn"); } void IHero.KillBadGuy() { Console.WriteLine("kill the bad guy..."); } } }