軟件設計原則之第一篇——開閉原則(OCP)

 

這篇是軟件設計原則系列文章的第一篇,之前寫過一篇博客裏面介紹了七種設計原則,但是將七種原則容納到一篇文章之中總感覺哪裏不對:說的太多文章就會變得冗長影響閱讀體驗,說的太少總感覺有的話沒說完就進行下一項了,於是乎本人突發奇想爲何不分開來寫?更於是乎就有了“軟件設計原則系列文章”,本文是個開篇。廢話不多說了,Let's come to the point!

軟件設計一共有七大原則,在這七大原則之上還有一個總體原則,也就是我們常說的“高內聚,低耦合”。它們之間就好比白雪公主和七個小矮人的關係,那它們叫什麼名字呢?讓我們先來認識一下,它們分別是:開閉原則(OCP)、依賴倒置原則(DIP)、單一職責原則(SRP)、接口隔離原則(ISP)、迪米特原則(LoD)、里氏替換原則(LSP)和合成複用原則(CARP)。今天的主角是這七個矮人中的姚明,它就是開閉原則(OCP,Open Close Principle)。

門一開一閉,眼一閉一睜,這也算個原則?

沒錯,但這裏的開閉不是指門,而是對擴展開放對修改關閉,用擴展而不是修改來適應需求的變化。有句話說的好——唯一不變的是變化本身,應用到軟件界就是需求是一定會變化的,即使原來的不變新的東西也會加進來,如果你想一勞永逸,開發的東西永遠不會變,那只有一種可能——這個系統已經沒人在用了。怎麼證明一個軟件或者系統還活着?我們來看一下身邊的例子:Win10是不是會不定時地更新升級?Android和IOS等常用的手機系統是不是會經常提示你升級?我們經常玩兒的遊戲是不是會經常打補丁……那什麼樣的代碼不需要維護了呢?在我上大學的時候最火的手機品牌之一——諾基亞,當時它安裝的系統塞班現在已經被淘汰,這個代碼已經不需要維護了(壞了,暴露年齡了)。現在是2020年的3月份,就在兩個月前的1月14日,微軟官方宣佈Win 7操作系統退役,從此陪伴了大家10年之久的Windows 7正式退出歷史舞臺,它的代碼也不需要維護了,更早的像什麼Windows XP啊、Windows 98啊就更不用說了(再一次暴露年齡😂),估計再過十幾年的學生都不一定聽說過這些操作系統。說了這麼多,其實想表達的就一點:既然變化是一定要發生的,那我們應該應用什麼策略以及採取什麼措施來應對變化,我覺得這是所有軟件設計者包括開發者應該考慮的問題。哈哈,值得慶幸的是早已有前輩爲我們考慮好了這些事情,他們總結的經驗經過了大量工程實踐的考驗並被公認爲行之有效的方法,從而逐漸形成原則沉澱下來爲後人所用,這就是軟件設計的原則。

我們在討論本篇文章的主角——OCP之前再來廢一些話,那就是如果不遵守這七大原則能不能行?就問行!不!行!答案是可以的,沒問題,代碼照樣能運行!那你還扯個哩個兒愣~~散了,散了,都散了,各回各家吧,博主現場翻車!~~嘖嘖嘖……我是不是在這扯皮一會兒就知道了,下面用實際的代碼來感受一下用OCP和不用OCP的差別。

我們假設一個場景:小明買車,今天小明買了一輛奔馳,開着奔馳去兜風。

public class Benz {

	public void run() {
		System.out.println("Benz run...");
	}

}
public class Person {
	private String name;

	public Person(String name) {
		super();
		this.name = name;
	}

	public String getName() {
		return name;
	}

	public void driveACar(Benz benz) {
		System.out.println(getName());
		benz.run();
	}

}
public class OCP {
	public static void main(String[] args) {
		Person xiaoMing = new Person("小明");
		Benz benz = new Benz();
		xiaoMing.driveACar(benz);
	}
}

main方法模擬的是客戶端程序,Person模擬的是服務端程序,這樣看起來沒什麼問題,確實也沒什麼問題。然後需求變了:小明換車了,把奔馳換成了寶馬,然後開着寶馬去兜風。你會說那還不簡單,改一下不就行了嗎?

public class BMW {// 增加一個寶馬類

	public void run() {
		System.out.println("BMW run...");
	}

}
public class Person {
	private String name;

	public Person(String name) {
		super();
		this.name = name;
	}

	public String getName() {
		return name;
	}

	public void driveACar(Benz benz) {
		System.out.println(getName());
		benz.run();
	}
	
	public void driveACar(BMW bmw) {// 增加開寶馬的方法
		System.out.println(getName());
		bmw.run();
	}

}
public class OCP {
	public static void main(String[] args) {
		Person xiaoMing = new Person("小明");
//		Benz benz = new Benz();
//		xiaoMing.driveACar(benz);
		
		BMW bmw = new BMW();// 修改客戶端代碼:註釋掉原有調用開奔馳的方法,新增調用開寶馬的方法
		xiaoMing.driveACar(bmw);
	}
}

我們來看一下這一次的需求改動需要做什麼工作:首先服務端代碼Person要增加一個開寶馬的方法,因爲原有開奔馳的方法不能傳入寶馬對象所以只能新增一個;其次,客戶端也要改變調用的代碼,因爲原先調用開奔馳的方法已不滿足新的需求。所以,Look一下,一個小小的需求改動會牽扯到從服務端到客戶端整體的改造,所謂牽一髮而動全身。也許你又會問:這樣不行嗎?在本例中我只是用很簡單的代碼來舉栗子,實際項目裏面代碼可能會很複雜,所以需求變化可能會改很多代碼,工作量比較大;另一方面,改造原有代碼意味着引入風險,因爲原有代碼可能是經過了很長時間的“磨合”,功能已經非常穩定了,你去改造它能保證百分百不引入bug嗎?很有可能你改造A功能的代碼,但是卻無意間影響了原有的B功能,導致原有B功能不可用或出現問題。所以,面對新的需求我們不嚴格要求絕對不能改動原有代碼,但我們應該做到的是儘可能少的改動。拿本例來說,如果小明再換一輛奧迪呢?是不是Person又得加一個開奧迪的方法?如果我們將這些具體品牌的車抽象出一個更高層的概念——汽車類,那麼你需要開什麼車就傳入子類(或實現類)不就可以了嗎?下面我們就按照這一思路來對上述代碼做改造:

public interface ICar {
	/**
	 * 將具體的車抽象出來形成一個更高層的概念,可以是一個接口或者抽象類,
	 * 它裏面有一個run方法,所有具體品牌的車都繼承或者實現這個更高層的類。
	 */
	void run();
}
public class Benz implements ICar {

	public void run() {
		System.out.println("Benz run...");
	}

}
public class BMW implements ICar{

	public void run() {
		System.out.println("BMW run...");
	}

}
public class Person {
	private String name;

	public Person(String name) {
		super();
		this.name = name;
	}

	public String getName() {
		return name;
	}

	public void driveACar(ICar car) {
		System.out.println(getName());
		car.run();
	}
}
public class OCP {
	private static ICar car;
	
	static {
		// 模擬Spring注入ICar實例,需要不同品牌的車就注入不同品牌的實例即可
		car = new Benz();
	}
	
	public static void main(String[] args) {
		Person person = new Person("小明");
		person.driveACar(car);
	}
}

這樣服務提供端Person就提供一個driveACar方法即可,入參是一個接口,所有具體品牌的車都實現ICar接口,這樣需要什麼車就傳入相關的實現類即可,在客戶端需要什麼車就注入一個汽車實例即可,所以面對小明不斷地換車,只需要擴展ICar的實現類即可,客戶端(main方法)和服務提供方(Person)都不需要改代碼,這就是“對擴展開放,對修改關閉”,其實面向抽象(接口)編程就是這個道理。如果再放開一點,可以把Person也抽象成接口,這樣不僅小明可以開奔馳,張三、李四都可以開,需要不同的人開車就在客戶端注入不同個人的實例,這一點就不在這裏用代碼演示了。

所以,到這裏讀者應該明白什麼是OCP了以及遵循OCP原則會帶來哪些好處,本文也接近尾聲了。最後說一下爲什麼在文章開頭處我用“白雪公主和七個小矮人”來做比喻,目的是爲了讓讀者記住“7”這個數字,可能你看完這個系列文章之後過一段時間會忘記一共有幾大原則,今後哪天如果被面試官問到只要你腦海記住有七個小矮人就可以了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章