經典設計模式實戰演練
課程簡介
面向物件的程式應該具有可維護性,程式碼可複用性,擴充套件性以及靈活性。為了實現以上目的,前輩們從實踐中總結出了一套可套用的武功招式,這就是設計模式。使用設計模式可以讓你寫出一手令人賞心悅目的程式碼。
我認為每一個後端開發者都應該學習設計模式,它是程式碼的精華,是程式發展的強力支撐,是能夠讓你發出驚歎的神來之筆。
本課程共有10篇,結合作者的開發經驗,從理論到實戰,剖析設計模式經典案例,幫助讀者掌握將設計模式應用於實際專案開發的能力。
課程主要分為兩大部分:
第一部分(第01-09課),介紹常用的幾種設計模式,通過具體案例的分析與程式碼實現帶領大家深入學習與理解設計模式;
第二部分(第10課),將結合多種設計模式手把手帶領大家開發一個綜合案例,提升設計模式實戰的能力。
作者介紹
周君,資深後端工程師,CSDN部落格專家,精通 PHP、Java、Web開發。多年實戰經驗,熱衷分享。
課程內容
導讀:如何學好設計模式
什麼是設計模式
設計模式(Design Pattern)代表了最佳的實踐,通常被有經驗的面向物件的軟體開發人員所採用。設計模式是軟體開發人員在軟體開發過程中面臨的一般問題的解決方案。這些解決方案是眾多軟體開發人員經過相當長的一段時間的試驗和錯誤總結出來的。
上面的解釋來自於某度某科,是比較標準的定義,可以從中篩選出幾個關鍵字來幫助我們理解什麼是設計模式:
- 最佳實踐
- 解決方案
- 試驗和錯誤總結
從上面的三個關鍵詞中可以總結出,設計模式就是在針對編碼過程中遇到的問題總結出來的最佳解決方案。
那麼這些問題指的是什麼問題呢?
面向物件的程式應該具有可維護性、程式碼可複用性、擴充套件性及靈活性,要解決的問題就是程式碼可維護性問題、複用性問題、擴充套件性問題、靈活性問題。
簡單來說,設計模式就是指導你如何寫出可維護、可複用、可擴充套件及靈活的程式碼。
設計模式分類
設計模式總共有23種,總體來說可以分為三大類:建立型模式(Creational Patterns)、結構型模式(Structural Patterns)和行為型模式(Behavioral Patterns)。
分類 | 關注點 | 包含 |
---|---|---|
建立型模式 | 關注於物件的建立,同時隱藏建立邏輯 | 工廠模式 抽象工廠模式 單例模式 建造者模式 原型模式 |
結構型模式 | 關注類和物件之間的組合 | 介面卡模式 過濾器模式 裝飾模式 享元模式 代理模式 外觀模式 組合模式 橋接模式 |
行為型模式 | 關注物件之間的通訊 | 責任鏈模式 命令模式 中介者模式 觀察者模式 狀態模式 策略模式 模板模式 空物件模式 備忘錄模式 迭代器模式 直譯器模式 訪問者模式 |
上面的三種分類,有助於在開發時思考當前場景應該使用哪種分類。
大家不一定要全部記住,有個大概的瞭解即可。
學習設計模式
為什麼要學設計模式
寫出可維護、可複用、可擴充套件及靈活的程式碼是我們的目的,也是學習設計模式的理由,但是這個理由對我們來說太抽象,下面從“讀”和“寫”兩方面來說明到底為什麼要學習設計模式。
讀
作為開發人員,不可避免的要接觸其他人寫的程式碼,有的是一些知名的庫或框架,例如 Spring、Shiro 等。
但是當我們去閱讀這些框架原始碼的時候會發現無從下手,因為類太多了,關係太複雜,而且很多類的命名看不懂,比如 xxxBuilder、xxxStrategy、xxxFilter 等,一個詞看不懂就可能導致你直接放棄繼續閱讀。
如果沒有學過設計模式,自然看不懂,學習設計模式可以有效的幫助你閱讀程式碼,即便不能百分百幫到你,至少也能幫到百分之三四十。
寫
每一個開發人員必然噴過其他人寫的程式碼,覺得其他人的程式碼寫的很垃圾,尤其是要擴充套件功能或者修改功能的時候,恨不得全部刪掉重新再寫,其實在其他人看來你的程式碼也是如此。所以寫出一手讓人無話可說的程式碼是很有必要的,不僅可以滿足你的小小成就感,也可以讓你的程式更快速穩定的發展。
在一個專案組中,如果大家都學習過設計模式,那麼當你閱讀或修改同事寫的程式碼時也將得心應手,少了很多麻煩。
如何學好設計模式
現如今網上和書上都有大量的設計模式的教程,但是他們大部分都有一個共同點:僅僅使用生活中的例子。
比如前幾年我第一次學習設計模式,在學到介面卡模式時,教程中丟擲了一個電器的插頭問題:
你家插座只有三頭的,但電器插頭是兩頭的,咋辦?弄個插頭介面卡將兩頭轉換成三頭。
nice,這個例子簡單明瞭,作為新手的我瞬間明白了介面卡的含義,就是在不相容的雙方中間做一層轉化。
但是後來發現在實際編碼中根本用不上這個設計模式,因為我根本不會用。
生活中的例子的確可以幫助我們理解設計模式,這是毋庸置疑的,但是想要真正用好設計模式,實際專案中的案例是必不可少的,這也是我寫這門課的原因,希望通過分析實際案例,能夠幫到更多想要學習設計模式的同行。
下面給出幾點更加具體的建議:
- 從生活例子中去理解設計模式;
- 從實際案例去了解設計模式的使用場景;
- 動手實踐,在學完實際案例之後,不妨動手寫一寫,不要寫生活中的例子,自己構造一個小功能,用上你的設計模式;
- 改變自己的意識,在開發或修改一個功能時,首先要下意識地去思考這個功能將來在修改和擴充套件上會遇到什麼問題,能否使用上設計模式。記住一定要思考、一定要思考、一定要思考,即便最終用不上,也能讓你回顧一遍設計模式的內容,使其知識更牢固。很多開發者不是不會用,而是根本沒有想過要用設計模式,久而久之這方面的能力自然就弱化了。
課程說明
課程內容
本課程每一篇文章主要包含三大部分:
- 解釋和理解設計模式;
- 至少介紹一個實際案例(實際案例有些是我自己寫的,有些來自於已有的框架或庫);
- 設計模式優缺點。
必要準備
本課程將使用 Java 語言講解設計模式,雖然設計模式與語言本身無關,但是本課程中有許多實際案例都是來自於知名的 Java 框架原始碼,如果沒有 Java 基礎,學習效果可能不佳。
除了要求 Java 基礎之外,還需要了解 UML 圖,如果不瞭解 UML,只需要知道以下幾種 UML 關係即可:
- 泛化:可以簡單的理解為繼承關係;
- 實現:一般是介面和實現類之間的關係;
- 關聯:一種擁有關係,比如老師類中有學生列表,那麼老師類和學生類就是擁有關係;
- 聚合:整體與部分的關係,但是整體和部分是可以分離而獨立存在的,如汽車類和輪胎類;
- 組合:整體與部分的關係,但是二者不可分離,分離了就沒有意義了,例如,公司類和部門類,沒有公司就沒有部門;
- 依賴:一種使用關係,例如建立 A 類必須要有 B 類。
參考下圖:
記不住也沒關係,後續課程主要使用泛化和實現這兩種,先記住這兩種即可,如果有遇到看不懂的再回頭來看一眼。
第01課:策略模式
策略模式定義了演算法族,分別封裝起來,讓他們之間可以互相替換,此模式讓演算法的變化獨立於使用演算法的客戶。
一般情況下我們是將一種行為寫成一個類方法,比如計算器類中有加、減、乘、除四種方法,而策略模式則是將每一種演算法都寫成一個類,然後動態的選擇使用哪一個演算法。
這裡所說的演算法並不是指“氣泡排序演算法”、“搜尋演算法”之類的演算法,它可以是一段程式碼、一個請求、一個業務操作。
策略模式:
從上圖可以看到,我們將操作封裝到類中,他們實現了同一個介面,然後在 Context 中呼叫。
這裡我們舉一個計算器的例子:
此例中,為加法和減法分別建立了一個類。
其實策略不一定要命名為 Strategy,Context 不一定要叫 Context,可以根據實際情況自己命名,在計算器的例子中,你如果非要命名為 Strategy 和 Context,反而讓人產生疑惑。
實際程式碼也很簡單,具體如下。
Operation 介面:
public interface Operation { public int doOperation(int num1, int num2);}
兩個實現類——加法和減法:
public class OperationAdd implements Operation{ @Override public int doOperation(int num1, int num2) { return num1 + num2; }}public class OperationSub implements Operation { @Override public int doOperation(int num1, int num2) { return num1 - num2; }}
計算器類:
public class Calculator { private Operation operation; public void setOperation(Operation operation){ this.operation = operation; } public int doOperation(int num1, int num2){ return this.operation.doOperation(num1,num2); }}
使用:
Calculator calculator = new Calculator();calculator.setOperation(new OperationAdd());int result = calculator.doOperation(1,2);System.out.println(result);
使用計算器類時,如果要進行加法運算,就 new 一個加法類傳入,減法也是同理。
看到這裡,相信大家一定會有疑惑,為什麼要把加、減、乘、除四則運算分別封裝到類中?直接在 Calculator 中寫 add()、sub() 等方法不是更方便嗎?
甚至如果要新增其他的運算方法,每次都要建立一個類,反而更麻煩。
的確用了策略模式之後程式碼比普通寫法多了一些,但是這裡假設一種場景:
假設把寫好的計算器程式碼打包好作為一個庫釋出出去給其他人用,其他人發現你的計算器中只有加、減、乘、除四個方法,而他想增加平方、開方等功能,咋辦?
如果是用普通寫法寫的計算器,想要增加功能唯一的辦法就是修改你寫好的 Calculator,增加平方和開方兩個 method。
可是你提供的是一個 jar 包啊,jar 包,jar...jar...jar...jar...包……
就算你提供的是原始碼,你希望其他人可以隨意的修改你寫好的程式碼嗎?一般我們釋出出去的開源框架或庫都是經過千錘百煉,經過測試的程式碼,其他人隨意修改我們的原始碼很容易產生不可預知的錯誤。
如果你用的是策略模式,那麼其他人想要增加平方或開平方功能,只需要自己定義一個類實現你的 Operation 介面,然後呼叫 calculator.setOperation(new 平方類()); 即可。
看到這裡相信你已經對策略模式有了一定的好感,甚至驚歎一聲:哇,還有這種操作?
順便提一嘴,這裡很好的體現了一個設計模式的基本原則:開閉原則。
開閉原則說的是 對修改關閉、對擴充套件開放。
對修改關閉就是不希望別人修改我們的程式碼,此路不通,對擴充套件開放就是希望別人以擴充套件的方式增加功能,策略模式把開閉原則體現得淋漓盡致。
案例1:主題
隔壁老王準備開發一個客戶端框架,允許其他的開發者進行二次開發,其中有一個更換主題的功能,開發者們可以自己定義主題。老王很快就想到了策略模式,並且提供了一個預設主題 DefaultTheme:
程式碼:
public interface Theme { public void showTheme();}public class DefaultTheme implements Theme { @Override public void showTheme() { //此處設定主題顏色,背景,字型等 System.out.println("顯示預設主題"); }}public class ThemeManager { private Theme theme; public void setTheme(Theme theme){ this.theme = theme; } public void showTheme(){ this.theme.showTheme(); }}
使用:
ThemeManager themeManager = new ThemeManager();themeManager.setTheme(new DefaultTheme());themeManager.showTheme();
看完更換主題的案例程式碼,你會發現跟計算器驚人的相似,沒錯,所謂設計模式就是前人總結出來的武功套路,經常可以直接套用。當然也要靈活的根據實際情況進行修改,設計模式想要傳達給我們的更多的是一種程式設計思想。
這裡還有一個小竅門:
themeManager.setTheme(new DefaultTheme());
在這裡老王 new 一個預設主題物件,如果其他開發者加了主題,還要修改這行程式碼,new 開發者自定義的主題物件。根據開閉原則,我們不希望其他人修改我們的任何一行程式碼,否則拔刀相見。老王機智的將主題的包名和類名寫到了配置檔案中,利用 Java 的反射機制動態生成主題物件,因此更換主題也只要修改配置檔案即可。
案例2:shiro
shiro 是 Java 界最著名的許可權控制框架之一,相信大家都不陌生。在 shiro 中,我們可以建立多個許可權驗證器進行許可權驗證,如驗證器 A、驗證器 B、驗證器 C,三個驗證器可以同時生效。
那麼就產生了一個問題,如果驗證器 A 驗證通過,B 驗證不通過,C 驗證通過,這種情況怎麼辦?到底算當前使用者驗證通過還是不通過呢?
shiro 給我們提供了三種驗證策略,就像老王預設提供了一種主題一樣:
- AtLeastOneSuccessfulStrategy:只要有一個驗證通過,那麼最終驗證結果就是通過。
- FirstSuccessfulStrategy:只有第一個成功地驗證的 Realm 返回的資訊將被使用,所有進一步的 Realm 將被忽略,如果沒有一個驗證成功,則整體嘗試失敗。
- AllSucessfulStrategy:所有驗證器都必須驗證成功。
如果你不熟悉 shiro,看不懂上面三種策略的含義,沒關係,本課程講的是設計模式,而不是 shiro 的使用,你只要知道 shiro 預設為我們提供了三種策略即可。
作為開發者,在使用 shiro 的時候,shiro 預設的策略未必符合我們的需求,比如我們要求三個驗證器中通過兩個才算通過,怎麼辦?
很簡單,shiro 這裡用的也是策略模式,我們只要自定義一個 MyAuthenticationStrategy 繼承 shiro 的 AbstractAuthenticationStrategy。咦?前面不是說實現介面嗎,這裡怎麼是繼承?變通,要懂得變通。設計模式不是一成不變的,重要的是這種程式設計思想。
然後在 MyAuthenticationStrategy 實現父類要求的方法,再修改配置檔案將當前驗證策略改為你定義的驗證策略:
authcStrategy = 你的包名.MyAuthenticationStrategy
優點
講完上面的例子,優點已經十分明顯了,那就是遵循了開閉原則,擴充套件性良好。
缺點
- 隨著你的策略增加,你的類也會越來越多。
- 所有的策略類都要暴露出去,所以如果你在實際開發中使用了策略模式,一定要記得寫好文件讓你的夥伴們知道已有哪些策略。就像 shiro 預設提供了三種驗證策略,就必須在文件中寫清楚,否則我們根本不知道如何使用。
當然,權衡利弊,跟優點比起來,這些缺點都不算事兒。