前言
喜歡看技術性的文章,記得以前總是說設計模式不用太早接觸之類的話, 促使我決定好好學習設計模式的原因
- 現(xiàn)在我覺得時機也差不多了
- 實在有些代碼寫完之后發(fā)現(xiàn)很難維護,或者改動的地方特別多, 肯定是我當時的設計有問題
所發(fā)表所有內(nèi)容僅代表個人觀點。
設計模式概念
說到設計模式,第一反應就是很深奧,完全理解不了這個概念到底是什么意思, 網(wǎng)上的定義最多的是: 設計模式是一套被反復使用的、多數(shù)人知曉的、經(jīng)過分類編目的、代碼設計經(jīng)驗的總結。使用設計模式是為了重用代碼、讓代碼更容易被他人理解、保證代碼可靠性。
反復使用, 多人知曉: 意味著被眾人認可
分類編目: 歸納和總結特征進行分類
代碼設計經(jīng)驗: 經(jīng)前人驗證過的, 保證好使的套路
當然, 其實這都是個人理解的不同而不同, 權威的概念:
設計模式(Design pattern)代表了最佳的實踐,通常被有經(jīng)驗的面向?qū)ο蟮能浖_發(fā)人員所采用。設計模式是軟件開發(fā)人員在軟件開發(fā)過程中面臨的一般問題的解決方案。這些解決方案是眾多軟件開發(fā)人員經(jīng)過相當長的一段時間的試驗和錯誤總結出來的。設計模式是一套被反復使用的、多數(shù)人知曉的、經(jīng)過分類編目的、代碼設計經(jīng)驗的總結。使用設計模式是為了重用代碼、讓代碼更容易被他人理解、保證代碼可靠性。毫無疑問,設計模式于己于他人于系統(tǒng)都是多贏的,設計模式使代碼編制真正工程化,設計模式是軟件工程的基石,如同大廈的一塊塊磚石一樣。項目中合理地運用設計模式可以完美地解決很多問題,每種模式在現(xiàn)實中都有相應的原理來與之對應,每種模式都描述了一個在我們周圍不斷重復發(fā)生的問題,以及該問題的核心解決方案,這也是設計模式能被廣泛應用的原因。
為何學習設計模式
就像開篇我所說的, 當你發(fā)現(xiàn)一個新的需求,或者小的需求變更, 難以更改以前的代碼, 有木有種牽一發(fā)而動全身的感覺?有? 那你跟我一樣, 該好好學學設計模式了, 同學,其他不用多講了, 還需要其他理由就自行百度吧.
設計模式的六大原則
單一職責
描述的意思是每個類都只負責單一的功能,切不可太多,并且一個類應當盡量的把一個功能做到極致。
這個我參考了一些資料, 發(fā)現(xiàn)怎么寫的都有, 還要根據(jù)項目實際情況考慮,也不能說過分的拆分職責.
建議:
接口一定要做到單一職責,類的設計盡量做到只有一個原因引起變化
例子:
我看到一個例子, 簡單描述:
public interface IPhone {
public void 撥號(string number)
public void 接電話()
public void 通話()
public void 應答()
public void 掛斷()
}
原文解釋:
撥號 ()和掛斷 ()兩個方法實現(xiàn)的是協(xié)議管理,分別負責撥號接通和掛機;通話 ()和應答 ()是數(shù)據(jù)的傳送,把我們說的話轉換成模擬信號或數(shù)字信號傳遞到對方,然后再把對方傳遞過來的信號還原成我們聽得懂語言。我們可以這樣考慮這個問題,協(xié)議接通的變化會引起這個接口或?qū)崿F(xiàn)類的變化嗎?會的!那數(shù)據(jù)傳送(想想看,電話不僅僅可以通話,還可以上網(wǎng))的變化會引起這個接口或?qū)崿F(xiàn)類的變化嗎?會的!那就很簡單了,這里有兩個原因都引起了類的變化,而且這兩個職責會相互影響嗎?電話撥號,我只要能接通就成,甭管是電信的還是網(wǎng)通的協(xié)議;電話連接后還關心傳遞的是什么數(shù)據(jù)嗎?不關心,你要是樂意使用56K的小貓傳遞一個高清的片子,那也沒有問題。通過這樣的分析,我們發(fā)現(xiàn)類圖上的電話 接口包含了兩個職責,而且這兩個職責的變化不相互影響,那就考慮拆開成兩個接口,然后把這個接口拆成兩個接口, 一個負責數(shù)據(jù)傳輸, 一個負責接與掛電話...
我認為這個說法也不完全正確, 像這種情況, 設計一個成一個"電話相關的接口, 也不是不對的,它具有這些方法, 很正常. 所以這東西仁者見仁, 智者見智!
單一職責原則提出了一個編寫程序的標準,用“職責”或“變化原因”來衡量接口或類設計得是否合理,但是“職責”和“變化原因”都是沒有具體標準的,一個類到底要負責那些職責?這些職責怎么細化?細化后是否都要有一個接口或類?這些都需從實際的情況考慮。因項目而異,因環(huán)境而異。
總結:
這應該是很簡單的一個原則了, 同時也是很模糊的一個原則, 需要經(jīng)驗, 環(huán)境, 團隊協(xié)作等很多方面的考慮, 所以原則是死的 , 人是活的! !!! 盡量遵守!
里氏替換原則
這個原則表達的意思是一個子類應該可以替換掉父類并且可以正常工作。同樣的,用基類替換現(xiàn)在的子類,必須保證程序運行(包括業(yè)務流程)都是不變的。這是我看了一些資料后的理解
例子:
首先設計一個Person類 人類, 具有很多能力, 包括生育, 但僅限女性對吧
public class Person {
// 生育 需要傳入一個女人對象
public void bearChildren(Woman woman) {
woman.bearChildren();
}
}
再設計一個Woman 女人對象和更細致一點的Girl 女孩對象, 女孩對象繼承女人對象
public class Woman {
public void bearChildren() {
System.out.println("順利生產(chǎn)");
}
}
public class Girl extends Woman {
// 重寫了父類的生育方法
@Override
public void bearChildren() {
throw new RuntimeErrorException(null, "我還是小孩,無法生孩子");
}
}
運行Person類的生育方法
public class App {
public static void main(String[] args) {
Person person = new Person();
person.bearChildren(new Woman());
person.bearChildren(new Girl());
}
}
運行結果:
woman 順利生產(chǎn)
girl Exception in thread "main" javax.management.RuntimeErrorException: 我還是小孩,無法生孩子
這個異常是運行時才會產(chǎn)生的,也就是說,Person類并不知道會出現(xiàn)這種情況,Woman傳給Person.bearChildRen完成生育功能,Girl類繼承了Woman,當然也可以了實現(xiàn)同樣功能,但是最終這個調(diào)用會拋出異常。
注意:
一個子類應該可以替換掉父類并且可以正常工作, 那么現(xiàn)在替換后程序沒有正常工作, 所以違背了里氏替換原則
這項原則并不是要避免多態(tài),而是要求子類在繼承父類的時候,不能與父類已經(jīng)定好的契約沖突,也就是不要重寫父類已經(jīng)實現(xiàn)的方法。
簡而言之:子類必須可以替換成父類對象(并且行為不會變),子類不要添加基類沒有的約束,那么怎么檢驗是否符合這個原則呢?就是用基類替換現(xiàn)在的子類,必須保證程序運行(包括業(yè)務流程)都是不變的。
子類不能添加父類沒有的約束的例子:
比如場景是讓汽車奔跑,子類是1.奔馳(前提是加100L汽油)2.寶馬(前提要洗車才能跑)。
問題來了,不同的子類有了不同的約束條件。
public abstract class Car {
public abstract void Run();
}
public class Benz : Car {
public int Oil;
public overrride void Run() {
//跑
}
}
public class BMW: Car {
public bool WashCar;
public overrride void Run() {
//跑
}
}
下面有一個工廠方法(目的是體現(xiàn)用基類替換子類,因為工廠方法返回值是父類類型)
public Class Factory {
public static Car Create() {
if(1) return new Benz();
if(2) return new BMW();
}
}
最后是實際調(diào)用代碼:
來一輛奔馳:Car Benz=Factory.Create(1);
來一輛寶馬:Car BMW=Factory.Create(2);
然后我準備開車了....
Benz.Run();
BMW.Run();
您覺得能行嗎?肯定不行,還沒加油呢,還沒洗車呢。
我前面還得先
Benz.Oil=100;
BMW.WashCar=true;
才能
Benz.Run();
BMW.Run();
但是明顯這樣就違背父類預期的原則了,怎么辦?其實可以把參數(shù)放到子類構造函數(shù)的形參中。解決問題方案有多種,但原則就是”子類必須可以替換成父類對象(并且行為不會變),子類不要添加基類沒有的約束"
依賴倒轉原則
針對接口編程,依賴于抽象而不依賴于具體。
例子
我現(xiàn)在需要從一個文本文件中獲取變量number的值, 有如下類
public class Reader {
public static int getNumber(String path){
BufferedReader br = new BufferedReader(new FileReader(new File(path)));
return Integer.valueOf(br.readLine())''
}
}
我調(diào)用 Reader.getNumber("file.text")獲取到了number的值
那么如果需求變更了, 需要從數(shù)據(jù)庫中獲取這個number的值呢? 從XML文件中獲取呢? 我需要改Reader getNumber的方法,或者我需要加一堆方法, getNumberFormXMl... getNumberFormDB?
不如直接寫一個接口
public interface Redaer {
public int getNumber();
}
用不同的實現(xiàn)getNumber即可. 你可以有XMLReader, 或者DBReader來實現(xiàn)getNumber,客戶端只管Reader.getNumber()就好, 無需關心number是怎么來的.
接口隔離原則
也稱接口最小化原則,強調(diào)的是一個接口擁有的行為應該盡可能的小。
例子:
比如我們設計一個手機的接口時,就要手機哪些行為是必須的,要讓這個接口盡量的小,或者通俗點講,就是里面的行為應該都是這樣一種行為,就是說只要是手機,你就必須可以做到的。
public interface Mobile {
public void call();//手機可以打電話
public void sendMessage();//手機可以發(fā)短信
public void playBird();//手機可以玩憤怒的小鳥?
}
上面第三個行為明顯就不是一個手機應該有的,或者說不是一個手機必須有的,那么上面這個手機的接口就不是最小接口,假設我現(xiàn)在的非智能手機去實現(xiàn)這個接口,那么playBird方法就只能空著了,因為它不能玩。所以我們更好的做法是去掉這個方法,讓Mobile接口最小化,然后再建立下面這個接口去擴展現(xiàn)有的Mobile接口。
public interface SmartPhone extends Mobile{
public void playBird();//智能手機的接口就可以加入這個方法了
}
這樣兩個接口就都是最小化的了,這樣我們的非智能手機就去實現(xiàn)Mobile接口,實現(xiàn)打電話和發(fā)短信的功能,而智能手機就實現(xiàn)SmartPhone接口,實現(xiàn)打電話、發(fā)短信以及玩憤怒的小鳥的功能,兩者都不會有多余的要實現(xiàn)的方法。
還有一個例子:
比如電商項目的訂單系統(tǒng),看看別人是怎么設計的
訂單這個類,有兩個地方會使用到,一般是前臺門戶,用戶查詢訂單, 后臺管理, 管理員處理訂單, 增刪改查, 其實門戶也可以增刪改查訂單的,有些系統(tǒng),這里就假設門戶只能查詢訂單, 有其他也是同樣道理
interface IOrderForPortal{ 門戶接口
String getOrder();
}
interface IOrderForAdmin{ 后臺接口
String deleteOrder();
String updateOrder();
String insertOrder();
String getOrder();
}
/*
// 如果門戶和后臺查詢訂單方法相同實現(xiàn), 也可以這樣
interface IOrderForPortal{
String getOrder();
}
interface IOrderForAdmin extendsIOrderForPortal{
String updateOrder();
String deleteOrder();
}
*/
class Order implements IOrderForPortal,IOrderForAdmin{
private Order(){
//--什么都不干,就是為了不讓直接 new,防止客戶端直接New,然后訪問它不需要的方法.
}
//返回給Portal
public static IOrderForPortal getOrderForPortal(){
return (IOrderForPortal)new Order();
}
//返回給Admin
public static IOrderForAdmin getOrderForAdmin(){
return (IOrderForAdmin)new Order();
}
//--下面是接口方法的實現(xiàn).只是返回了一個String用于演示
public String getOrder(){
return "implemented getOrder";
}
public String insertOrder(){
return "implementedinsertOrder";
}
public String updateOrder(){
return "implementedupdateOrder";
}
public String deleteOrder(){
return "implementeddeleteOrder";
}
}
public class TestCreateLimit{
public static void main(String[]
args){
IOrderForPortal orderForPortal =Order.getOrderForPortal();
IOrderForAdmin orderForAdmin = Order.getOrderForAdmin();
System.out.println("Portal門戶調(diào)用方法:"+orderForPortal.getOrder());
System.out.println("Admin管理后臺調(diào)用方法:"+orderForAdmin.getOrder()+";"+orderForAdmin.insertOrder()+";"+orderForAdmin.updateOrder()+";"+orderForAdmin.deleteOrder());
}
}
這樣就能很好的滿足接口隔離原則了,調(diào)用者只能訪問它自己的方法,不能訪問到不應該訪問的方法.
很多人會覺的接口隔離原則跟之前的單一職責原則很相似,其實不然。其一,單一職責原則原注重的是職責;而接口隔離原則注重對接口依賴的隔離。其二,單一職責原則主要是約束類,其次才是接口和方法,它針對的是程序中的實現(xiàn)和細節(jié);而接口隔離原則主要約束接口接口,主要針對抽象,針對程序整體框架的構建。
采用接口隔離原則對接口進行約束時,要注意以下幾點:
- 接口盡量小,但是要有限度。對接口進行細化可以提高程序設計靈活性是不掙的事實,但是如果過小,則會造成接口數(shù)量過多,使設計復雜化。所以一定要適度。
- 為依賴接口的類定制服務,只暴露給調(diào)用的類它需要的方法,它不需要的方法則隱藏起來。只有專注地為一個模塊提供定制服務,才能建立最小的依賴關系。
- 提高內(nèi)聚,減少對外交互。使接口用最少的方法去完成最多的事情。
運用接口隔離原則,一定要適度,接口設計的過大或過小都不好。設計接口的時候,只有多花些時間去思考和籌劃,才能準確地實踐這一原則。
迪米特法則,又稱最少知道原則
最少知道原則是指:一個實體應當盡量少地與其他實體之間發(fā)生相互作用,使得系統(tǒng)功能模塊相對獨立
這個原則的制定,是因為如果一個類知道或者說是依賴于另外一個類太多細節(jié),這樣會導致耦合度過高,應該將細節(jié)全部高內(nèi)聚于類的內(nèi)部,其他的類只需要知道這個類主要提供的功能即可。
所謂高內(nèi)聚就是盡可能將一個類的細節(jié)全部寫在這個類的內(nèi)部,不要漏出來給其他類知道,否則其他類就很容易會依賴于這些細節(jié),這樣類之間的耦合度就會急速上升,這樣做的后果往往是一個類隨便改點東西,依賴于它的類全部都要改。
開閉原則
就是說我任何的改變都不需要修改原有的代碼,而只需要加入一些新的實現(xiàn),就可以達到我的目的,這是系統(tǒng)設計的理想境界,但是沒有任何一個系統(tǒng)可以做到這一點,哪怕我一直最欣賞的spring框架也做不到,雖說它的擴展性已經(jīng)強到變態(tài)。
這個原則更像是前五個原則的總綱,前五個原則就是圍著它轉的,只要我們盡量的遵守前五個原則,那么設計出來的系統(tǒng)應該就比較符合開閉原則了,相反,如果你違背了太多,那么你的系統(tǒng)或許也不太遵循開閉原則。
參考:
感謝這些作者為我們這些后來者提供了大量的閱讀資料, 還有一些百度查到的零碎的資料
http://www.cnblogs.com/zuoxiaolong/p/pattern1.html
http://www.itdecent.cn/p/60aea0cf6bda