《敏捷軟件開發──原則、模式與實踐》閱讀筆記
Table of Contents
- 1. 敏捷開發
- 2. 極限編程
- 3. 設計原則
- 4. 經常使用設計模式
- 4.1. Command模式和Active Object
- 4.2. Template Method模式和Strategy模式:繼承和委託
- 4.3. Facade模式和Mediator模式
- 4.4. Singleton模式和Monostate模式
- 4.5. Null Object模式
- 4.6. Facotry模式
- 4.7. Composite模式
- 4.8. Observer模式
- 4.9. Abstract Server模式、Adapter模式和Bridge模式
- 4.10. Proxy模式和Stairway To Heaven模式
- 4.11. Visitor設計模式系列
- 4.12. State模式
- 5. 包的設計原則
1 敏捷開發
1.1 敏捷聯盟宣言
- 個體和交互賽過過程和工具
- 人是得到成功的最爲重要的因素。若是團隊中沒有優秀的成員,那麼就是使用好的過程也不能從失敗中挽救項目。 可是,孬的過程卻能夠使最優秀的團隊成員推進盜用。若是不能做爲一個團隊進行工做,那麼即便擁有最優秀的成員也同樣會慘敗。
- 能夠工做的軟件賽過面面俱到的文檔
- 對於團隊來講,編寫並維護一份系統原理和結構方面的文檔將老是一個好主意, 可是那套文檔應該是短小而且主題突出的。
- 客戶合做賽過合同談判
- 成功的項目須要有序、頻繁的客戶反饋。不是依賴於合同或者關於工做的陳述, 而是讓軟件的客戶和開發團隊密切地在一塊兒工做,並儘可能常常地提供反饋。
- 響應變化賽過遵循計劃
- 計劃不能考慮得過遠。道德,商務環境極可能會變化,這會會引發需求的變更。其次,一旦客戶看到系統開始運做, 他們極可能會改變需求。最後,即便咱們熟悉需求,而且確信它們不會發跡,咱們仍然不能很好地估算出開發它們須要的時間。
1.2 敏捷開發的原則
- 咱們最優先要作的是經過儘早的、待續的交付有價值的軟件來使客戶滿意
- 即便到了開發的後期,也歡迎改變需求。敏捷過程利用變化來爲客戶創造競爭優點。
- 常常性地交付能夠工做的軟件,交付的間隔能夠從幾周到幾個朋,交付的時間間隔越短越好。
- 在整個項目開發期間,業務人員和開發人員必須每天都在一塊兒工做。
- 圍繞被激勵起來的我的來構建項目。給他們提供所須要的環境和支持,而且信任他們可以完成工做。
- 在團隊內部,最具備效果而且富有效率的傳遞信息的方法,就是面對面的交談。
- 工做的軟件是首要的進度度量標準。
- 敏捷過程提倡可持續的開發速度。責任人、開發者和用戶應該可以保持一個長期的、恆定的開發速度。
- 不能地關注優秀的技能和好的設計會加強敏捷能力。
- 簡單──使未完成的工做最大化的──是根本的。
- 最好的構架、需求和設計出自於自組織的團隊。
- 每隔必定時間,團隊會在如何才能更有效地工做方面進行檢討,而後相應地對本身的行爲進行調整。
2 極限編程
極限編程是敏捷方法中最著名的一個。它由一系列簡單卻互相依賴的實踐組成。這些實踐結合在一塊兒造成了一個勝於部分結合的總體。 css
- 客戶做爲團隊成員
- 用戶素材
- 短交付週期
- 驗收測試
- 結對編程
- 測試驅動的開發方法
- 集體全部權
- 持續集成
- 可持續的開發速度
- 開放的工做空間
- 計劃遊戲
- 簡單的設計
- 重構
- 隱喻
3 設計原則
3.1 單一職責原則(SRP)
- 定義
- 就一個類而言,應該僅有一個引發它變化的緣由。
- 什麼是職責
- 在SRP中,咱們把職責定義爲「變化的緣由」。若是你可以想到多於一個的動機去改變一個類,那麼這個類就具備多於一個的職責。 有時,咱們很難注意到這一點。咱們習貫於以組的形式去考慮職責。
3.2 開放——封閉原則(OCR)
- 定義
- 軟件實體(類、模塊、函數等等)應該是能夠擴展的,可是不可修改的。
3.2.1 遵循開放──封閉原則設計出的模塊具備兩個主要的特徵
- 對於擴展開放
- 模塊的行爲是能夠擴展的。當應用的需求改變時,咱們能夠對模塊進行擴展,使其具備知足那些改變的新行爲。
- 對於更改是封閉的
- 對模塊行爲進行擴展時,沒必要改動模塊的源代碼或者二進制代碼。模塊的二進制可執行版本, 不管是可連接的庫、DLL或者Java的.jar文件,都無需改動。
3.3 Liskov替換原則(LSP)
- 定義
- 子類型必須可以替換掉它們的基類型。
- 相對知足
- 事實上,一個模型,若是孤立地看,里氏替換並不具備真正意義上的有效性,模型的有效性只能經過它的客戶程序來表現。
- 啓發示方法
-
- 在派生類中存在退化函數並不老是表示違反了LSP,可是當這種狀況存在時,
- 當在派生類中添加了其基類不會拋出的異常時,若是基類的使用者不指望這些異常,那麼把它們添加到派生類的方法中應付致使不可替換性。 此時要遵循LSP,要麼就必須改變使用者的指望,要麼派生類就不該該拋出這些異常。
3.4 依賴倒置原則(DIP)
- 定義
-
- 高層模塊不該該依賴於低層模塊,兩者都應該位賴於抽象。
- 抽象不該該依賴於細節,細節應該依賴於抽象。
- 解釋
- 請注意這裏的倒置不只僅是依賴關係的倒置,它也是接口全部權的倒置。當應用了DIP時,每每是客戶擁有抽象接口, 而它們的服務者則從這些抽象接口派生。
- 啓發示規則──領事於抽象
-
- 任何變量都不該該持有一個指向具體類的指針或者引用。
- 任何類都不該該從具體類派生。
- 任何方法都不該該覆寫它的任何基類中的已經實現了的方法。
- 若是一個具體類不太會改變,而且也不會建立其餘相似的派生類,那麼依賴於它並不會形成損害。
3.5 接口隔離原則(ISP)
- 定義
- 不該該強制客戶領事於它們不用的方法。若是強迫客戶程序依賴於那些它們不使用的方法, 那麼這些客戶程序就面臨着因爲這些未使用方法的改變所帶來的變動,這無心中致使了全部客戶程序之間的耦合。
4 經常使用設計模式
4.1 Command模式和Active Object
4.1.1 Command模式的優勢
- 經過對命令概念的封裝,能夠解除系統的邏輯互聯關係和實際鏈接的設備以前的耦合。
- 另外一個Command模式的常見用法是建立和執行事務操做。
- 解耦數據和邏輯,能夠將數據放在一個列表中,之後再進行實際的操做。
4.1.2 Active Object模式
- 描述
- Active Object模式是實現多線程控制的一項古老的技術。 控制核心對象維護了一個Command對象的鏈表。用戶能夠向鏈表中增長新的命令,或者調用執行動做,該動做只是遍歷鏈表,執行並去除每一個命令。
- RTC任務
- 採用該技術的變體一去構建多線程系統已是而且將會一直是一個很常見的實踐。這種類型的線程被稱爲run-to-completion任務(RTC), 由於每一個Command實例在下一個Command補全能夠運行以前就運行完成了。RTC的名字意味着Command實例不會阻塞。
- 共享運行時堆棧
- Command實例一經運行就必定得完成的的賦予了RTC線程有趣的優勢,尋就是它們共享同一個運行時堆棧。和傳統的多線程中的線程不一樣, 沒必要爲每一個RTC線程定義或者分配各處的運行時堆棧。這在須要大量線程的內存受限系統中是一個強大的優點。
4.2 Template Method模式和Strategy模式:繼承和委託
4.2.1 Template Method模式
- 描述
- Template Method模式展現了面向對象編程上諸多經典重用形式中的一種。其中通用算法被放置在基類中, 而且經過繼承在不一樣的具體上下文實現該通用算法。
- 代價
- 繼承是一種很是強的關係,派生類不可避免地要和它們的基類綁定在一塊兒。
4.2.2 Strategy模式
- 描述
- Strategy模式使用了一種很是不一樣的方法來倒置通用算法和具體實現之間的依賴關係。不是將通用的應用算法放進一個抽象基類中, 而是將它放進一個具體類中,在該具體類中定義一個成員對象,該成員對象實現了實際須要執行的具體算法, 在執行通用算法時,把具體工做委託給這個成員對象的所實現的抽象接口去完成。
4.2.3 對比
- 共同點
- Template Method模式和Strategy模式均可以用來分離高層的算法和的具體實現細節,都容許高速的算法獨立於它的具體實現細節重用。
- 差別
- Strategy模式也容許具體實現細節獨立於高層的算法重用,不過要唯一些額外的複雜性、內存以及運行時間開銷做爲代價。
4.3 Facade模式和Mediator模式
4.3.1 facade模式
- 使用場景
- 當想要爲一組具備複雜且全面的接口的對象提供一個簡單且特定的接口時,能夠使用Facade模式,以下圖所示的場景。
Figure 1: Facade模式封裝數據庫操做html
- 基於約定
- 使用Facade模式意味着開發人員已經接受了全部數據庫調用都要經過DB類的約定。若是任務一部分代碼越過該Facade直接去訪問java.sql, 那麼就違反了該約定。基於約定,DB類成爲了java.sql包的唯一代理。
4.3.2 Mediator模式
- 示例
- 圖中展現用一個JList和一個JTextField構造了一個QuickEntryMediator類的實例。QuickEntryMediator向JTextField註冊了一個匿名的
DocumentListener,每當文本發生變化時,這個listener就調用textFieldChanged方法。接着,該方法在JList中査找以這個文本爲前綴的元素並選中它。 JList和JTextField的使用者並不知道該Mediator的存在。它安靜地呆着,把它的策略施加在那些對象上,而無需它們的容許或者知曉。 java
Figure 2: Mediator模式python
4.3.3 對比
- 相同點
- 兩個模式都有着共同的目的,它們都把某種策略施加到另一組對象上,這些對象不須要知道具體的策略細節。
- 不一樣點
- Facade一般是約定的關注點,每一個人都贊成去使用該facade而不是隱藏於其下的對象;而Mediator則對用戶是隱藏的,
它的策略是既成事實而不是一項約定事務。 算法
4.4 Singleton模式和Monostate模式
4.4.1 Singleton模式
- 描述
- Singleton是一個很簡單的模式。Singleton實例是經過公有的靜態方法instance()訪問的,即便instance方法被屢次調用,
每次返回的都是指向徹底相同的實例的引用。Singleton類沒有公有構造函數,因此若是不使用instance方法,就沒法去建立它的實例。 sql
- 優勢
-
- 跨平臺。使用合適的中間件(例如RMI),能夠把Singleton模式擴展爲跨多個JVM和多個計算機工做
- 適用於任何類:只需把一個類的構造函數變成私有的,而且在其中增長相應的靜態函數和變量,就能夠把這個類變爲Singleton
- 能夠透過派生建立:給定一個類,能夠建立它的一個Singleton子類。
- 延遲求值(Lazy Evaluation):若是Singleton從未使用過,那麼就決不會建立它。
- 代價
-
- 摧毀方法未定義:沒有好的方法去推毀(destroy)一個Singleton,或者解除其職責。即便添加一個decommission方法把theInstance置爲null,
系統中的其餘模塊仍然持有對該Singleton實例的引用。這樣,隨後對instance方法的調用會建立另一個實例,導致同時存在兩個實例。 這個問題在C++中尤其嚴重,由於實例能夠被推毀,可能會致使去提領(dereference)一個已被摧毀的對象。 shell
- 不能繼承:從Singleton類派生出來的類並非Singleton。若是要使其成爲Singleton,必需要增長所需的靜態函數和變量。
- 效率問題:每次調用instance方法都會執行語句。就大多數調用而言,語句是多餘的。(使用JAVA的初始化功能可避免)
- 不透明性:Singleton的使用者知道它們正在使用一個Singleton,由於它們必需要調用instance方法
4.4.2 Monostate模式
- 描述
- 該模式經過把全部的變量都變成靜態變量,使全部實例表現得象一個對象同樣。
- 優勢
-
- 透明性:使用Monostate對象和使用常規對象沒有什麼區別,使用者不須要知道對象是Monostate
- 可派生性:Monostate的派生類都是Monostate。事實上,Monostate的全部派生類都是同一個Monostate的一部分。它們共享相同的靜態變量。
- 多態性:因爲Monostate的方法不是靜態的,因此能夠在派生類中覆寫它們。所以,不一樣的派生類能夠基於一樣的靜態變量表現出不一樣的行爲。
- 代價
-
- 不可轉換性:不能透過派生把常規類轉換成Monostate類。
- 效率問題:由於Monostate是真正的對象,因此會致使許多的建立和摧毀開銷。
- 內存佔用:即便從未使用Monostate,它的變量也要佔據內存空間。
- 平臺侷限性:Monostate不能跨多個JVM或者多個平臺工做。
4.4.3 對比
- Singleton模式使用私有構造函數和一個靜態變量,以及一下靜態方法對實例化進行控制和限制;Monostate模式只是簡單地把對象的全部變量變成靜態的。
- 若是但願經過派生去約束一個現存類,而且不介意它的全部調用都都必需要調用instance方法來獲取訪問權,那麼Singleton是最合適的。
- 若是但願類的單一性本質對使用者透明,或者但願使用單一對象的多態派生對象,那麼Monostate是最合適的。
4.5 Null Object模式
Employee e = DB.getEmployee("Bob"); if (e != null && e.isTimeToPay(today)) e.pay();
- 場景
- 考慮如上代碼,咱們常使用這&&這樣的表達式進行空值檢查,大多數人也曾因爲忘記進行null檢查而受挫。該慣用方法雖然常見,
但倒是醜陋且易出錯的。經過讓getEmployee方法拋出異常,能夠減小出錯的可能,但try/catch塊比null檢查更加醜陋。 這種場景下能夠使用Null Object模式來解決這些問題(以下圖所示)。 數據庫
Figure 3: Null Object模式編程
4.6 Facotry模式
- 問題示例
-
依賴倒置原則(DIP)告訴咱們應該優先依賴於抽象類,而避兔依賴於具體類。當這些具體類不穩定時,更應該如此。 所以,該代碼片斷違反了這個原則:
Circle c= new Circle(origin, 1)
,Circle是一個具體類。 因此,建立 Circle類實例的模塊確定違反了DIP。事實上,任何一行使用了new關鍵字的代碼都違反了DIP。 - 應用場景
- Factory模式容許咱們只依賴於抽象接口就能建立出具體對象的實例。 因此,在正在進行的開發期間,若是具體類是高度易變的,那麼該模式是很是有用的。
4.6.1 可替換的工廠
使用工廠的一個主要好處就是能夠把工廠的一種實現替換爲另外一種實現。這樣,就能夠在應用程序中替換一系列相關的對象。 設計模式
Figure 4: 可替換的工廠
4.6.2 合理使用工廠模式
嚴格按照DIP來說,必需要對系統中全部的易變類使用工廠。此外,Factory模式的威力也是誘人的。這兩個因素有時會誘使開發者把工廠做爲缺省方式使用。 我不推薦這種極端的作法。我不是一開始就使用工廠,只是在很是須要它們的狀況下,我才把它們放入到系統中。 例如,若是有必要使用Proxy模式,那麼就可能有必要使用工廠去建立持久化對象。或者,在單元測試期間, 若是遇到了必需要欺騙一個對象的建立者的狀況時,那麼我極可能會使用工廠。可是我不是開始就假設工廠是必要的。
使用工廠會帶來複雜性,這種複雜性一般是能夠避免的,尤爲是在一個正在演化的設計的初期。 若是缺省地使用它們,就會極大地增長擴展設計的難度。爲了建立一個新類,就必需要建立出至少4個新類: 兩個表示該新類及其工廠的接口類,兩個實現這些接口的具體類。
4.7 Composite模式
Figure 5: Composite模式
- 描述
- 上圖展現了Composite模式的基本結構。基類Shape有兩個派生類:Circle和Square,第三個派生類是一個組合體。
CompositeShape持有一個含有多個Shape實例的列表。當調用CompositeShape的draw()方法時,它就把這個方法委託給列表中的每個Shape實例。 所以,一個CompositeShape實例就像是一個單一的Shape,能夠把它傳遞給任何使用Shape的函數或者對象,而且它表現得就像是個Shape。 不過,實際上它只是一組Shape實例的代理。
4.8 Observer模式
- 問題描述
- 有一個計時器,會捕獲來自操做系統的時鐘中斷,生成一個時間戳。如今咱們想實現一個數字時鐘,將時間戳轉換爲日期和時間,並展現。 一種可行的方式是不停輪詢獲取最新的時間戳,而後計算時間。但時間戳只有在捕獲到時鐘中斷時,都會發生變化,輪詢是會形成CPU的極大浪費。
- 描述
- 另外一種解決方案時在計時器時間發生變化時,告知數字時鐘,數字時鐘既而更新時間。這裏,數字時鐘爲計時器的觀察者(Observer)。
- 示例
-
Figure 6: Observer模式示例
其中MockTimeSink是MockTimeSource的觀察者,經過TestClockDriver將MockTimeSink註冊到MockTimeSource的觀察者隊列中。 當MockTimeSource發生變化時,它會調用notifyObservers()方法遍歷各個觀察者,並調用其update()方法。 MockTimeSource實現觀察者(Observer)接口,當被通知時,獲取當前時間並展現。
- 推模型與拉模型
- 上述示例,觀察者在接收到消息後,查詢被觀察者獲得數據,這種模型被稱爲「拉」模型。相應的若是數據是經過update方法傳遞, 則爲「推」模型。
4.9 Abstract Server模式、Adapter模式和Bridge模式
4.9.1 Abstract Server模式
- 問題
-
考慮實現一個簡單的開關控制器,能夠控制燈泡的開關,一種簡單的設計以下
Figure 7: 一種簡單燈泡實現
這個設計違反了兩個設計原則:依賴倒置原則(DIP)和開放封閉原則(OCP)。對DIP的違反是明顯的,Switch依賴了具體類Light。 DIP告訴咱們要優先依賴於抽象類。對OCP的違反雖然不那麼明顯,可是更加切中要害:在任何須要Switch的地方都要附帶上Light, 不能容易地擴展Switch去管理除Light外的其餘對象,如當須要控制音樂的開關時(好比在回家後,打開門,同時打開燈光和音樂的開關)。
- 描述
-
爲了解決這個問題,能夠使用一個最簡單的設計模式:Abstract Server模式。在Switch和Light之間引入一個接口, 這樣就使得Switch可以控制任何實現了這個接口的東西,這當即就知足了DIP和OCP
Figure 8: AbstractServer模式
- 誰擁有接口
- 接口屬於它的客戶,而不是它的派生類。 客戶和接口之間的邏輯綁定關係要強於接口和它的派生類之間的邏輯綁定關係。 它們之間的關係強到在沒有Switchable有的狀況下就沒法使用Switch;可是,在沒有Light的狀況下卻徹底能夠使用Switch。 邏輯關係的強度和實體(physical)關係的強度是不一致的。繼承是一個比關聯強得多的實體關係。
- 如何打包
- 在20世紀90年代初期,咱們一般認爲實體關係支配着一切,有使多人都建議把繼求層次構一塊兒放到同一個實體包中。 這彷佛是合理的,由於繼承是一種很是強的實體關係。可是在最近10年中,咱們已經認識到繼承的實體強度是一個誤導, 而且繼承層次結構一般也不該該被打包在起。相反,每每是把客戶和它們控制的接口打包在一塊兒。
4.9.2 Adapter模式
- 問題
- 上述Adapter設計可能會違反單一職責原則(SRP):咱們把Lght和Switchable定在一塊兒,而它們可能會由於不一樣的緣由改變。 另外,若是沒法把繼承關係加到Light上該怎麼辦呢,好比從第三方購買了Light而沒有源代碼。這個時候能夠使用Adapter模式。
- 描述
-
定義一個適配器,使其繼承Switchable接口,並將全部接口的實現委託給實際的Light執行。 事實上,Light對象中甚至不須要有turnOn和turnOff方法。
Figure 9: Adapter模式
- 使用Adapter模式隱藏雜湊體
- 原設計
-
請考慮一下下圖中的情形:有大量的調制解調器客戶程序,它們都使用Modem接口。 Modem接口被幾個派生類HayesModem、UsRoboticsModem和ErniesModem實現。這是常見的方案,它很好地遵循了OCP、LSP和DIP。
Figure 10: 調制解調器問題
- 攪亂設計的需求變更
- 如今假定客戶提出了一個新的需求:有某些種類的調制解調器是不撥號的,它們被稱爲專用調制解調器, 由於它們位於一條專用鏈接的兩端。有幾個新應用程序使用這些專用調制解調器,它們無需撥號,咱們稱這些使用者爲DedUser。 可是,客戶但願當前全部的調制解調器客戶程序均可以使用這些專用調制解調器,他們不但願去更改許許多多的調制解調器客戶應用程序, 因此徹底能夠上這些調制解調器客戶程序去撥一些假(dummy)電話號碼。
- 沒法使用的理想解決方案
-
若是能選擇的話,咱們會把系統的設計更改成下圖所示的那樣。咱們會使用ISP把撥號和通訊功能分離爲兩個不一樣的接口。 原來的調制解調器實現這兩個接口,而調制解調器客戶程序使用這兩個接口。DedUser只使用Modem接口, 而DedicateModem只實現Modem接口。糟糕的是,這樣作會要求咱們更改全部的調制解調器客戶程序,這是客戶不容許的。
Figure 11: 理想解決方案
- 一種簡單的解決方案
- 一個可能的解決方案是讓DedicatedModem從Modem派生而且把dial方法和hangup方法實現爲空
- 存在的問題
- 兩個退化函數預示着咱們可能違反了LSP;另外,基類的使用者可能指望dial和hangup會明顯地改變調制解調器的狀態。 DedicatedModem中的退化實現可能會違背這些指望:假定調制解調器客戶程序指望在調用dial方法前調制解調器處於體眠狀態, 而且當調用hangup時返回休眠狀態。換句話說,它們指望不會從沒有撥號的調制解調器中收到任何字符。 DedicatedModem違背了這個指望。在調用dial以前,它就會返回字符,而且在調用hangup調用以後,仍會不斷地返回字符。 因此,DedicatedModem可能會破壞某些調制解調器的使用者。
- 雜湊體的出現
-
咱們能夠在DedicatedModem的dial方法和hangup方法中模擬一個鏈接狀態。 若是尚未調用dial,或者已經調用了hangup,就能夠拒絕返回字符。 若是這樣作的話,那麼全部的調制解調器客戶程序均可以正常工做而且也沒必要更改。只要讓DedUser去調用dial和hangup便可。 你可能認爲這種作法會令那些正在實現DedUser的人以爲很是沮喪,他們明明在使用DedicatedModem。 爲何他們還要去調用dial和hangup呢?不過,他們的軟件尚未開始編寫,因此還比較容易讓他們按照咱們的想法去作。
Figure 12: 使用雜湊體解決問題
- 醜陋的雜湊體
- 幾個月後,已經有了大量的DedUser,此時客戶提出了一個新的更改。客戶但願可以撥打任意長度的電話號碼, 他們須要去撥打國際電話、信用卡電話、PIN標識電話等等,而原有的電話號碼使用char[10]存儲電話號碼。 顯然,全部的調制解調器客戶程序都必須更改,客戶贊成了對調制解調器客戶程序的更改。 糟糕的是,如今咱們必需要去告訴DedUser的編寫者,他們必需要更改他們的代碼! 你能夠想象他們聽到這個會有多氣憤,他們之因此調用了dial是由於咱們告訴他們必需要這樣作,而他們根本不須要dial和hangup方法。
- 使用適配器模式隱藏雜湊體
-
DedicatedModem不從Modem繼承,調制解調器客戶程序經過DedicatedModemAdapter間接地使用DedicatedModem。 在這個適配器的dial和hangup的實現中去模擬鏈接狀態,同時把send和receive調用委託給DedicatedModem。 請注意,雜湊體仍然存在,適配器仍然要模擬鏈接狀態。然而,請注意,全部的依賴關係都是從適配器發起的。 雜湊體和系統隔離,藏身於幾乎無人知曉的適配器中,只有在某處的某個工廠纔可能會實際依賴於這個適配器。
Figure 13: 使用Adapter模式解決問題
4.9.3 Bridge模式
- 解決調制解調器問題的另外一種思路
-
看待調制解調器問題,還有另一個方式,對於專用調制解調器的, 須要向Modem類型層次結構中增長了一個新的自由度,咱們可讓DialModem和DedicatedModem從Modem派生。 以下圖所示,每個葉子節點要麼向它所控制的硬件提供撥號行爲,要麼提供專用行爲。 DedicatedHayesModem對象以專用的方式控制着Hayes品牌的調制解調器,而HayesDialModem則以撥號的方式控制着Hayes品牌的調制解調器。
Figure 14: 擴展層次結構解決問題
- 存在的問題
- 這不是一個理想的結構,每當增長一款新硬件時,就必須建立兩個新類個針對專用的狀況,一個針對撥號的狀況。 而每當增長一種新鏈接類型時,就必須建立三個新類,分別對應三款不一樣的硬件。 若是這兩個自由度根本就是不穩定的,那麼不用多久,就會出現大量的派生類。
- Bridge模式的使用
-
在類型層次結構具備多個自由度的狀況中,Bridge模式一般是有用的,咱們能夠把這些層次結構分開並經過橋把它們結合到一塊兒, 而不是把它們合併起來。
Figure 15: 使用Bridge模式解決問題
- Bridge模式的優點
- 這個結構雖然複雜,可是頗有趣,改造爲該模式時,不會影響到調制解調器的使用者,而且還徹底分離了鏈接策略和硬件實現。 ModemConnectController的每一個派生類表明了一個新的鏈接策略。 在這個策略的實現中能夠使用sending、receiveImp、dialImp和hangup,新imp方法的增長不會影響到使用者。 能夠使用ISP來給鏈接控制類增長新的接口。這種作法能夠建立出一條遷移路徑, 調制解調器的客戶程序能夠沿着這條路徑慢慢地獲得一個比dial和hangup層次更高的API。
4.10 Proxy模式和Stairway To Heaven模式
4.10.1 Proxy模式
- 問題
- 假設咱們編寫一個購物車系統,這樣的系統中會有一些關於客戶、訂單(購物車) 及訂單上的商品的對象。 若是向訂單中增長新商品條目,並假設這些對象所表明的數據保存在一個關係數據庫中的, 那麼咱們在添加商品的代碼中就不可避免的使用JDBC去操做關係數據模型──客戶、訂單、商品屬於不一樣的表, 添加商品到客戶在關係數據庫中的體現,就是在創建外鍵聯繫。這嚴重違反了SRP,而且還可能違反CCP。 這樣把商品條目和訂單的概念與關係模式(schema)和SQL的概念混合在了一塊兒。不管任何緣由形成其中的一個概念須要更改, 另外一個概念就會受到影響。
- Proxy模式
-
請考慮一下Product類,咱們經過用一個接口來代替它實現了對它的代理,這個接口具備Product類的全部方法。 ProductImplementation類是一個簡單的數據對象,同時ProductDbProxy實現了Product中的全部方法, 這些方法從數據庫中取出產品,建立一個ProductImplementation實例,而後再把邏輯操做委託給這個實例。
Figure 16: Proxy模式
- 優勢
- Product的使用者和ProductImplementation都不知道所發生的事情,數據庫操做在這二者都不知道的狀況下被插入到應用程序中。 這正是 PROXY模式的優勢。理論上,它能夠在兩個協做的對象都不知道的狀況下被插入到它們之間。 所以,使用它能夠跨越像數據庫或者網絡這樣的障礙,而不會影響到任何一個參與者。
4.10.2 Stairway To Heaven模式
- Stairway To Heaven模式
-
Stairway To Heaven模式是另外一個能夠完成和Proxy模式同樣的依賴關係倒置的模式。 咱們引入一個知道數據庫的抽象類PersistentObject,它提供了read和write兩個抽象方法, 同時提供了一組實現方法做爲實現read和write所須要的工具。在PersistentProduct的read和write的實現中, 會使用這些工具把Product的全部數據字段從數據庫中讀出或者寫入到數據庫。 現使PersistentProduct同時繼承Product的PersistentObject類,以下圖所示。
Figure 17: StairwayToHaven模式
- 優點
- Product的使用者並不須要知道PersistentObject,在須要數據庫操做的少許代碼中,則能夠將類型向下轉換(如dynamic cast), 將Product類轉換成實際的PersistentObject類,調用其write和read方法。 這樣,就能夠將有關數據庫的知識和應用程序的業務規則徹底分離開來。
4.11 Visitor設計模式系列
- 問題
- 在Modem對象的層次結構,基類中具備對於全部調制解調器來講公共的通用方法,派生類表明着針對許多不一樣調制解調器廠商和類型的驅動程序。 假設你有一個需求,要增長一個configureForUnix方法,調制解調器進行配置,使之能夠工做於UNX操做系統中。 由於每一個不一樣廠商的調制解調器在UNIX中都有本身獨特的配置方法和行爲特徵,這樣在每一個調制解調器派生類中,該函數的實現都不相同, 這樣咱們將面臨一種糟糕的場景,增長configureForUnix方法其實反映了一組問題:對於Windows該怎麼辦?對於MacOs該怎麼辦呢? 對於Linux又該怎麼辦呢?咱們難產要針對每一種新操做系統向Modem層次結構中增長一個新方法嗎? 這種作法是醜陋的,咱們將永遠沒法封閉Modem接口,每當出現一種新操做系統時,咱們就必須更改該接口並從新部署全部的調制解調器軟件。
- Visitor模式系列
- Visitor模式系列容許在不更改現有類層次的狀況下向其中增長新方法。該系列中的模式包括:Visitor模式、 Acyclic Visitor模式、Decorator模式、Extension Object模式。
4.11.1 Visitor模式
Visitor模式使用了雙重分發技術。之因此被稱爲雙重分發是由於它涉及了兩個多態分發,第一個分發是accept函數, 該分發辨別出所調用的accept方法所屬對象的類型,第二個分發是viist方法,它辨別出要執行的特定函數。(以下圖所示)
Figure 18: Visitor模式
Visitor模式中的兩次分發造成了個功能矩陣,在調制解調器的例子中,矩陣的一條軸是不一樣類型的調制解調器, 另外一條軸是不一樣類型的操做系統。該矩陣的每一個單元都被一項功能填充,該功能很好的解決把特定的調制解調器初始化爲能夠在特定的操做系統中使用的問題。
4.11.2 Acyclic Visitor模式
- 問題
- 在Visitor模式中,被訪問層次結構的基類(Modem)依賴於訪問者層次結構的基類(Modem Visitor)。 同時,訪問者層次結構的基類中對於被訪問層次結構中的每一個派生類都有一個對應函數。 這樣, 就有造成了一個依賴環,把全部被訪問的派生類(全部的調制解調器)綁定在一塊兒,致使很難實現對訪問者結構的增量編譯, 而且也很難向被訪問層次結構中增長新的派生類。
- Acyclic Visitor模式
-
該變體把Visitor基類(modemVisitor)變成退化的,從而解除了依賴環,這個類中不存在任何方法, 使它再也不依賴於被訪問層次結構的派生類(以下圖所示)。
Figure 19: AcyclicVisitor模式
對於被訪問層次結構的每一個派生類,都有個對應的訪問者接口,且訪問者派生類派生自這些訪問者接口。 這是一個從派生類到接口的180度旋轉,被訪問派生類中的accept函數把Visitor基類轉型(cast)爲適當的訪問者接口。若是轉型成功, 該方法就調用相應的visit函數。
- 優勢
- 這種作法解除了環賴環,而且更易於增長被訪問的派生類以及進行增量編譯。
- 缺點
- 糟糕的是,它一樣也使得解決方案更加複雜了。更糟糕的是,轉型花費的時間依賴於被訪問層次結構的寬度和深度,因此很難進行測定。 因爲轉型須要花費大量的執行時間,而且這些時間是不可預測的,因此Acycllic Visitor模式不適用於嚴格的實時系統。 該模式的複雜性可能一樣會使它不適用於其餘的系統,可是對於那些被訪問的層次結構不穩定,而且增量編譯比較重要的系統來講, 該模式是一個不錯的選擇。
- 動態轉型帶來的稀疏特性
- 正像Visitor模式建立了一個功能矩陣(一個軸是被訪問的類型,另外一個軸是要執行的功能)同樣, Acyclic Visitor模式建立了一個稀疏矩陣。訪問者類不須要針對每個被訪問的派生類都實現visit函數。 例如,若是Ernie調制解調器不能夠配置在UNIX中,那麼UnixModemConfigurator就不會實現EnineVisitor接口。 所以,Acyclic Visitor模式容許咱們忽略某些派生類和功能的組合。有時,這多是一個有用的優勢。
4.11.3 Decorator模式
- 問題
- 假設咱們有ー個具備不少使用者的應用程序,每一個使用者均可以坐在他的計算機前,要求系統使用該計算機的調制解調器呼叫另外一臺計算機。 有些用戶但願聽到撥號聲,有些用戶則但願他們的調制解調器保持安靜。一種簡單的解決方案是在全部的Modem派生類中加入邏輯, 在撥號前詢問使用者是否靜音;另外一種解決方案,是將Modem接口變爲一個類,將通用的邏輯放在基類中,而派生類只實現撥號動做。 前一種方案,須要在每個派生類中加入重複的代碼,並須要新派生類的開發者必需要記着複製這段代碼。然後一種方案雖然更好, 可是否大聲撥號與調制解調器的內在功能沒有任何關係,這違反了單一職責原則。
- Decorator模式
-
Decorator模式經過建立一個名爲LoudDialModem的全新類來解決這個問題。 LoudDialModem派生自Modem, 而且攜有的一個Modem實例,它捕獲對dial函數的調用並在委託前撥號動做前把音量設高。
Figure 20: Decorator模式
4.11.4 Extension Object模式
還有另一種方法能夠在不更改類層次結構的狀況下向其中增長功能,那就是使用Extension Object模式。 這個模式雖然比其餘的模式複雜一些,可是它也更強大、更靈活一些。
- Extension Object模式
-
層次結構中的每一個對象都持有一個特定擴展對象(Extension Object)的列表。 同時,每一個對象也提供一個經過名字查找擴展對象的方法,擴展對象提供了操做原始層次結構對象的方法。 舉個例子,假設有一個材料單系統,咱們想讓其中的每一個對象都有將本身數據導出爲XML和CVS的能力, 這個需求和調制解調器對不一樣操做系統支持的需求相似,此時,除了Acyclic Visitor模式外, 咱們也能夠使用Extension Object模式(以下圖所示):Part接口定義了添加擴展對象和獲取擴展對象的方法, 同時,針對Assembly和PiecePart都實現了相應的XML和CVS導出能力的類,剩下只須要經過類構造方法或使用工廠模式, 將擴展對象裝配到相應的數據對象中便可。
Figure 21: ExtensionObject模式
4.12 State模式
- 使用場景
- 有限狀態機(FSM)是軟件寶庫中最有用的抽象之一, 它們提供了一個簡單、優雅的方法去揭示和定義複雜系統的行爲。 它們一樣也提供了一個易於理解、易於修改的有效實現策略。在系統的各個層面, 從控制髙層邏輯的GUP到最低層的通信協議,都會使用它們。它們幾乎適用於任何地方。 實現有限狀態機的經常使用方法包括switch-case語句、使用轉移表進行驅動以及State模式。
- 示例場景
- 現假設去實現一個十字門的狀態機,當門「鎖住」時,「投幣」後可將十字門「解鎖」; 在門「鎖住」的狀態下,若是有人嘗試「經過」,十字門將「發出警報」; 在門「解鎖」的狀態下,人「經過」十字門後,門自動「鎖住」; 在門「解鎖」的狀態下,若是繼續「投幣」,十字門將「播報感謝語音」。
- State模式
-
以下圖所示,Turnstyle類擁有關於事件的公有方法以及關於動做的受保護方法, 它持有一個指向TurnstyleState接口的引用,而TurnstyleState的兩個派生類表明FSM的兩個狀態。 當Turnstyle的兩個事件方法中的一個被調用時,它就把這個事件委託給TurnstyleState對象。 其中TurnstyleLockedState實現了LOCKED狀態下的相應動做, TurnstyleUnlockedState的方法實現了UNLOCKED狀態下的相應動做。 爲了改變FSM的狀態,就要把這兩個派生類之一的實例賦給Turnstyle對象中的引用。
Figure 22: State模式
- State模式與Strategy模式對比
-
這兩個模式都有一個上下文類,都委託給一個具備幾個派生類的多態基類。 不一樣之處在於,在State模式中,派生類持有回指向上下文類的引用,全部狀態設置方法都在上下文類中實現, 派生類的主要功能是使用這個引用選擇並調用上下文類中的方法進行狀態轉移。 而在Strategy模式中,不存在這樣的限制以及意圖,Strategy的派生類沒必要持有指向上下文類的引用, 而且也不須要去調用上下文類的方法。因此,全部的State模式實例一樣也是Strategy模式實例, 可是並不是全部的Strtegy模式實例都是State模式實例。
Figure 23: State模式與Strategy模式對比
- 優點
- State模式完全地分離了狀態機的邏輯和動做,動做是在Context類中實現的, 而邏輯則是分佈在State類的派生類中,這就使得兩者能夠很是容易地獨立變化、互不影響。 例如,只要使用State類的另一個派生類, 就能夠很是容易地在一個不一樣的狀態邏輯中重用Context類的動做。 此外,咱們也能夠在不影響State派生類邏輯的狀況下建立Context子類來更改或者替換動做實現。 該方法的另一個好處就是它很是高效,它基本上和嵌套switch/case實現的效率徹底同樣。 所以,該方法既具備表驅動方法的靈活性,又具備嵌套switch/case方法的效率。
- 缺點
- 這項技術的代價體如今兩個方面。第一,State派生類的編寫徹底是一項乏味的工做, 編寫一個具備20個狀態的狀態機會令人精神麻木。第二,邏輯分散, 沒法在一個地方就看到整個狀態機邏輯,所以,就使得代碼難以維護。 這會令人想起嵌套switch/case方法的晦澀性。
5 包的設計原則
5.1 粒度:包的內聚性原則
5.1.1 重用發佈等價原則(Release Reuse Equivalency Principle)
- 定義
- 重用的粒度就是發佈的粒度
RFP指出,一個包的重用粒度能夠和發佈粒度同樣大,咱們所重用的任何東西都必須同時被髮布和跟蹤。 簡單的編寫一個類,而後聲稱它是可重用的作法是不現實的。只有在創建一個跟蹤系統,爲潛在的使用者提供所須要的變動通知、安全性以及支持後, 重用纔有可能。
5.1.2 共同重用原則(Common Reuse Principle)
- 定義
- 一個包中的全部類應該是共同重用的。若是重用了包中的一個類,那麼就要重用包中的全部類。
類不多會孤立的重用,通常來講,可重用的類須要與做爲該可重用抽象一部分的其餘類協做。
CRP規定了這些類應該屬於同一個包。在這樣的一個包中,咱們會看到類之間有不少的互相依賴。一個簡單的例子是容器類以及與它關聯的迭代器類, 這些類彼此之間緊密耦合在一塊兒,所以必須共同重用,因此它們應該在同一個包中。
所以,我想確信當我依賴於一個包時,我將依賴於那個包中的每個類。換句話說,我想確信我放入一個包中的全部類是不可分開的, 僅僅依賴於其中一部分的狀況是不可能的。不然,我將要進行沒必要要的從新驗證和從新發行,而且會白費至關數量的努力。
5.1.3 共同封閉原則(Common Closure Principle)
- 定義
- 包中的全部類對於同一類性質的變化應該是共同封閉的。一個變化若對一個包產生影響,則將對該包中的全部類產生影響,
而對於其餘的包不形成任何影響。
這是單一職責原則對於包的從新規定。正如SRP規定的一個類不該該包含多個引發變化的緣由那樣,這條原則規定了一個包不該該包含多個引發變化的緣由。
CCP鼓勵咱們把可能因爲一樣的緣由而更改的全部類共同彙集在同一個地方。若是兩個類之間有很是緊密的綁定關係,無論是物理上的仍是概念上的, 那麼它們老是會一同進行變化,於是它們應該屬於同一個包中。這樣作會減小軟件的發佈、從新驗證、從新發行的工做量。 CCP經過把對於一些肯定的變化類型開放的類共同組織到同一個包中,從而加強了上述內容。於是,當需求中的一個變化到來時, 那個變化就會頗有可能被限制在最小數量的包中。
5.1.4 總結
過去,咱們對內聚性的認識要遠比上面3個原則所蘊含的簡單,咱們習慣於認爲內聚性不過是指一個模塊執行一項而且僅僅一項功能。 然而,這3個關於包內聚性的原則描述了有關內聚性的更加豐富的變化。在選擇要共同組織到包中的類時,必需要考慮可重用性與可開發性之間的相副作用力。 在這些做用力和應用的須要之間進行平衡不是一件簡單的工做。此外,這個平衡幾乎老是動態的。 也就是說,今天看起來合適的劃分到了明年也許就再也不合適了。 所以,當項目的重心從可開發性向可重用性轉變時,包的組成極可能會變更並隨時問而演化。
5.2 穩定性:包的耦合性原則
5.2.1 無環依賴原則
- 定義
- 在包的依賴關係圖中不容許存在環
若是開發環境中存在有許多開發人員都在更改相同的源代碼文件集合的狀況,那麼就會出現由於他人的更改致使你沒法構建的狀況。 當項目和開發團隊的規模增加時,這種問題就會帶來可怕的噩夢,每一個人都忙於一遍遍地更改他們的代碼,試圖使之可以相容於其餘人所作的最近更改。
經過將開發環境劃分紅可發佈的包,能夠解決這個問題,這些包能夠做爲工做單元被一個開發人員或者一個開發團隊修改,將一個包能夠工做時, 就把它發佈給其餘開發人員使用。所以,全部的開發團隊都不會受到其餘開發團隊的支配,對一個包做的理性沒必要當即反應至其餘開發團隊中, 每一個開發團隊獨立決定什麼時候採用上前所使用的包的新版本。此外,集成是以小規模增量的方式進行。
這是一個很是簡單、合理的過程,並被普遍使用。不過,要使其可以工做,就必需要對包的依賴關係結構進行管理,包的依賴關係結構中不能有環。
5.3 啓發:不能自頂向下設計包的結構
這意味着包結構不是設計系統時首先考慮的事情之一。事實上,包結構應該是隨着系統增加、變化而逐步演化的。
事實上,包的依賴關係圖和描繪應用程序的功能之間幾乎沒有關係,相反,它們是應用程序可構建性的映射圖。 這就是爲什麼不在項目開始時設計它們的緣由。在項目開始時,沒有軟件可構建, 所以也無需構建映射圖。 可是,隨着實現和設計初期累積的類愈來愈多,對依賴關係進行管理,避免項目開發中出現晨後綜合症的須要就不斷增加。 此外,咱們也想盡量地保持更改的局部化,因此咱們開始關注SRP和CCP,並把可能會一同變化的類放在一塊兒
若是在設計任何類以前試圖去設計包的依賴關係結構,那麼極可能會遭受慘敗。咱們對於共同封閉尚未多少了解,也尚未覺察到任何可重用的元素, 從而幾乎固然會建立產生依賴環的包。因此,包的依賴關係結構是和系統的邏輯設計一塊兒增加和演化的。
5.4 穩定依賴原則(Stable Dependencies Principle)
- 定義
- 朝着穩定的方向進行依賴
對於任何包而言,若是指望它是可變的,就不該該讓一個難以更改的包依賴於它!不然,可變的包一樣也會難以更改。
5.4.1 穩定性
韋伯斯特認爲,若是某物「不容易被移動」,就認爲它是穩定的。穩定性和更改所須要的工做量有關。 硬幣不是穩定的,由於推倒它所需的工做量是很是少的。可是,桌子是很是穩定的,由於推倒它要花費至關大的努力。
5.4.2 穩定性度量
- (Ca)輸入耦合度(Afferent Coupling):指處於該包的外部並依賴於該包內的類的類的數目
- (Ce)輸出耦合度(Efferent Coupling):指處於該包的內部並依賴於該包外的類的類的數目
- 不穩定性I: \(I = C_e / (C_a + C_e)\)
SDP規定一個包的I度量值應該大於它所依賴的包的I度量值,也就是說,度量值應該順着依賴的方向減小。
若是一個系統中全部的包都是最大程度穩定的,那麼該系統就是不能改變的。這不是所但願的情形。 事實上,咱們但願所設計出來的包結構中,一些包是不穩定的而另一些是穩定的。 其中可改變的包位於頂部並依賴於底部穩定的包,把不穩定的包放在圖的頂部是一個有用的約定, 由於任何向上的箭頭都意味着違反了SDP。
5.5 穩定抽象原則(Stable Abstractions Principle)
- 定義
- 包的抽象程度應該和其穩定程度一致
該原則把包的穩定性和抽象性聯繫起來。它規定,一個穩定的包應該也是抽象的,這樣它的穩定性就不會使其沒法擴展。 另外一方面,它規定,一個不穩定的包應該是具體的,由於它的不穩定性使得其內部的具體代碼易於更改。
SAP和SDP結合在一塊兒造成了針對包的DIP原則。這樣說是準確的,由於SDP規定依賴應該朝着穩定的方向進行,而SAP則規定穩定性意味着抽象性。 所以,依賴應該朝着抽象的方向進行。然而,DIP是一個處理類的原則。類沒有灰度的概念(the shades of grey)。 一個類要麼是抽象的,要麼不是。SDP和SAP的結合是處理包的,而且容許一個包是部分抽象、部分穩定的。
5.5.1 抽象性度量
- Nc:包中類的總數N
- Na:包中抽象類的數目。請記住,一個抽象類是一個至少具備一個純接口(pure interface)的類,而且它不能被實例化。
- A是一個測量包抽象程度的度量標準。它的值就是包中抽象類的數目和所有類的數目的比值: \(A = N_a / N_c\)
5.6 主序列
如今,咱們來定義穩定性(I)和抽象性(A)之間的關係。
Figure 24: 穩定-抽象座標-被排除區域
咱們能夠建立一個以A爲縱軸,I爲橫軸的座標圖。若是在座標圖中繪製出兩種「好」的包類型,會發現那些最穩定、最抽象的包位於左上角(0,1)處。 那些最不穩定、最具體的包位於右下角(1,0)處。 並不是全部的包都會落在這兩個位置,包的抽象性和穩定性是有程度的。例如,一個抽象類派生自另外一個抽象類的狀況是很常見的。 派生類是具備依賴性的抽象體。所以,雖然它是最大限度抽的,可是它卻不是最大程度穩定的,它的依賴性會下降它的穩定性。 由於不能強制全部的包都位於(0,1)或者(1,0),因此必需要假定在A/I圖上有一個定義包的合理位置的點的軌跡。 咱們能夠經過找出包不該該在的位置(也就是,被排除的區域)來推斷該軌跡的含意。
- 痛苦地帶(Zone of Pain)
- 考慮一個在(0,0)附近的包,這是一個高度穩定且具體的包,咱們不想要這種包,由於它是僵化的:沒法對它進行擴展,由於它不是抽象的; 而且因爲它的穩定性,也很難對它進行更改。所以,一般,咱們不指望看到設計良好的包位於(0,0)附近。 (0,0)周圍的區域被排除在外,咱們稱之爲痛苦地帶。
- 無用地帶(Zone of Uselessness)
- 考慮一個在(1,1)附近的包,這不是一個好位置,由於該位置處的包具備最大的抽象性卻沒有依賴者。 這種包是無用的,所以,稱這個區域爲無用地帶
- 主序列(Main Sequence)
- 顯然,咱們想讓可變的包都儘量地遠離這兩個被排除的區域。 那些距離這兩個區域最遠的軌跡點組成了鏈接和(1,0)和(0,1)的線。該線稱爲主序列。
5.6.1 到主序列的距離
- 距離D
- \(D = |A + I - 1| / \sqrt{2}\)
- 規範化距離D`
- \(D` = | A + I - 1|\)