前言
世間唯一“不變”的是“變化”本身,這句話同樣適用於軟件設計和開發。
在軟件系統中,模塊(類、方法)應該依賴於抽象,而不應該依賴於實現。
當需求發生“變化”時,如果模塊(類、方法)依賴於具體實現,具體實現也需要修改;
如果模塊(類、方法)依賴於接口,則無需修改現有實現,而是基於接口擴展新的實現。
面向實現?面向接口?
接口可以被復用,但接口的實現卻不一定能被復用。
面向實現編程,意味着軟件的模塊(類、方法)之間的耦合性非常高,每次遭遇“變化”,都會涉及到修改,並且可能是牽一發而動全身的。
每次修改,都需要對原有的代碼重新測試,也可能給舊的代碼引入新的錯誤。
面向接口編程,是為了應對軟件設計和開發中的“變化”,它是一種“以不變應萬變”的思維模式。
只要確保我們的抽象(接口)是不變的,無論需求怎么變化,我們總能通過擴展新的實現自如地應對。
接口是穩定的,關閉的,但接口的實現是可變的,開放的。
開閉原則
“依賴於抽象,而不是具體實現”,它同時也是開閉原則的一種體現。
開閉原則的定義:當軟件需要變化時,盡量通過擴展軟件實體的行為來實現變化,而不是通過修改已有的代碼來實現變化。它是面向對象的基本原則之一。
開閉原則主要有兩個特征:
(1)擴展開放(Open for extension)
(2)修改關閉(Closed for modification)
開閉原則要求模塊(類、方法)應具備良好的擴展性,同時對現有的功能具有一定的保護能力。
開閉原則是一個比較模糊的一個原則,它沒有告訴你如何才能對擴展開放,以及如何才能對修改關閉。
這需要借助我們自身的經驗,以及對需求的理解程度,去分析軟件系統中抽象的部分,識別其中的“變化”和“不變”。
提取接口
“提取接口”是面向對象編程常用的解耦策略,將一些可能發生變化的具體實現提取為接口,將“變化”封裝起來,從而達到依賴接口、而非具體實現的目的。
示例
重構前
以下是一個課程注冊的場景,這段代碼提供了2個類:ClassRegistration和RegistrationProcessor,RegistrationProcessor依賴於ClassRegistration的Create()方法和Total屬性。
public class ClassRegistration { public void Create() { // create registration code } public decimal Total { get; private set; } } public class RegistrationProcessor { public decimal ProcessRegistration(ClassRegistration registration) { registration.Create(); return registration.Total; } }
假如系統的業務發生了變化,ClassRegistration類的Create()方法已經不能滿足新的業務了,我們需要使用另外的注冊方法。
這意味着,我們需要修改ClassRegistration類的Create()方法,且需要讓其同時滿足舊業務和新業務。
RegistrationProcessor依賴於ClassRegistration,既然ClassRegistration存在着諸多變數,我們可以使用“提取接口”的重構策略,讓RegistrationProcessor依賴於某個接口。
重構后
重構后,RegistrationProcessor依賴於IClassRegistration接口,RegistrationProcessor不必去關心IClassRegistration的具體實現是什么。
新的業務要求不同的Create()方式時,我們無需更改現有的ClassRegistration,而是添加新class並實現IClassRegistration接口。
另外,在不同場景下,舊業務和新業務的實現可能在不同場景下被使用。
這時,我們可以借助IoC框架將IClassRegistration接口的實例注入到指定場景。
public interface IClassRegistration { void Create(); decimal Total { get; } } public class ClassRegistration : IClassRegistration { public void Create() { // create registration code } public decimal Total { get; private set; } } public class RegistrationProcessor { public decimal ProcessRegistration(IClassRegistration registration) { registration.Create(); return registration.Total; } }
最后,下面這幅圖描述了這次重構過程。