1.什麼是開閉原則
開閉原則的英文是Open Closed Principle,縮寫就是OCP。其定義如下:
軟體實體(模組、類、方法等)應該“對擴充套件開放、對修改關閉”。
從定義上看,這個原則主要包含兩部分:
-
對擴充套件開放:“ 這意味著模組的行為是可以擴充套件的。當應用程式的需求改變時,我們可以對其模組進行擴充套件,使其具有滿足那些需求變更的新行為。換句話說,我們可以改變模組的功能。
-
對修改關閉:“ 對模組行為進行擴充套件時,不必改動該模組的原始碼或二進位制程式碼。模組的二進位制可執行版本,無論是可連結的庫、DLL或Java的.jar檔案,都無需改動。
通俗解釋就是,新增一個新的功能,應該通過在已有程式碼(模組、類、方法)的基礎上進行擴充套件來實現,而不是修改已有程式碼。
之前的一篇文章《何謂高質量程式碼?》中,我們總結了高質量程式碼的幾個衡量標準。
而開閉原則解決的就是程式碼的擴充套件性問題。如果某段程式碼在應對未來需求變化的時候,能夠做到“對擴充套件開放、對修改關閉”,那就說明這段程式碼的擴充套件性比較好。
2.如何做到對擴充套件開放、對修改關閉
那麼應該怎樣寫出擴充套件性好的程式碼呢?
在思想上我們要具備擴充套件意識、抽象意識、封裝意識。這些意識的培養要比一些具體的方法更為重要,這依賴我們對物件導向的理解、對業務的掌握度,以及長期的經驗積累...... 這要求我們在寫程式碼的時候後,要多花點時間往前多思考一下,未來可能有哪些需求變更,識別出程式碼的易變部分與不易變部分,合理設計程式碼結構,事先留好擴充套件點,以便在未來不需要改動程式碼整體結構、做到最小程式碼改動的情況下,新的程式碼能夠很靈活地插入到擴充套件點上。
在方法上,我們主要可以通過多型、依賴注入、面向介面程式設計等方式來實現程式碼的可擴充套件性。做到“對擴充套件開放、對修改關閉”。我們要將可變部分抽象出來以隔離變化,提供抽象化的不可變介面,給上層系統使用。當具體的實現發生變化的時候,我們只需要基於相同的抽象介面,擴充套件一個新的實現,替換掉老的實現即可,上游系統的程式碼幾乎不需要修改。
比如,我們的專案中通常會用到一些第三方元件,訊息中介軟體,快取中介軟體......訊息中介軟體我們可能一開始使用RabbitMQ
,但是可能後來會換成Kafka
,快取中介軟體可能會從Memcache
換成Redis
。這種情況,如果我們的上層應用直接依賴這些中介軟體呼叫程式碼,那麼更換的成本就會更高,這種程式碼就不利於擴充套件。
public class MemcacheClient {
public boolean set(String key, String value) {
return false;
}
public String get(String key) {
return null;
}
public boolean remove(String key) {
return false;
}
}
public class OcpApplication {
public void test() {
// 業務程式碼
//...
//...
//寫快取
MemcacheClient client = new MemcacheClient();
client.set("testKey", "testValue");
}
}
如上示例,我們的上層應用OcpApplication
直接依賴了MemcacheClient
,如果未來有需要把Memcache
換成Redis
,我們就需要替換掉所有呼叫了MemcacheClient
的上層應用方法,這嚴重違背了開閉原則。
在這種情況下,通常我們會把這種中介軟體的呼叫設計成可插拔的。我們提供一個這些中介軟體的抽象介面出來,讓所有上層系統都依賴這組抽象的介面程式設計,並且通過依賴注入的方式來呼叫。當我們要替換新的中介軟體的時候,比如將 Memcache
替換成 Redis
,就可以可以很方便地拔掉老的Memecache
實現,插入新的Redis
實現。
/**
* 快取中介軟體的使用抽象出介面
*/
public interface ICacheClient {
boolean set(String key, String value);
String get(String key);
boolean remove(String key);
}
/**
* MemcacheClient
*/
public class MemcacheClient implements ICacheClient {
public boolean set(String key, String value) {
return false;
}
public String get(String key) {
return null;
}
public boolean remove(String key) {
return false;
}
}
/**
* RedisClient
*/
public class RedisClient implements ICacheClient {
@Override
public boolean set(String key, String value) {
return false;
}
@Override
public String get(String key) {
return null;
}
@Override
public boolean remove(String key) {
return false;
}
}
public class OcpApplication {
/**
* 依賴注入cacheClient
*/
ICacheClient cacheClient;
public OcpApplication(ICacheClient cacheClient){
this.cacheClient=cacheClient;
}
public void test() {
// 業務程式碼
//...
//...
//寫快取
cacheClient.set("testKey", "testValue");
}
}
3.如何靈活運用開閉原則
開閉原則看似簡單,但我卻認為是SOLID 中最難掌握的一條原則。其難點就在於如何在真正的專案中去靈活運動開閉原則。而且OCP同樣存在著一些陷阱,怎麼才算滿足或違反開閉原則,修改程式碼就一定意味著違反開閉原則嗎,擴充套件點設計的越多越好嗎......
3.1 靈活設計擴充套件點
對於業務系統,要想識別出盡可能多的擴充套件點,就要求你對業務有足夠的瞭解,能夠預見一些未來可能的變化。
對於偏技術的系統,比如,框架、元件、類庫等,就需要充分了解它的使用場景?以及今後想要擴充套件點功能?使用者未來會有哪些更多的訴求......
但即便我們對業務、對系統有足夠的瞭解,也不可能識別出所有的擴充套件點,即便可以,併為這些地方都預留擴充套件點,也是沒有必要的。同樣有一條原則叫KISS原則,那就是儘量保持簡單,不要進行過度設計,實際上很多人都會陷入這樣一個誤區,我們常常為了一些很可能不存在的擴充套件而絞盡腦汁!
最合理的做法就是,對於一些比較確定的、短期內可能就會擴充套件,或者需求改動對程式碼結構影響比較大的情況,或者實現成本不高的擴充套件點,可以事先做些擴充套件性設計。但對於一些不確定未來是否要支援的需求,或者實現起來比較複雜的擴充套件點,我們可以等到有需求驅動的時候,再通過重構程式碼的方式來支援擴充套件的需求。
3.2 修改程式碼一定違反開閉原則嗎
開閉原則中對於修改是封閉的
並非是一個絕對的概念。
1.修復缺陷所做的改動
缺陷在軟體中很常見,是不可能完全消除的。當缺陷出現時,就需要我們修復現有的程式碼。軟體修復明顯傾向於實用主義而不是堅持開放封閉原則。
2.客戶端無法感知到的改動
如果一個類的改動會引起另一個類的改動,那麼這兩個類就是緊密耦合的。相反,如果一個類的修改總是獨立的,並不會引起其他類的改動,那麼這些類就是鬆散耦合的。我們要記住,任何情況下,鬆散耦合都比緊密耦合要好。如果我們對現有程式碼的修改不會影響客戶端程式碼,那麼也就談不上違背開放封閉原則。
3.修改還是擴充套件?
從開閉原則定義中,我們可以看出,開閉原則可以應用在不同粒度的程式碼中,可以是模組,也可以類,還可以是方法(及其屬性)。同樣一個程式碼改動,在粗程式碼粒度下,可以被認定為“修改”,但在細程式碼粒度下,又可以被認定為“擴充套件”。
比如,在類這個層面新增屬性和方法相當於修改類,這個程式碼改動可以被認定為“修改”;但這個改動並沒有修改已有的屬性和方法,在方法(及其屬性)這一層面,它又可以被認定為“擴充套件”。
實際上,當糾結於某個程式碼改動是“修改”還是“擴充套件”的時候,我們就已經背離了設計原則的初衷,開閉原則的本質目的就是為了讓我們的程式碼更具有擴充套件性,更容易維護,如果我們可以很容易的完成修改,又不會影響到既有的程式碼與單測,就可以認為這是一個合理的改動。
3.3 擴充套件性與可讀性的平衡
在有些情況下,程式碼的擴充套件性會跟可讀性相沖突。為了更好地支援擴充套件性,我們對程式碼進行了重構,重構之後的程式碼要比之前的程式碼複雜很多,理解起來也更加有難度。實際上很多時候,我們都要結合具體的場景在擴充套件性和可讀性之間做權衡。在某些場景下,擴充套件性很重要,我們就可以適當地犧牲一些可讀性;而在另一些場景下,可讀性更加重要,那我們就適當地犧牲一些擴充套件性。
小結
絕大多數情況下,我們的系統都不是一錘子買賣,通常隨著需求的迭代,我們需要不斷地對其進行維護與擴充套件。而開閉原則的思想可以很好的解決擴充套件性的問題,因此理解並掌握開閉原則至關重要,但這需要我們充分的理解物件導向的思想,合理的利用封裝、多型等方法以及長期大量的積累!
系列文章
關注下方公眾號,回覆“程式碼的藝術”,可免費獲取重構、設計模式、程式碼整潔之道等提升程式碼質量等相關學習資料