設計模式

設計模式

 

第1章 代碼無錯就是優?—代碼無錯未必優—簡單工廠模式

1.1 面試受挫
1.2 初學者代碼毛病
1.3 代碼規範
1.4 面向對象編程
  所有編程初學者都會有這樣的問題。就是碰到問題就直覺地用計算機能夠理解的邏輯來描述和表達待解決的問題及具體的求解過程。這其實是用計算機的方式去思考,比如這個計算器程序,先要求輸入兩個數和運算符號,然後根據運算符號判斷選擇如何運算,得到結果,這本身沒錯,但這樣的思維卻使得我們的程序只爲滿足實現當前的需求,程序不容易維護,不容易擴展,更不容易複用。從而達不到高質量代碼的要求。
1.5 活字印刷,面向對象(故事請見書)
  第一,要改,只需要改要改的字,此爲可維護
  第二,這些字並非用完這次就無用,完全可以在後來的印刷中重複使用,此乃可複用
  第三,此詩若要加字,只需要另刻字加入即可,這是可擴展
  第四,字的排列其實可能是豎排可能是橫排,此時只需將活字移動就可做到滿足排列需求,此是靈活性好
  而在活字印刷術出現之前,上面的四種特性都無法滿足,要修改,必須重刻,要加字,必須重刻,要重新排列,必須重刻,印完這本書後,此版已無任何可再利用價值。
1.6 面向對象的好處
  學習了面向對象的分析設計編程思想,開始考慮通過封裝、繼續、多態把程序的耦合度降低,傳統印刷術的問題就在於所有的字都刻在同一版面上造成耦合度太高所致,開始用設計模式使得程序更加靈活,容易修改,並且易於複用。
1.7 複製 VS 複用
  有人說初級程序員的工作就是ctrl+c和ctrl+v,這其實是非常不好的編碼習慣,因爲當你的代碼中重複的代碼多到一定程序,維護的時候,可能就是一場災難。越大的系統,這種方式帶來的問題越嚴重,編程有一原則,就是用儘可能的辦法去避免重複。
1.8 業務的封裝
  準確地說,就是讓業務邏輯與界面邏輯分開,讓它們之間的耦合度下降,只有分離開,可以達到容易維護或擴展。
1.9 緊耦合 VS 鬆耦合
  應該把加減乘除等運算分離,修改其中一個不影響另外的幾個,增加運算算法也不影響其他代碼。
1.10 簡單工廠模式
  現在的問題其實就是如何去實例化對象的問題,簡單工廠模式,也就是說,到底要實例化誰,將來會不會增加實例化的對象,比如增加開根運算,這是很容易變化的地方,應該考慮用一個單獨的類來做這個創造實例的過程,這就是工廠類。
  你需要輸入運算符號,工廠就實例化出合適的對象,通過多態,返回父類的方式實現了計算器的結果。
1.11 UML類圖
  類圖分三層,第一層顯示類的名字,如果是抽象類,則就用斜體顯示,第二層是類的特性,通常就是字段和屬性,第三層是類的操作,通常是方法或行爲。注意前面的符號,‘+’表示public,'-'表示private,'#'表示protected
  接口圖,與類圖的區別主要是頂端有<<interface>>。第一行是接口名稱,第二行是接口方法。
  繼承關係空心三角形△+實線來表示
  實現接口空心三角線+虛線來表示。
  關聯關係實線箭頭→來表示。當一個類‘知道’另一個類時,可以用關聯。
  聚合關係空心的菱形◇+實線箭頭→來表示。例:大雁是羣居動物,每隻大雁都是屬於一個雁羣,一個雁羣可以有多隻大雁。所以它們之間就滿足聚合(Aggregation)關係。聚合表示一種弱的‘擁有’關係,體現的是A對象可以包含B對象,但B對象不是A對象的一部分。
  合成關係實心的菱形+實線箭頭→來表示。例:鳥和翅膀就是合成(組合)關係,因爲它們是部分和整體的關係,並且翅膀和鳥的生命週期是相同的。合成(組合)是一種強的‘擁有’關係,體現了嚴格的部分和整體的關係,部分和整體的生命同期一樣。
  依賴關係虛線箭頭來表示。例:動物幾大特徵,比如新陳代謝,能繁殖。而動物要有生命力,需要氧氣、水以及食物等。也就是說,動物依賴於氧氣和水。他們之間是依賴關係。
  編程是一門技術,更加是一門藝術,不能只滿足於寫完代碼運行結果正確就完事,時常考慮如何讓代碼更加簡練,更加容易維護,容易擴展和複用。只有這樣纔可以真正得到提高。寫出優雅的代碼真的是一種很爽的事情。


第2章 商場促銷——策略模式

2.1 商場收銀軟件
  做一個商場收銀軟件,營業員根據客戶所購買商品的單價和數量,向客戶收費。
2.2 增加打折
  需求1;要求商場對商品搞活動,所有的商品都打八折。還有可能因爲週年慶,打五折情況。還有可能滿300返100的促銷算法。還有可能滿200送50活動。難道再去增加子類?這當中哪些是相同,哪些是不同?
2.3 簡單工廠實現
  這裏打折基本都是一樣的,只要有個初始化參數就可以了。滿幾送幾的,需要兩個參數才行。
  面向對象的編程,並不是類越多越好,類的劃分是爲了封裝,但分類的基礎是抽象,具有相同屬性和功能的對象的抽象集合纔是類。打一折和打九折只是形式不同,抽象分析出來,所有的打折算法都是一樣的,所以打折算法應該是一個類。
  簡單工廠模式雖然也能解決這個問題,這這個模式只是解決對象的創建問題,而且由於工廠本身包括了所有的收費方式,商場是可能經常性地更改打折額度和返利額度,每次維護或擴展收費方式都是改動這個工廠,以致代碼需要重新編譯部署,這真的是很糟糕,所以用它不是最好的辦法。面對算法的時常變動,應該有更好的辦法。
2.4 策略模式
  它定義了算法家族,分別封裝起來,讓它們之間可以互相替換(這些算法是隨時都可能互相替換的,這就是變化點),此模式讓算法的變化,不會影響到使用算法的客戶。封裝變化行爲爲抽象或接口。
2.5 策略模式實現
  正常收費、打折收費、返利收費就是三個具體策略,也就是策略模式中說是具體算法。
2.6 策略與簡單工廠的結合
  簡單工廠模式需要讓客戶端認識兩個類,cashSuper和cashFactory,而策略模式與簡單工廠結合的用法,客戶端就只需要一個類cashContent就可以了,耦合更加降低。使得具體的收費算法徹底與客戶端分離。
2.7 策略模式解析
  反思一下策略模式,策略模式是一種定義一系列算法的方法,從概念上來看,所有這些算法完成的都是相同的工作,只是實現不同,它可以以相同的方式調用所有的算法,減少了各種算法類與使用算法類神之間的耦合。
  策略模式的Strategy類層次爲Context定義了一系列的可供重用的算法或行爲。繼承有助於析取出這些算法中的公共功能。對於打折、返利或者其他的算法,其實都是對實際商品收費的一種計算方式,通過繼承,可以得到它們的公共功能,這公共功能是什麼呢
  公共的功能就是獲得計算費用的結果GetResult,這使得算法間有了抽象的父類CashSuper。
  另外一個策略模式的優點是簡化了單元測試,因爲每個算法都有自己的類,可以通過自己的接口單獨測試。
  每個算法可保證它沒有錯誤,修改其中一個是時也不會影響其他的算法。
  策略模式就是用來封裝算法的,但在實踐中,我們發現可以用它來封裝幾乎任何類型的規則,只要在分析過程中聽到需要在不同時間應用不同的業務規則,就可以考慮使用策略模式處理這種變化的可能性
  在基本的策略模式中,選擇所用具體實現的職責由客戶端對象承擔,並轉給策略模式的Context對象。這本身並沒有解除客戶端需要選擇判斷的壓力,而策略模式與簡單工廠模式結合後,選擇具體實現的職責也可以由Context承擔,這就最大化地減輕了客戶端的職責。
  但是這裏還有一缺點,如果再次增加一種算法,比如滿200送50,就必須要更改CashContext中的switch代碼。解決辦法使用反射,(在抽象工作模式中有對反射的講解)。

 

第3章 拍攝UFO——單一職責原則

3.1 新手機
3.2 拍攝
3.3 沒用的東西
3.4 單一職責原則
  大多數時候,一件產品簡單一些,職責單一一些,或許是更好的選擇。這就和設計模式中的一大原則————單一職責的道理是一樣的。
  可以簡單地這麼理解,它的準確解釋是,就一個類而言,應該僅有一個引起它變化的原因。我們在編程的時候,很自然地就會給一個類加各種各樣的功能,比如我們寫一個窗體應用程序,一般都會生成一個Form這樣的類,於是我們把各種各樣的代碼,像某種商業運算的算法,像數據庫訪問的SQL語句什麼的都寫在這樣的類當中,這就意味着,無論任何需求來,你都要更改空上窗體類,這其實是很糟糕的,維護麻煩,複用不可能,也缺乏靈活性。
3.5 方塊遊戲的設計
  如果一個類承擔的職責過多,就等於把這些職責耦合在一起,一個職責的變化可能會消弱或者抑制這個類完成其他職責的能力。這種耦合會導致脆弱的設計,當變化發生時,設計會遭受到意想不到的破壞。事實上,完全可以找出哪些是界面,哪些是遊戲邏輯,然後進行分離。
  軟件設計真正要做的許多內容,就是發現職責並把那些職責相互分離。其實要去判斷是否應該分享出來,也不難,那就是如果你能夠想到多於一個的動機去改變一個類,那麼這個類就是有多於一個的職責,就應該考慮類的職責分離。
  界面的變化是和遊戲本身沒有關係的,界面是容易變化的,而遊戲邏輯是不太容易變化的,將它們分離開有利於界面的改動。
3.6 手機職責過多嗎?
  總的來說,手機的發展有它的特點,我們卻是要在類的職責分離上多思考,做到單一職責,這樣你的代碼纔是真正的易維護、易擴展、易複用、靈活多樣。
 

第4章 考研求職兩不誤————開放-封閉原則
4.1 考研失敗
  香港、澳門迴歸,一國兩制思想。爲了迴歸的大局,增加一種制度,一個國家,兩種制度,這在政治上,是偉大的發明。在軟件設計模式中,這種不能修改,但可以擴展的思想也是最重要的一種設計原則,它就是開放-封閉原則(the open-closeed principle,簡稱OCP)或叫開-閉原則。
4.2 開放-封閉原則
  開放-封閉原則,是說軟件實體(類、模塊、函數等等)應該可以擴展,但是不可修改。
  這個原則其實是有兩個特徵,一個是說“對於擴展是開放的”,另一個是說“對於更改是封閉的”。
  我們在做任何系統的時候,都不要指望系統一開始時需求確定,就再也不會變化,這是不現實也不科學的想法,而既然需要是一定會變化的,那麼如何在面對需求的變化時,設計的軟件可以相對容易修改,不至於說,新需求一來,就是把整個程序推倒重來。怎樣的設計才能面對需求的改變卻可以保持相對穩定,從而使得系統可以在第一個版本以後不斷推出新的版本呢?開放-封閉給我們答案。
  設計軟件要容易維護又不容易出問題的最好的辦法,就是多擴展,少修改。
4.3 何時應對變化
  開放-封閉原則的意思就是說,你設計的時候,時刻要考慮,儘量讓這個類是足夠好,寫好了就不要去修改了,如果新需求來,我們增加一些類就完事了,原來的代碼能不動則不動。
  絕對的對修改關閉是不可能的。無論模塊是多麼的“封閉”,都會存在一些無法對之封閉的變化。既然不可能完全封閉,設計人員必須對於他設計的模塊應該對哪種變化封閉做出選擇。他必須先猜測出最有可能發生的變化種類,然後構造抽象來隔離那些變化。
  我們是很難預先猜測,但我們卻可以在發生小變化時,就及早去想辦法應對發生更大的變化的可能。也就是說,等到變化發生時立即採取行動。正所謂,同一地方,摔第一跤不是你的錯,再次在些摔跤就是你的不對了。
  在我們最初編寫代碼時,假設變化不會發生。當變化發生時,我們就創建抽象來隔離以後發生的同類變化。比如,之前寫的加法程序,很快在一個clieng類中就完成,此時變化還沒有發生。然後添加一個減法功能,發會現,增加功能需要修改原來這個類,這就違背了今天講到的“開放-封閉原則”,於是就該考慮重構程序,增加一個抽象的運算爲在,通過一些面向對象的手段,如繼承,多態等來隔離具體加法、減法與client耦合,需求依然可以滿足,還能應對變化。這時又要再加乘除法功能,就不需要再去更改client以及加法減法的類了,而是增加乘法和除法子類就可。即面對需求,對程序的改動是通過增加新代碼進行的,而不是更改現有的代碼。這就是“開放-封閉原則”的精神所在。
  我們希望的是在開發工作展開不久就知道可能發生的變化。查明可能發生的變化所等待的時間越長,要創建正確的抽象就越困難。
  開放-封閉原則是面向對象設計的核心所在。遵循這個原則可以帶來面向對象技術所聲稱的巨大好處,也就是可維護、可擴展、可複用、靈活性好。開發人員應該僅對程序中呈現出頻繁變化的那些部分做出抽象,然而,對於應用程序中的每個部分都刻意地進行抽象同樣不是一個好主意。拒絕不成熟的抽象和抽象本身一樣重要。
4.4 兩手準備,並全力以赴

 

第5章 會修電腦不會修收音機?——依賴倒轉原則
5.1 MM請求修電腦
5.2 電話遙控修電腦
5.3 依賴倒轉原則
  可以把PC電腦理解成是大的軟件系統,任何部件如CPU、內存、硬盤、顯示等都可以理解爲程序中封裝的類或程序集,由於PC易插拔的方式,那麼不管哪一個出問題,都可以在不影響別的部件的前提下進行修改或替換。
  PC電腦裏叫易插拔,面向對象裏把這種關係叫強內聚、鬆耦合。
  電腦裏的CPU全世界也就是那麼幾家生產的,大家都在用,但卻不知道Intel、AMD等公司是如何做出這個精密的小東西的。這就說明CPU的強內聚的確是強。但它又獨自成爲了產品,在千千萬萬的電腦主板上插上就可以使用。
  因爲CPU的對外就是針腳式或觸點式等標準的接口。這就是接口的最大好處。CUP需要把接口定義好,內部再複雜我也不讓外界知道,而主板只需要預留與CPU針腳的插槽就可以了。
  這裏面有提到了面向對象的幾大設計原則,比如之前講過的單一職責原則,就剛纔修電腦的事,顯然內存壞了,不應該成爲更換CPU的理由,它們各自的職責是明確的。再比如開放—封閉原則,內存不夠只要插槽足夠就可以添加,硬盤不夠可以用移動硬盤等,PC的接口是有限的,所以擴展有限,軟件系統設計得好,卻可以無限地擴展。這兩個原則我們之前都已經提過了。這裏重點講講一個新的原則,叫依賴倒轉原則,也有翻譯成依賴倒置原則。
  依賴倒轉原則,原話解釋是抽象不應該依賴細節,細節應該依賴於抽象,這放繞口,說白了,就是要針對接口編程,不要對實現編程,無論主板、CPU、內存、硬盤都是在針對接口設計的,如果針對實現來設計,內存就要對應到具體的某個品牌的主板,那就會出現內存需要把主板也換了的尷尬。
  依賴倒轉原則:A.高層模塊不應該依賴低層模塊。兩個都應該依賴抽象。B.抽象不應該依賴細節。細節應該依賴抽象。
  1. 爲什麼要叫倒轉呢?
  這裏面是需要好好解釋一下,面向過程的開發是,爲了使得常用代碼可以複用,一般都會把這些常用代碼寫成許許多多函數的程序庫,這樣我們在做新項目時,去調用這些低層的函數就可以了。比如我們做的項目大多要訪問數據庫,所以我們就把訪問數據庫的代碼寫成了函數,每次做新項目時就去調用這些函數。這也就叫做高層模塊依賴低層模塊。
  問題出在這裏,我們要做新項目時,發現業務邏輯的高層模塊都是一樣的,但客戶卻希望使用不同的數據庫或存儲信息方式,這時就出現麻煩了。我們希望能再次利用這些高層模塊,但高層模塊都是與低層的訪問數據庫綁定在一起的,沒辦法複用這些高層模塊,這就非常糟糕了。就像剛纔說的,PC裏如果CPU、內存、硬盤都需要依賴具體的主板,主板一壞,所有的部件就都沒用了,這顯然不合理。反過來,如果內存壞了,也不應該造成其他部件不能用纔對。而如果不管高層模塊還是低層模塊,它們都依賴於抽象,具體一點就是接口或抽象類,只要接口是穩定的,那麼任何一個的更改都不用擔心其他受到影響。這就使得無論高層模塊還是低層模塊都可以很容易地被複用。這纔是最好的辦法。
  2. 爲什麼依賴了抽象的接口或抽象類,就不怕更改呢?
  瞭解里氏代換原則。
5.4 里氏代換原則
  里氏代換原則是Barbara Liskov女士在1988年發表的,它的白話翻譯就是一個軟件實體如果使用的是一個父類的話,那一定適用於其子類,而且它察覺不出父類對象和子類對象的區別。也就是說,在軟件裏面,把父類都替換成它的子類,程序的行爲沒有變化,簡單地說,子類型必須能夠替換掉它們的父類型。
  這好像是學繼承時就要理解的概念,子類繼承了父類,所以子類可以以父類的身份出現。
  鳥會飛,而企鵝不會飛。儘管在生物學分類上,企鵝是一種鳥,但在編程世界裏,企鵝不能以父類——鳥的身份出現,因爲前提說所有鳥都能飛,而企鵝飛不了,所以,企鵝不能繼承鳥類。
  也正因爲有了這個原則,使得繼承複用成爲了可能,只有當子類可以替換掉父類,軟件單位的功能不受到影響時,父類才能真正被複用而子類也能夠在父類的基礎上增加新的行爲。比方說,貓是繼承動物類的,以動物的身份擁有吃、喝、跑、叫等行爲,可當某一天,我們需要狗、牛、羊也擁有類似的行爲,由於它們都是繼承於動物,所以除了更改實例化的地方,程序其他處不需要改變。
  由於里氏代換原則,才使得開放-封閉成爲了可能。
  這樣說是可以的,正是由於子類型的可替換性才使得使用父類類型的模塊在無需修改的情況下就可以擴展。不然還談什麼擴展開放,修改關閉呢。再回過頭來看依賴倒轉原則,高層模塊不應該依賴低層模塊,兩個都應該依賴抽象,對這句話就會有更深入的理解了。
  依賴倒轉其實就是誰也不要依靠誰,除了約定的接口,大家可以靈活自如。

5.5 修收音機

  收音機裏都是些電阻、三極管,電路板等等東西,全都焊接在一起。
  收音機就是典型的耦合過度,只要收音機出故障,不管是沒有聲音、不能調頻,還是有雜音,反正都很難修理,不懂的人根本沒法修,因爲任何問題都可能涉及其他部件。各個部件相互依賴,難以維護。依賴倒轉其實可以說是面向對象設計的標誌,用哪種語言來編寫程序不重要,如果編寫時考慮的都是如何針對抽象編程而不是針對細節編程,即程序中所有的依賴關係都是終止於抽象類或者接口,那就是面向對象的設計,反之那就是過程化的設計了。

 

第6章 穿什麼有這麼重要?——裝飾模式
6.1 穿什麼有這麼重要?
6.2 小菜扮靚第一版
  寫一個可以給入搭配不同的服飾的系統,比如類似QQ、網絡遊戲或論壇都有的Avatar系統(通常用在電腦遊戲上,是通過細分角色模型或圖像並重新組合來增加角色外觀數量的系統)。
 意思是那種可以換各種各樣的衣服褲子的個人開解系統。穿服飾可以理解爲是一種行爲,例如:穿T恤、穿球鞋等直接創建person類,添加各種行爲。
6.3 小菜扮靚第二版
  抽象出一個服飾類,穿球鞋定義爲類,繼承服飾類,添加自己的行爲“穿球鞋”
  建造者模式要求建造的過程必須是穩定的,而現在我們這個例子,建造過程是不穩定的,比如完全可以內穿西裝,外套T恤,再加披風,打上領帶,皮鞋外再穿上破球鞋;換句話就是說,通過服飾組合出一個有個性的人完全可以有無數種方案,並非是固定的。
  我們需要把所需的功能按正確的順序串聯起來進行控制。
6.4 裝飾模式
  裝飾模式(Decorator):動態地給一個對象添加一些額外的職責,就增加功能來說,裝飾模式比生成子類更爲靈活。
  無論衣服、鞋子、領帶、披風其實都可以理解爲對人的裝飾。
  Component是定義一個對象接口,可以給這些對象動態地添加職責
  ConcreteCompoent是定義了一個具體的對象(人類person),也可以給這個對象添加一些職責。
  Decorator是一個裝飾抽象類,繼承了Component,從外類來擴展Component類的功能,但對於Component來說,是無需知道Decorator的存在的。
  至於ConcreteDecorator就是具體的裝飾對象(T恤、球鞋等),起到給Component添加職責的功能。
  原來裝飾模式是利用SetComponent來對對象進行包裝的。這樣每個裝飾對象的實現就和如何使用這個對象分離開了,每個裝飾對象只關心自己的功能,不需要關心如何被添加到對象鏈當中。用剛纔的例子來說就是,完全可以先穿外褲,再穿內褲,而不一定要先內後外。
  學習模式要善於變通,如果只有一個ConcreteComponent類而沒有抽象的Component類,那麼Decroator類可以是ContcreteComponent的一個子類。同樣道理,如果只有一個ConcreteDecorator類,那麼就沒有必要建立一個單獨的Decorator類,而可以把Decorator和ConcreteDecorator的責任合併成一個類。
6.5 小菜扮靚第三版
6.6 裝飾模式總結
  裝飾模式是爲已有功能動態地添加更多功能的一種方式。但到底什麼時候用它呢?
  起初的設計中,當系統需要新功能的時候,是向舊的類中添加新的代碼。這些新加的代碼通常裝飾了原有類的核心職責或主要行爲,比如用西裝或嘻哈服來裝飾小菜,但這種做法的問題在於,它們在主類中加入了新的字段,新的方法和新的邏輯,從而增加了主類的複雜度,就像你起初的那個“人”類,而這些新加入的東西僅僅是爲了滿足一些只在某種特定情況下才會執行的特殊行爲的需要。而裝飾模式卻提供了一個非常好的解決方案,它把每個要裝飾的功能放在單獨的類中,並讓這個類包裝它所要的裝飾的對象,因此,當需要執行特殊行爲時,客戶代碼就可以在運行時根據需要有選擇地、按順序地使用裝飾功能包裝對象了。所以就出了上面的例子的情況,可以通過裝飾,讓你全副武裝到牙齒,也可以只掛一絲到內褲。
  那麼裝飾模式的優點總結下就是,把類中的裝飾功能從類中搬移去除,這樣可以簡化原有的類。這樣更大的好處就是有效地把類的核心職責和裝飾功能區分開了。而且可以去除相關類中重複的裝飾邏輯。

 

第7章 爲別人做嫁衣——代理模式
7.1 爲別人做嫁衣
7.2 沒有代理的代碼
  追求者類---被追求者類
7.3 只有代理的代碼
  代理類---被追求者類:把追求者給忽略了,事實上應該是追求者通過代理送給被追求者禮物,這纔是合理的。
  仔細觀察一下,追求者和被追求者是有相似的地方。他們都有送禮物的方法,只不過被追求者送的禮物是追求者買的,實質是追求者送的。既然兩者都有相同的方法,那就意味他們需要實現同樣的接口
7.4 符合實際的代碼
  定義送禮物接口(送鮮花、送巧克力),追求者類實現送禮物接口,代理類也去實現送禮物接口(在實現方法中去調用“追求者”類的相關方法).
7.5 代理模式
  代理模式(proxy):爲其他對象提供一種代理以控制對這個對象的訪問。
7.6  代理模式應用
  代理模式都用在一些什麼場合呢?
  一般來說分爲幾種:
  第一,遠程代理,也就是爲一個對象在不同的地址空間提供局部的代表。這樣可以隱藏一個對象存在於不同地址空間的事實。
  例如WebService在.NET中的應用,當在應用程序的項目中加入一個web引用,引用一個webService,此時會在項目中生成一個WebReference的文件夾和一些文件,其實它們就是代理,這就使用客戶端程序調用代理就可以解決遠程訪問的問題。
  第二,虛擬代表,是根據需要創建開銷很大的對象。通過它來存放實例化需要很長時間的真實對象。這樣可以達到性能的最優化。
  例如打開一個很大的HTML網頁時,裏面可能有很多的文字和圖片,但你還是可以很快打開它,此時你所看到的是所有的文字,但圖片卻是一張一張地下載後才能看到。那些未打開的圖片框,就是通過虛擬代理來替代了真實的圖片,此時代理存儲了真實圖片的路徑和尺寸。
  第三,安全代理,用來控制真實對象訪問時的權限。一般用於對象應該有不同的訪問權限的時候。
  第四,智能指引,是指當調用真實的對象時,代理處理另外一些事。
  例如計算真實對象的引用次數,這樣當該對象沒有引用時,可以自動釋放它;或當第一次引用一個持久對象時,將它裝入內存;或在訪問一個實際對象前,檢查是否已經鎖定它,以確保其他對象不能改變它。它們都是通過代理在訪問一個對象時附加一些內務處理。
  代理模式其實就是在訪問對象時引入一定程序的間接性,因爲這種間接性,可以附加多種用途。
7.7 秀才讓小六代其求婚

 

第8章 雷鋒依然在人間——工廠方法模式
8.1 再現活雷鋒
8.2 簡單工廠模式實現
  一直在研究工廠方法模式,但還是不太理解它和簡單工廠的區別,感覺還不如簡單工廠方便,爲什麼要用這個模式,到底這個模式的精髓在哪是?
8.3 工廠方法模式實現
  先構建一個工廠接口,然後加減乘除各建一個具體工廠(加法類工廠、減法類工廠)去實現這個接口。
8.4 簡單工廠 VS 工廠方法
  以前說過,如果現在需要增加其他運算,比如求M數的N次方,或者求M數的N次方根,這些功能的增加,在簡單工廠裏,是先去加“求M數的N次方”功能類,然後去更改工廠方法,當中加“Case”語句來做判斷現在用了工廠方法,加功能類沒問題,再加相關的工廠類,這也沒問題,但是我再去更改客戶端,定不等於不但沒有減化難度,反而增加了很多類和方法,把複雜性增加了嗎?爲什麼要這樣?
  這其實就是工廠方法模式和簡單工廠的區別所在。簡單工廠模式的最大優點在於工廠類中包含了必要的邏輯判斷,根據客戶端的選擇條件動態實例化相關的類,對於客戶端來說,去除了與具體產品的依賴。
  就像計算器,讓客戶端不用管該用哪個類的實例,只需要把“+”給工廠,工廠自動就給出了相應的實例,客戶端只要去做運算就可以了,不同的實例會實現不同的運算。但問題也就在這裏,如你所說,如果要加一個“求M數的N次方”的功能,我們是一定需要給運算工廠類的方法里加“Case”的分支條件的,修改原有的類?這可不是好辦法,這就等於說,我們不但對擴展開放了,對修改也開放了,這樣就違背了開放-封閉的原則。
  工廠方法模式(Factory Method):定義一個用於創建對象的接口,讓子類決定實例化哪一個類。工廠方法使用一個類的實例化延遲到其子類。
  我們講過,既然這個工廠類與分支耦合,那麼我就對它下手,根據依賴倒轉原則,我們把工廠類抽象出一個接口,這個接口只有一個方法,就是創建抽象產品的工廠方法。然後,所有的要生產具體類的工廠,就去實現這個接口,這樣,一個簡單工廠模式的工廠類,變成了一個工廠抽象接口和多個具體生產對象的工廠,於是我們要增加“求M數的N次方”的功能時,就不需要更改原有的工廠類了,只需要增加此功能的運算類和相應的工廠類就可以了。
  這樣整個工廠和產品體系其實都沒有修改的變化,而只是擴展的變化,這就完全符合了開放-封閉原則的精神。
  其實仔細觀察就會發現,工廠方法模式實現時,客戶端需要決定實例化哪一個工廠來實現運算類,選擇判斷的問題還是存在的,也就是說,工廠方法把簡單工廠的內部邏輯判斷移到了客戶端代碼來進行。你想要加功能,本來是改工廠類的,而現在是修改客戶端。
8.5 雷鋒工廠
  雷鋒是衆人皆知的做好人好事的模範,而班級裏的某某現學以學習雷鋒的名義做好事,而現在去代替他做好事,這其實就是典型的工廠方法模式應用了。
  雷鋒類,擁有掃地、洗衣、買米等方法。
  學雷鋒的大學生類,繼承 雷鋒
  然後客戶端實現,假設有三個人要去代替他做這些事,那就應該實例化三個“學雷鋒的大學生”對象了。
  學生都是要畢業的,而幫助老人卻是長期工作,所以“社區志願者”更合適,所以還需要增加一個繼承“雷鋒”類的“社區志願者”類。
  此時會發現,需要在任何實例化的時候寫出這個工廠的代碼。這裏有重複,也就有了壞味道。再用工廠方法模式寫一遍。
  工廠方法克服了簡單工廠違背開放-封閉原則的缺點,又保持了封裝對象創建過程的優點。它們都是集中封裝了對象的創建,使得要更換對象時,不需要做大的改動就可實現,降低了客戶程序與產品對象的耦合。工廠方法模式是簡單工廠模式的進一步抽象和推廣。由於使用了多態性,工廠方法模式保持了簡單工廠模式的優點,而且克服了它的缺點。但缺點是由於每加一個產品,就需要加一個產品工廠的類,增加了額外的開發量。所以工廠方法也並不是最佳的做法。之前提到過,利用“反射”可以避免分支判斷的問題。 

 

第9章 簡歷複印——原型模式
9.1 誇張的簡歷
9.2 簡歷代碼初步實現
  簡歷類、客戶端調用
  三分簡歷需要三次實例化。如果要二十份,就需要二十次實例化。如果寫錯一個字,那就要改二十次。
9.3 原型模式
  原型模式:用原型實例指定創建對象的各種類,並且通過拷貝這些原型創建新的對象。
  原型模式其實就是從一個對象再創建另外一個可定製的對象,而且不需要知道任何創建的細節。原型模式代碼如下:
  抽象原型類(添加抽象clone方法,返回抽象原型類)、具體原型類實現抽象原型類、客戶端代碼
  java類中提供了Cloneable接口,其中就是唯一的一個方法Clone(),這樣就只需要實現這個接口就可以完成原型模式了。
9.4 簡歷的原型實現
  簡歷類實現Cloneable接口、客戶端調用(只需要調用clone方法就可以實現新簡歷的生成,並且可以再修改新簡歷的細節)
  使用clone可對性能的提高,如果每new一次,都需要執行一次構造函數,如果構造函數的執行時間很長,那麼多次的執行這個初始化操作就實在是太低效了。一般在初始化的信息不發生變化的情況下,克隆是最好的辦法。這即隱藏了對象創建的細節,又對性能是大大的提高。
  它等於是不用重新初始化對象,而是動態地獲得對象運行時的狀態。
9.5 淺複製與深複製
  如果字段是值類型的,則對該字段執行逐位複製,如果字段是引用類型,則複製引用但不復制引用的對象,因此,原始對象及其複本引用同一對象。如果“簡歷”類當中有對象引用,那麼引用的對象數據是不會被克隆過來的。
  淺複製:被複制的對象所有變量都含有與原來的對象相同的值,而所有的對其他對象的引用都仍然指向原來的對象。但我們可能更需要這樣的一種需求,把要複製的對象所引用的對象都複製一遍。
  深複製:把引用對象的變量指向複製過的新對象,而不是原有的被引用的對象。
  如果“簡歷”對象引用了“工作經歷”,“工作經歷”再引用“公司”,“公司”再引用“職位”......,這樣一個引用一個很多層,如何辦?
  這的確是個很難回答的問題,深複製要深入到多少層,需要事先就考慮好,而且要當心出現循環引用的問題,需要小心處理,這裏比較複雜,可以慢慢研究。就現在這個例子,問題應該不大,深入到第一層就可以了。
9.6 簡歷的深複製實現
  代碼組織:讓工作經歷也實現Cloneable接口,實現克隆方法。在簡歷類中,調用工作經歷的私有構造函數(clone),以便克隆工作經歷數據。
  由於在一些特定場合,會經常涉及到深複製或淺複製,比如說,數據集對象DataSet,它就有Clone()方法和Copy()方法,Clone()方法用來複制DataSet的結構,但不復制DataSet的數據,實現了原有模式的淺複製。Copy()方法不但複製結構,也複製數據,其實就是實現了原型模式的深複製。
9.7 複製簡歷 VS 手寫求職信

 

第10章 考題抄錯會做也白搭——模板方法模式
10.1 選擇題不會做,蒙唄!
  考試試卷最大的好處就是,大家都是一樣的題目,特別是標準化的考試,比如全是選擇或判斷的題目,那就最大化地限制了答題者的發揮,大家都是ABCD或打勾打叉,非對即錯的結果,這其實就是一個典型的設計模式。
10.2 重複=易錯+難改
  抄題目的程序如下:
  學生甲抄的試卷
  學生乙抄的試卷
  客戶端代碼:學生甲 = new 學生甲;學生乙 = new 學生乙
10.3 提煉代碼
  學生甲和學生乙兩個抄試卷類非常類似,除了答案不同,沒什麼不一樣,這樣寫又容易錯,又難以維護。
  老師出一份試卷,打印多份,讓學生填寫答案就可以了。在這裏應該就是把試題和答案分享,抽象出一個父類,讓兩個子類繼承於它,公共的試題代碼寫到父類當中,就可以了。
  試卷父類代碼:
  父類金庸小說考題試卷,行爲(考題),學生子類代碼=學生甲抄的試卷繼承父類,學生乙抄的試卷繼承父類 行爲(給出各個考題的答案)
  以上代碼中還有相同的東西,比如都有base.考題1(),還有console.writeline("答案:"),除了選項的abcd,其他都是重複的。
  我們既然用了繼承,並且肯定這個繼承有意義,就應該要成爲子類的模板,所有重複的代碼都應該要上升到父類去,而不是讓每個子類都去重複。
  模板方法登場,當我們要完成在某一細節層次一致的一個過程或一系列督,但其個別步驟在更詳細的層次上的實現可能不同是時,我們通常考慮用模板方法模式來處理。
  考題都一樣,只有答案不一樣,這時需要增加一個答案的虛方法。虛方法的目的就是給繼承的子類重寫,因爲這裏每個人的答案都是不同的。
  子類只需要重寫虛方法,把答案填上,其他什麼都不用管。因爲父類建立了所有重複的模板。
  客戶端代碼需要改動一個小地方,即本來是子類變量的聲明,改成了父類,這樣就可以利用多態性實現代碼的複用了。而這其實就是典型的模板方法模式。
10.4 模板方法模式
  模板方法模式(TemplateMethod):定義一個操作中的算法的骨架,而將一些步驟延遲到子類中。模板方法使得子類可以不改變一個算法的結構即可重定義該算法的某些特定步驟。
  AbstractClass是抽象類,其實也就是一抽象模板,定義並實現了一個模板方法。這個模板方法一般是一個具體方法,它給出了一個頂級邏輯的骨架,而邏輯的組成步驟在相應的抽象操作中,推遲到子類實現。頂級邏輯也有可能調用一些具體方法。
  ConcreteClass是子類,實現父類所定義的一個或多個抽象方法。每一個AbstractClass都可能 有任意多個ConcreteClass與之對應,而每一個ConcreteClass都可以給出這些抽象方法(也就是頂級邏輯的組成步驟)的不同實現,從而使得頂級邏輯的實現各不相同。
10.5 模板方法模式特點
  模板方法模式是通過把不變行爲搬移到超類,去除子類中的重複代碼來體現它的優勢。
  模板方法模式就是提供了一個很好的代碼複用平臺。有時候,我們會遇到由一系列步驟構成的過程需要執行。這個過程從高層次上看是相同的,但有些步驟的實現可以能不同。這時候,我們通常就應該要考慮用模板方法模式了。
  碰到這個情況,當不變的和可變的行爲在方法的子類實現中混合在一起的時候,不變的行爲就會在子類中重複出現。我們通過模板方法模式把這些行爲搬移到單一的地方,這樣就幫助子類擺脫重複的不變行爲的糾纏。
  模板方法模式是很常用的模式,對繼承和多態玩得好的人幾乎都會在繼承體系中多多少少用到它。比如在.NET或Java類庫的設計中,通常都會利用模板方法模式提取類庫中的公共行爲到抽象類中

注:每當有兩個行爲類似但又不完全相同的代碼段時,我總是會想到模板方法。提取公共流程和可複用的方法到父類,保留不同的地方作爲abstract方法,由不同的子類去實現。
10.6 主觀題,看你怎麼蒙

 

第11章 無熟人難辦事?—— 迪米特原則
11.1 第一天上班
  到IT部門領取電腦,認識一個叫小張的同事,結果正打算裝電腦的時候,來了個電話,叫他馬上去一個客戶那裏處理PC故障。結果一等就是一上午。去問人事部可不可以讓其他人幫忙,人事部讓自己去找一下IT部的小李,小李接過領取電腦單子,看上面寫着小張的名字,於是說這個事是小張負責,他不管。還得等小張回來再說。
11.2 無熟人難辦事
  以上問題倒是我們設計模式的一個原則。如果IT部有一個主管,負責分配任務,不管任何需要IT部配合的工作都讓主管安排,不就沒問題了嗎?
  沒有管理,單靠人際關係協調是很難辦成事的。如果公司IT部就一個小張,那什麼問題也沒有。再來個小李,那工作叫誰去做呢?外人又不知道他們兩人誰忙誰閒的。要是三個人在IT部還沒有管理人員,則更加麻煩了。正所謂一個和尚挑水吃,兩個和尚擡水吃,三個和尚沒水吃。
  其實不管認不認識IT部的人,只要電話或親自找到IT部,他們都應該想辦法幫忙解決問題。不管公司任何人,無論認不認識,找IT部就可以了。
  在這裏,IT部代表是抽象類或接口,小張小李代表是具體類,之前分析會修電腦不會修改收音機裏講的依賴倒轉原則,即面向接口編程,不要面向實現編程就是這個意思。
11.3 迪米特原則
  迪米特原則:如果兩個類不必彼此直接通信,那麼這兩個類就不應當發生直接的相互作用。如果其中一個類需要調用另一個類的某一個方法的話,可以通過第三者轉發這個調用。
  迪米特原則首先強調的前提是在類的結構設計上,每一個類都應當儘量降低成員的訪問權限,也就是說,一個類包裝好自己的private狀態,不需要讓別的類知道的字段或行爲就不要公開。
  當然,面向對象的設計原則和麪向對象的三大特性本就不是矛盾的。迪米特法則其根本思想,是強調了類之間的鬆耦合。就拿今天碰到的這件事做例子,第一天去公司,怎麼會認識IT部的人呢,如果公司有很好的管理,那麼應該是人事部打個電話到IT部,告訴主管安排人裝電腦,就算開始是小張負責,他臨時有事,主管也可以再安排小李來處理。同樣道理,我們在程序設計時,類之間的耦合越弱,越有利於複用,一個處在弱耦合的類被修改,不會對有關係的類造成波及。也就是說,信息的隱藏促進了軟件的複用。

 

第12章  牛市股票還會虧錢?—— 外觀模式
12.1 牛市股票還會虧錢?
  自己的錢買了股票,天天都在變化,誰能不關心,特別是剛開始,都希望能漲漲漲。儘管不現實,不過賺錢的人還是有的是。不過一打開股票軟件,一千多隻股票,紅紅綠綠,又是指數大盤,又是個股K線指標,一下說基本面如何如何重要,一下又說什麼有題材纔可以賺大錢,頭暈眼花,迷茫困惑阿。
  基金就是你的幫手。它將投資者分散的資金集中起來,交由專業的經理人進行管理,投資於股票、債券、外匯等領域,而基金投資的收益歸持有投資者所有,管理機構收取一定比例的託管管理費用。想想看,這樣做有什麼好處?
  由於衆多投資者對衆多股票的聯繫太多,反而不利於操作,這在軟件中就稱爲耦合性過高。而有了基金以後,變成衆多用戶只和基金打交道,關心基金的上漲和下跌就可以了,而實際上的操作卻是基金經理人在與上千支股票和其他投資產品打交道。
  這裏其實提到了一個在面向對象開發當中用得非常多的一個設計模式——外觀模式,又收門面模式。先試着把股民炒股票的代碼寫寫看。
12.2 股民炒股代碼
  客戶類、股票1類(行爲買、賣)、股票2類,債券類等,用戶需要了解股票、債券情況,需要參與這些項目的具體買和賣。耦合性很高。
12.3 投資基金代碼 增加基金類
  客戶類、基金類(它需要了解所有的股票或其他投資方式的方法和屬性,進行組合,以備外界調用。行業-基金購買和基金贖回)、股票1類(行爲買、賣)、股票2類,債券類等
  客戶端調用,此時用戶不需要了解股票,甚至可以對股票一無所知,買了基金就回家睡覺,一段時間後再贖回就可以大把數錢。參與股票的具體買賣都由基金公司完成。客戶端代碼簡捷明瞭。
  這樣的寫法其實就是外觀模式的基本代碼結構。
12.4 外觀模式(Facade)
  外觀模式:爲子系統中的一組接口提供一個一致的界面,此模式定義了一個高層接口,這個接口使得這一子系統更加容易使用。
  對於面向對象有一定基礎的朋友,即使沒有聽說過外觀模式,也完全有可能在很多時候使用它,因爲它完美地體現了依賴倒轉原則和迪米特原則的思想,所以是非常常用的模式之一
12.5 何時使用外觀模式
  這是分三個階段來說:
  首先,在設計初期階段,應該要有意識的將不同的兩個層分離,比如經典的三層架構,就需要考慮在數據訪問層和業務邏輯層、業務邏輯層和表示層的層與層之間建立外觀Facade,這樣可以爲複雜的子系統提供一個簡單的接口,使得耦合大大降低。
  其次,在開發階段,子系統往往因爲不斷的重構演化而變得越來越複雜,大多數的模式使用時都會產生很多很小的類,這本是好事,但也給外部調用它們的用戶程序帶來了使用上的困難,增加外觀Facade可以提供一個簡單的接口,減少它們之間的依賴。
  第三,在維護一個遺留的大型系統時,可能這個系統已經非常難以維護和擴展了,但因爲它包含非常重要的功能,新的需求開發必須要依賴於它。此時用外觀模式Facade也是非常合適的。
  總結,你可以爲新系統開發一個外觀Facade類,來提供設計粗糙或高度複雜的遺留代碼的比較清晰簡單的接口,讓新系統與Facade對象交互,Facade與遺留代碼交互所有複雜的工作。
  對於複雜難以維護的老系統,直接去改或去擴展可能產生很多問題,分兩個小組,一個開發Facade與老系統的交互,另一個只要瞭解Facade的接口,直接開發新系統調用這些接口即可,確實可以減少很多不必要的麻煩。

 

第13章 好菜每回味不同 —— 建造者模式
13.1 炒麪沒放鹽
  麥當勞、肯德基的漢堡,不管在哪家店裏吃,什麼時間去吃,至少在中國,味道基本都是一樣的。而我們國家,比如那道魚香肉絲,幾乎是所有大小中餐飯店都有的一道菜,但卻可以吃出上萬種口味來,這是爲什麼?因爲廚師不一樣,每個人的做法不同的。
  麥當勞、肯德基他們比較規範,味道是由他們的工程流程決定的,原料放多少,加熱幾分鐘,都有嚴格規定。
  而我們的炒麪有沒有放鹽、好吃不好吃都是由燒菜的人決定的,心情好就是一盤好面,心情不好,就是一盤垃圾。
  今天就吃了兩盤垃圾,其實這裏面最關鍵的就在於我們是吃得爽還是吃得難受都要依賴於廚師。再想想我們設計模式的原則?
  依賴倒轉原則?抽象不應該依賴細節,細節應該依賴於抽象,由於我們要吃的菜都依賴於廚師這這樣的細節,所以我們就很被動。
  老麥老肯的工作流程可以是一種抽象的流程,具體放什麼醭、烤多長時間等細節依賴於這個抽象。
13.2 建造小人一
  要求用程序畫一個小人,這在遊戲裏非常常見,現在簡單一點,要求是小人要有頭、身體、兩手、兩腳就可以。
  實現:建立一支黃色的畫筆,在畫布上畫出頭、身體、左手、右手、左腳、右腳。
  現在要求再畫一個身體比較胖的小人呢
  實現:改變畫筆其中一個身體即可。但是卻少畫了一條腿。
  這就和吃炒麪一樣,老闆忘記了放鹽,讓本是非常美味的夜宵變得無趣。在遊戲程序裏,畫人的時候,頭身手腳必不可少,不管什麼人物,開發時是不能少的。
  現在的代碼全寫在單獨的窗體裏,要是需要在別的地方用這些畫小人的程序怎麼辦?
13.3 建造小人二
  分離,建兩個類,一個是瘦人類,一個是胖人類,不管誰都可以調用它。
  瘦人類:初始化時確定畫板和顏色,build()建造小人
  胖人類:初始化時確定畫板和顏色,build()建造小人
  客戶端實現:創建瘦人類、胖人類
  這樣寫還是沒有解決炒麪忘記放鹽的問題。比如現在需要加一個高個的小人,會不會因爲編程不注意,又讓他缺胳膊少腿呢?
  最好的辦法是規定,凡是建造小人,都必須要有頭和身體,以及兩手兩腳。
13.4 建造者模式
  建造者模式(Builder):將一個複雜對象的構建與它的表示分離,使得同樣的構建過程可以創建不同的表示。
  仔細分析會發現,這裏建造小人的”過程“是穩定的,都需要頭身手腳,而具體建造的“細節”是不同的,有胖有瘦有高有矮。但對於用戶來講,我纔不管這些,我只想告訴你,我需要一個胖小人來遊戲,於是你就建造一個給我就行了。如果你需要將一個複雜對象的構建與它的表示分離,使得同樣的構建過程可以創建不同的表示的意圖時,我們需要應用於一個設計模式,建造者(Builder)模式,又叫生成器模式。
  建造者模式可以將一個產品的內部表象與產品的生成過程分割開來,從而可以使一個建造過程生成具有不同的內部表象的產品對象。如果我們用了建造都模式,那麼用戶就只需指定需要建造的類型就可以得到它們,而具體建造的過程和細節就不需知道了。
  那怎麼用建造者模式呢?
  首先我們要畫小人,都需要畫什麼?頭、身體、左手、右手、左腳、右腳。
  對的,所以我們先定義一個抽象的建造人的類,來把這個過程給穩定住,不讓任何人遺忘當中的任何一步。
  然後,我們需要建造一個瘦的小人,則讓這個瘦子類去繼承這個抽象類,那就必須去重寫這些抽象方法。否則編譯器也不讓你通過。
  這樣子,在客戶端要調用時,還是需要知道頭身手腳這些方法,沒有解決問題。
  我們還缺建造都模式中一個很重要的類,指揮者(Director),用它來控制建造過程,也用它來隔離用戶與建造過程的關聯。
  用戶告訴指揮者PersonDirector,我需要什麼樣的小人,根據用戶的選擇建造小人createPerson。
  PersonDirector類的目的就是根據用戶的選擇來一步一步建造小人,而建造的過程在指揮者這時完成了,用戶就不需要知道了,而且,由於這個過程每一步都是一定要做的,那就不會讓少畫了一隻手,少畫一條腿的問題出現了。
13.5 建造者模式解析
  建造者模式(Builder)結構圖
  Builder是爲創建一個Product對象的各個部件指定的抽象接口。
  Director指揮者,是構建一個使用Builder接口的對象。
  ConcreteBuilder,具體建造者,實現Builder接口,構造和裝配各個部件。
  Product,具體產品。
  Builder是什麼?是一個建造小人各個部分的抽象類。概括地說,是爲創建一個Product對象的各個部件指定的抽象接口。
  ConcreteBuilder是什麼?具體的小人,具體實現如何畫出小人的頭身手腳各個部分。它是具體建造者,實現Builder接口,構造和裝配各個部件。
  Product是什麼?產品的角色。
  Director是什麼?指揮者,用來根據用戶的需求構建小人對象。它是構建一個使用Builder接口的對象。
  什麼時候需要使用建造者模式呢?
  它主要是用於創建一些複雜的對象,這些對象內部構建間的建造順序通常是穩定的,但對象內部的構建通常面臨着複雜的變化。
  建造者的好處就是使得建造代碼與表示代碼分離,由於建造者隱藏了該產品是如何組裝的,所以若需要改變一個產品的內部表示,只需要再定義一個具體的建造者就行了。
13.6 建造者模式基本代碼
  Product類——產品類,由多個部件組成,行爲add(string part)-添加產品部件
  Builder類——抽象建造者類,確定產品由兩個部件BuilderPartA和BuilderPartB組成,並聲明一個得到產品建造後結果的方法GetResult.
  ConcreteBuilder1類——具體建造者類,建造具體的兩個部件是部件A和部件B。
  Director類——指揮類,用來指揮建造過程.
  客戶端代碼,客戶不需知道具體的建造過程。指揮者用ConcreteBuilder1的方法來建造產品。
  所以說,建造者模式是在當創建複雜對象的算法應該獨立於該對象的組成部分以及它們的裝配方式時適用的模式。
  如果今天做炒麪的老闆知道建造者模式,他就明白,鹽是一定要放的,不然,編譯就通不過。

 

第14章 老闆回來,我不知道———觀察者模式
14.1 老闆回來?我不知道!
  通過前臺祕書,如果老闆出門後回來,就打個電話進來,大家(同事)馬上各就各位,這樣就不會被老闆發現問題了。
14.2 雙向耦合的代碼
  前面說的情形,其實是一個典型的觀察者模式。
  前臺祕書類:class Secretary(通知者類)
  擁有屬性:同事列表 list<StockObserver>
  擁有方法: 增加(就是有幾個同事請前臺幫忙,於是就給集合增加幾個對象) void Attach(StockObserver observer)
  擁有方法:通知(待老闆來時,就給所有的登記的同事發通知,老闆來了) void Notify()
  擁有方法:前臺狀態(前臺通過電話,所說的話或所做的事) string SecretaryAction
  看股票同事類:class StockObserver(觀察者類)
  擁有方法:更新(得到前臺的通知,趕快採取行動) void update()
  客戶端:
  初始化創建前臺祕書類;初始化創建看股票的同事(兩個);前臺記下兩位同事.Attach(observer);發現老闆回來.SecretaryAction = "老闆來了";通知兩個同事.Notify();
  以上代碼中,兩個類之間互相耦合,前臺類要增加觀察者,觀察者類需要前臺的狀態。如果觀察者當中還有人是想看NBA的網上直播,前臺類就需要改動。
  根據設計原則,首先開放—封閉原則,修改原有代碼就說明設計不夠好。其次是依賴倒轉原則,應該讓程序都依賴抽象,而不是相互依賴。
14.3 解耦實踐一
  增加抽象的觀察者 abstract class Observer抽象方法更新 void update();
  增加兩個具體觀察者繼承抽象觀察者 class StockObserver(看股票的同事):Observer,對update的方法做重寫操作。
  增加兩個具體觀察者繼承抽象觀察者 class NBAObserver(看NBA的同事):Observer,對update的方法做重寫操作。
  前臺祕書類:class Secretary,把所有的與具體觀察者耦合的地方都改成抽象觀察者。針對抽象編程,減少與具體類的耦合
  前臺祕書也是一個具體的類,也應該抽象出來。通知者可能是前臺祕書也可能是老闆。
14.4 解耦實踐二
  增加抽象通知者接口 interface Subject
  增加(就是有幾個同事請前臺幫忙,於是就給集合增加幾個對象) void Attach(Observer observer)
  減少(如果一個同事與前臺(通知者有矛盾),於是不通知這位同事)void Deatch(Observer observer)
  發出通知void Notify();
  狀態(前臺通過電話,所說的話或所做的事)String subjectState{get;set;}
  具體的通知類可能是前臺,也可能是老闆,它們也許有各自的一些方法,但對於通知者來說,它們是一樣的,所以它們都去實現這個接口。
  老闆類class Boss:Subject 同是列表、增加、減少、通知、老闆狀態
  對於具體的觀察者,需要改動的地方就是把與"前臺"耦合的地方都改成針對抽象通知者。
14.5 觀察者模式
  觀察者模式又叫做發佈-訂閱(Publish/Subscribe)模式。
  觀察者模式:定義了一種一對多的依賴關係,讓多個觀察者對象同時監聽某一個主題對象。這個主題對象在狀態發生變化時,會通知所有觀察者對象,使它們能夠自動更新自己。
  觀察者(Observer)結構圖
  Subject(抽象通知者)類,它把所有對觀察者對象的引用保存在一個聚集裏,每個主題都可以有任何數量的觀察者,抽象主題提供一個接口,可以增加和刪除觀察者對象。
  Observer類,抽象對象者,爲所有的具體觀察者定義一個接口,在得到主題的通知時更新自己。
  ConcreteSubject類,具體主題通知者,將有關狀態存入具體觀察者對象;在具體主題的內部狀態改變時,給所有登記過的觀察者發出通知。
  ConcreteObserver類,具體觀察者,實現抽象觀察者角色所要求的更新接口,以便使本身的狀態與主題的狀態想協調。
14.6 觀察者模式特點
  用觀察者的動機是什麼?
  將一個系統分割成一系列相互協作的類有一個很不好的副作用,那就是需要維護相關對象間的一致性。我們不希望爲了維持一致性而使各類緊密耦合,這樣會難維護、擴展和重用都帶來不便。而觀察者模式的關鍵對象是主題(通知者)Subject和觀察者Observer,一個Subject可以有任意數目的依賴它的Observer,一旦Subject的狀態發生了改變,所有的Observer都可以得到通知。Subject發出通知時並不需要知道誰是它的觀察者,也就是說,具體觀察者是誰,它根本不需要知道。而任何一個具體觀察都不知道也不需要知道其他觀察者的存在。
  什麼時候考慮使用觀察者模式呢?
  當一個對象的改變需要同時改變其他對象的時候。而且它不知道具體有多少對象有待改變時,應該考慮使用觀察者模式。當一個抽象模型有兩個方面,其中一方面依賴於另一方面,這時用觀察者模式可以將這兩者封裝在獨立的對象中使它們各自獨立地改變和複用。
  總的來講,觀察者模式所做的工作其實就是在解除耦合。讓耦合的雙方都依賴於抽象,而不是依賴於具體。從而使用得各自的變化都不會影響另一邊變化(這也是依賴倒轉的最佳體現)。
  在抽象觀察者者時,代碼裏用的是抽象類,爲什麼不用接口?
  因爲兩個具體觀察者,看股票觀察者和看NBA觀察者類是相似的,所以用了抽象類,這樣可以共用一些代碼。
  那麼抽象觀察者可不可以用接口來定義?interface observer{void update();}
  現實編程中,具體的觀察者完全有可能是風馬牛不相及的類,但它們都需要根據通知者的通知來做出Update()的操作,所以讓它們都實現一個接口就可以實現這個想法了。
14.7 觀察者模式的不足
  回到剛纔那個"老闆、前臺與同事的例子",看看它還有什麼不足之處?
  儘管已經用了依賴倒轉原則,但是"抽象通知者"還是依賴"抽象觀察者",也就是說,萬一沒有了抽象觀察者這樣的接口,我這通知的功能就完不成了。另外就是每個具體觀察者,它不一定是"更新"的方法要調用,就像剛纔說的,我希望的是"工具箱"是隱藏,"自動窗口"是打開,這根本就不是同名的方法。這就是不足的地方。
  如果通知者和觀察者之間根本就互相不知道,由客戶端來決定通知誰,那就好了。
14.8 事件委託實現
  "看股票觀察者"類和"看NBA觀察者"類,去掉了父類"抽象觀察類",所以補上一些代碼,並將“更新”方法名改爲各自適合的方法名。
  看股票的同事:StockObserver-void CloseStockMarket()方法"更新"名改爲"關閉股票程序"。
  看NBA的同事:NBAObserver-void CloseNBADirectSeeding()方法"更新"名改爲"關閉NBA直播"。
  現實中就是這樣的,方法名本就不一定相同。
  抽象通知者由於不希望依賴抽象觀察者,所以增加或減少的方法也就沒有必要了(抽象觀察者已經不存在了)。
  通知者接口:interface Subject{void Notify();}
  下面就是如何處理老闆類和前臺類的問題,它們當中通知方法有了對觀察者遍歷,所以不可小視之。但如果在.NET中,我們可以用一個非常好的技術來處理這個問題,它叫委託。
  聲明一個委託,名稱叫EventHandler(事件處理程序),無參數,無返回值。delegate void EventHandler();Delegate用來實現函數方法的間接調用
  老闆類和前臺祕書類:聲明一個事件Update,類型爲委託EventHandler;public event EventHandler Update;(聲明一EventHandler(事件處理程序)的委託事件,名稱叫Update(更新))
  在訪問通知方法時,調用更新public void Notify(){Update();}
  客戶端:將看股票者的關閉股票程序方法和看NBA者的關閉NBA直播方法掛鉤到老闆的更新上,也就是將兩個不同類的不同方法委託給老闆類的更新了。boss.Update += new EventHandler(nbaObserver.CloseNBADirectSeeding);
14.9 事件委託說明
  委託就是一種引用方法類型。一旦爲委託分配了方法,委託將與該方法具有完全相同的行爲。委託方法的使用可以像其他任何方法一樣,具有參數和返回值。委託可以看作是對函數的抽象,是函數的類,委託的實例將代表一個具體的函數。
  delegate void EventHandler();可以理解爲聲明瞭一特殊的類,而public event EventHandler Update;可以理解爲聲明瞭一個類的變量(應該是聲明瞭一個事件委託變量叫更新)。
  委託的實例將代表一個具體的函數,意思是說,new EventHandler(nbaObserver.CloseNBADirectSeeding)其實就是一個委託的實例,而它就等於將nbaObserver.CloseNBADirectSeeding這個方法委託給boss.Update這個方法了。
  一旦爲委託分配了方法,委託將與該方法具有完全相同的行爲。而且,一個委託要以搭載多個方法,所有方法被依次響起。更重要的是,它可以使得委託對象所搭載的方法羨慕不需要屬於同一個類。
  這樣主使得,本來是在老闆類中的增加或減少的抽象觀察者集合以及通知時遍歷的抽象觀察者都不必要了。轉到客戶端來讓委託搭載多個方法,這樣就解決了本來與抽象觀察者的耦合問題。
  但委託也是有前提的,那就是委託對旬所搭載的所有方法必須具有相同的原形和形式,也就是擁有相同的參數列表和返回值類型。
  注意,是先有觀察者模式,再有委託事件技術的。
14.10 石守吉失手機後的委託
  石守吉的手機丟失,原來的同學號碼都沒有了,委託小菜幫忙把同學的號碼抄一份發郵件給他。但是班級人數多,抄起來容易出錯,而且,如果現在同學有急事要找,還是會找不到。
  使用觀察者模式,小菜在這裏給班級所有同學羣發一條短消息,通知他們,石守吉換新號,請大家更新號碼。

 

第15章 就不能不換DB嗎?——抽象工廠模式
15.1 就不能不換DB嗎?
  給一家企業做電子商務網站,是用SQL Server作爲數據庫的。而後,公司接到另外一家公司類似需求的項目,但這家公司只能用Access,於是就要改造原來的項目代碼。
15.2 最基本的數據訪問程序
  工廠方法模式是定義一個用於創建對象的接口,讓子類決定實例化哪一個類。
15.3 用了工廠方法模式的數據訪問程序
  代碼結構圖:
  IUser接口,用於客戶端訪問,解除與具體數據庫訪問的耦合。interface IUser{void Insert(User user);User GetUser(int id)}
  SqlserverUser類,用於訪問SQL Server的User。 class SqlserverUser:IUser
  Accessuser類,用於訪問Access的User。class AccessUser:IUser
  IFactory接口,定義一個創建訪問User表對象的抽象的工廠接口。interface IFactory{IUser CreateUser();}
  SqlServerFactory類,實現IFactory接口,實例化SqlserverUser。 class SqlServerFactory:IFactory{IUser CreateUser(){return new SqlserverUser();}}
  AccessFactor類,實現IFactory接口,實例化AccessUser。class AccessFactory:IFactory{IUser CreateUser(){return new AccessUser();}}
  客戶端代碼:IFactory factory = new SqlServerFactory();若要更改成Access數據庫,只需要將本名改成IFactory factory = new AccessFactory();
  但是,代碼裏還是有指明new SqlServerFactory(),要改的地方,依然很多。數據庫裏不可能只有一個User表, 很可能有其他表,比如增加部門表(Department表),此時如何辦?
15.4 用了抽象工廠模式的數據訪問程序
  添加Department表與User表步驟一樣。只需要改IFactory factory = new AccessFactory()爲IFactory factory = new SqlServerFactory(),就實現了數據庫訪問的切換了。
  其實在不知不覺中,已經通過需要的不斷演化,重構出了一個非常重要的設計模式。
  只有一個User類和User操作類的時候,是只需要工廠方法模式的,但現在顯然數據庫中有很多的表,而SQL Server與Access又是兩大不同的分類,所以解決這種涉及到多個產品系統的問題,有一個專門的工廠模式叫抽象工廠模式。
15.5 抽象工廠模式
  抽象工廠模式(Abstract Factory):提供一個創建一系列相關或相互依賴對象的接口,而無需指定它們具體的類。
  抽象工廠結構圖:
  AbstractFactory:抽象工廠接口,它裏面應該包含所有的商品創建的抽象方法 CreateProductA();CreateProductB();
  ConcreteFactory1:ConcreteFactory2:具體的工廠,創建具有特定實現的產品對象。
  AbstractProductA:AbstractProductB:抽象產品,它們都有可能有兩種不同的實現。
  ProductA1:ProductA2:ProductB1:ProductB2:對兩個抽象產品的具體分類的實現。
  AbstractProductA和AbstractProductB是兩個抽象產品, 之所以爲抽象,是因爲它們都有可能有兩種不同的實現,就剛纔的例子來說就是User和Department,而ProductA1、ProductA2、和ProductB1、ProductB2就是對兩個抽象產品的具體分類的實現,比如ProductA1可以理解爲是SqlserverUser,而ProductB1是AccessUser。
  可以這個理解,IFactory是一個抽象工廠接口,它裏面應該包含所有的產品創建的抽象方法,而ConcreteFactory1和ConcreteFactory2就是具體的工廠了。就像SqlserverFactory和AccessFactory一樣。
  通常是在運行時刻再創建一個ConcreteFactory類的實例,這個具體的工廠再創建具有特定實現的產品對象,也就是說,爲創建不同的產品對象,客戶端應使用不同的具體工廠。
15.6 抽象工廠的優點與缺點
  這樣做的好處是什麼呢?
  最大的好處便是易於交手產品系統,由於具體工廠類,例如IFactory factory = new AccessFactory(),在一個應該中只需在初始化的時候出現一次,這就使得改變一個應用的具體工廠變得非常容易,它只需改變具體工廠即可使用不同的產品配置。
  我們的設計不能去防止需要的更改,那麼我們的理想便是讓改動變得最小,現在如果你要更改數據庫訪問,我們只需要改具體工廠就可以做到。第二大好處是,它讓具體的創建實例過程與客戶端分離,客戶端是通過它們的抽象接口操縱實例,產品的具體類名也被具體工廠的實現分離,不會出現在客戶代碼中。事實上,剛纔寫的例子,客戶端所認識的只有IUser和IDepartment,至於它是用SQL Server來實現還是Access來實現就不知道了。
  抽象模式也有缺點,是個模式都會有缺點,都有不適用的時候。抽象工廠模式可以很方便地切換兩個數據庫訪問的代碼。但是如果需要來自增加功能,比如現在要增加項目表Project,需要改動哪些地方?
  那就至少要增加三個類,IProject、SqlserverProject、AccessProject,還需要更改IFactory、SqlserverFactory和AccessFactory纔可以完全實現。這太糟糕了。
  還有,客戶端程序類顯然不會是隻有一個,有很多地方都在使用IUser或IDepartment,而這樣的設計,其實在每一個類的開始都需要聲明IFactory factory = new SqlServerFactory,如果我有100個調用數據庫訪問的類,是不是就要更改100次IFactory factory = new SqlServerFactory這樣的代碼才行?這不能解決我要更改數據庫訪問時,改動一處就完全更改的要求。
  編程是門藝術,這樣大批量的改動,顯然是非常醜陋的做法。
15.7 用簡單工廠來改進抽象工廠
  去除IFactory、SqlserverFactory和AccessFactory三個工廠類,取而代之的是DataAccess類,用一個簡單工廠模式來實現。
  由於事先設置了db的值(Sqlserver和Access),所以簡單工廠的方法都不需要輸入參數,這樣在客戶端就只需要DataAccess.CreateUser和DataAccess.CreateDepartment()來生成具體的數據庫訪問類實例,客戶端沒有出現任何一個SQL Server或Access的字樣,達到了解耦的目的。
  但是,此方式還有些問題,原因是如果需要增加Oracle數據庫訪問,本來抽象工廠只需增加一個OracleFactory工廠類就可以了,現在就比較麻煩了。需要在DataAccess類中每個方法的switch中加case了
15.8 用反射+抽象工廠的數據訪問程序
  我們要考慮的就是可不可以在程序裏寫明,如果是Sqlserver就去實例化SQL Server數據庫相關類,如果是Access就去實例化Access相關類這樣的語句,而是根據字符串db的值去某個地方找應該要實例化的類是哪一個。這樣,我們的switch就可以對它說再見了。
  所謂反射,是指在運行時狀態中,獲取類中的屬性和方法,以及調用其中的方法的一種機制。這種機制的作用在於獲取運行時才知道的類(Class)及其中的屬性(Field)、方法(Method)以及調用其中的方法,也可以設置其中的屬性值。
  反射格式:.NET實現方式=Assembly.Load("程序集名稱").CreateInstance("命名空間.類名")、java實現方式=Class.forName("包名.類名");
  只要在程序的頂端寫上using SystemReflection;來引用Reflection,就可以使用反謝來幫我們克服抽象工廠模式的先天不足了。
  實例化效果是一樣的,但這兩種方法的區別在哪裏?
  常規方法是寫明瞭要實例化SqlserverUser對象。反謝的寫法,其實也是指明瞭要實例化SqlserverUser對象。常規方法不能靈活更爲AccessUser,反射中CreateInstance("抽象工廠模式.SqlserverUser")可以靈活更找SqlserverUser爲AccessUser。
  原因是這裏是字符串,可以用變量來處理,也就可以根據需要更換。
  總結這裏的差別主要在原來的實例化是寫死在程序裏,而現在用了反射就可以利用字符串來實例化對象,而變量是可以更換的。
  寫死在程序裏,太難聽了。準確地說,是將程序由編譯時轉爲運行時。由於CreateInstance("抽象工廠模式.SqlserverUser")中的字符串是可以寫成變量的,而變量的值到底是SQL Server,還是Access,完全可以由事先的那個db變量來決定。所以就去除了switch判斷的麻煩。
  總體感覺還是有點缺憾,因爲在更換數據庫訪問時,還是需要去改程序(改db這個字符串的值)重編譯,如果可以不改程序,那纔是真正地符合開放-封閉原則。
15.9 用反射+配置文件實現數據訪問程序
  可以利用配置文件來解決更改DataAccess的問題。添加一個App.confing文件,給DB字符串賦值,在配置文件中寫是Sqlserver還是Access,這樣就邊DataAccess類也不用更改了。
  現在我們應用於反射+抽象工廠模式解決了數據庫訪問的可維護、可擴展的問題。
  從這個角度來說,所有在用簡單工廠的地方,都可以考慮用反射技術來去除switch或if,解除分支判斷帶來的耦合。
15.10 無癡迷,不成功

 

第16章 無盡加班何時休——狀態模式
16.1 加班,又是加班
  上午狀態好,中午想睡覺,下午漸恢復,加班苦煎熬。其實是一種狀態的變化,不同的時間,會有不同的狀態。用代碼實現其實就是根據時間的不同,做出判斷來實現。
16.2 工作狀態-函數版
  定義一個“寫程序”的函數,用來根據時間的不同體現不同的工作狀態:WriteProgram(){if(Hour< 12){}else if(Hour<13){}}
16.3 工作狀態-分類版
  定義work工作類,提供void WriteProgram()方法,判斷不同的時間狀態
16.4 方法過長是壞味道
  方法很長,而且有很多的判斷分支,這也就意味着它的責任過大了。無論是任何狀態,都需要通過它來改變,任何需求的改動或增加,都需要去更改這個方法,這實際上是很糟糕的。面向對象設計其實就是希望做到代碼的責任分解。
  把這些分支想辦法變成一個又一個的類,增加時不會影響其他類。然後狀態的變化在各自的類中完成。針對這類問題Gof提供瞭解決方案,那就是狀態模式。
16.5 狀態模式
  狀態模式(State):當一個對象的內在狀態改變時允許改變期行爲,這個對象看起來你是改變了其類。
  狀態模式主要解決的是當控制一個對象狀態轉換的條件表達式過於複雜時的情況。把狀態的判斷邏輯轉移到表示不同狀態的一系列類當中,可以把複雜的判斷邏輯簡化。當然,如果這個狀態判斷很簡單,那就沒必要用狀態模式了。
  狀態結構圖:
  abstract class State{abstract void Handle(Context context);} State類,抽象狀態類,定義一個接口以封裝與Context的一個特定狀態相關的行爲。
  class ConcreteStateA:state{override void Handle(Context context){context.State = new ConcreteStateB();//設置ConcreteStateA的下一狀態是ConcreteStateB}} ConcreteState類,具體狀態,每一個子類實現一個與Context的一個狀態相關的行爲。
  class ConcreteStateB:state{override void Handle(Context context){context.State = new ConcreteStateA();//設置ConcreteStateB的下一狀態是ConcreteStateA}}
  class Context{State state;void Request(){state.Handle(this);//對請求做處理,並設置下一狀態}}Context類,維護一個ConcreteState子類的實例,這個實例定義當前的狀態。
  客戶端 Main{Context c = new Context(new ConcreateStateA();//設置Context的初始狀態爲ConcreteStateA c.Request();c.Request();//不斷的請求,同時更改狀態)}
16.6 狀態模式好處與用處
  狀態模式的好處是將與特定狀態相關的行爲局部化,並且將不同狀態的行爲分割開來。
  其實就是將特定的狀態相關的行爲都放入一個對象中,由於所有與狀態相關的代碼都存在於某個ConcreteState中,所以通過定義新的子類可以很容易地增加新的狀態和轉換。
  這樣的目的就是爲了消除龐大的條件分支語句,在的分支判斷會使得它們難以修改和擴展,就像我們最早說的刻版印刷一樣,任何改動和變化都是致命的。
  狀態模式通過把各種狀態轉換邏輯分成到State的子類之間,來減少相互間的依賴,好比把整個版面改成一個又一個的活字,此時就容易維護和擴展了。
  什麼時候考慮使用狀態模式呢?
  當一個對象的行爲取決於它的狀態,並且它必須在運行時刻根據狀態改變它的行爲時,就可以考慮使用狀態模式了。另外如果業務需求某項業務有多個狀態,通常都是一些枚舉變量,狀態的變化都是依靠大量的分支判斷語句來實現,此時應該考慮將每一種業務狀態定義爲一個State的子類。這樣這些對象就可以不依賴於其他對象而獨立變化了,某一天客戶需要更改需求,增加或減少業務狀態或改變狀態流程,對我們來說都是不困難的事。
16.7 工作狀態-狀態模式版
  代碼結構圖:
  抽象狀態類,定義一個抽象方法“寫程序” abstract class State{abstract void WriteProgram(Work w)}
  上午和中午工作狀態類:class ForenoonState:State{override void WriteProgram(Work w){if(w.Hour < 12){}else{w.State(new NoonState());w.WriteProgram();//超過12點,則轉入中午工作狀態}}}
  中午工作狀態類:class NoonState:State{override void WriteProgram(Work w){if(w.Hour < 13){}else{w.State(new AfternoonState());w.WriteProgram();超過13點,則轉入下午工作狀態}
  下午和傍晚工作狀態類:
  晚間工作狀態類:
  睡眠狀態和下班休息狀態類:
  工作類,此時沒有了過長的分支判斷語句:class Work{Work(){sate = new ForenoonState();//工作初始化爲上午工作狀態,即上午9點開始上班}}
  此時的代碼,如果要完成“員工必須在20點之前離開公司”,我們只需要增加一個“強制下班狀態”,並改動一個傍晚工作狀態類的判斷就可以了,而這是不影響其他狀態的代碼的。

 

 第17章 在NBA我需要翻譯——適配器模式
17.1 在NBA我需要翻譯
17.2 適配器模式
  適配器模式(Adapter):將一個類的接口轉換成客戶希望的另外一個接口。Adapter模式使得原來由於接口不兼容而是不能一起工作的那些類可以一起工作。
  適配器模式主要解決什麼問題呢?
  簡單地說,就是需要的東西就在面前,但卻不能使用,而短時間又無法改造它,於是我們想辦法適配它。
  適配是什麼意思?
  這個詞最早出現在電工學裏,有些國家用110V電壓,而我們國家用的是220V,但我們的電器,比如筆記本電腦是不能什麼電壓都能用的,但國家不同,電壓可能不相同也是事實,於是就用一個電源適配器,只要是電,不管多少伏,都能把電源變成需要的電壓,這就是電源適配器的作用。適配器的意思就是使得一個東西適合另一個東西的東西。
  姚明剛到NBA打球,之前又沒有時間在學校裏認真學好英語,馬上學到可以聽懂會說的地步是很困難的。
  在我們不能更改球隊的教練、球員和姚明的前題下,我們能做的就是想辦法找個適配器。
  在軟件開發中,也就是系統的數據和行爲都正確,但接口不符時,我們應該考慮用適配器,目的是使用控制範圍之外的一個原有對象與某個接口匹配。適配器模式主要應用於希望複用一些現存的類,但是掊口又與複用環境要求不一致的情況,比如在需要對早期代碼複用一些功能等應用上很有實際價值。
  適配器(Adapter)結構圖:
  class Target{virtual void Request(){普通請求}};Target這是客戶所期待的接口。目標可以是具體的或抽象的類,也可以是接口。
  class Adaptee{void SpecificRequest(){特殊請求};};Adaptee需要適配的類
  class Adapter:Target{new Adaptee()//建立一個私有的Adaptee對象;override void Request(){adaptee.SpecificRequest();//這樣就可以把表面上調用Request()方法變成實際調用SpecificRequest()};}
  class Main{Target target = new Adater();target.Request();//對客戶端來說,調用的就是Target的Request()};客戶端類
17.3 何時使用適配器模式
  在想使用一個已經存在的類,但如果它的接口,也就是它的方法和你的要求不相同是時,就應該考慮用適配器模式。
  兩個類所做的事情相同或相似,但是具有不同的接口時要使用它。而且由於類都共享同一個接口,使用客戶代碼可以統一調用同一接口,這樣可以更簡單、更直接、更緊湊。
  其實用適配器模式也是無奈之舉,很有點“亡羊補牢”的感覺,沒辦法,是軟件就有維護的一天,維護就有可能會因不同的開發人員、不同的產品、不同的廠家而造成功能類似而接口不同的情況,此時就是適配器模式在展拳腳的時候了。
  有沒有之初就需要考慮用適配器模式的時候?
  當然有,比如公司設計一系統時考慮使用第三方開發組件,而這個組件的接口與我們自己的系統接口是不相同的,而我們也完全沒有必要了爲迎合它而改動自己接口,此時儘管是在開發的設計階段,也是可以考慮用適配器模式來解決接口不同的問題。
17.4 籃球翻譯適配器
17.5 適配器模式的.NET應用
  在.NET中有一個類庫已經實現的、非常重要的適配器,那就是DataAdaper。DataAdapter用作DataSet和數據源之間的適配器以便檢索和保存數據。DataAdapter通過映射Fill(這更改了DataSet中的數據以便與數據源中的數據相匹配)和Update(這更改了數據源中的數據以便與DataSet中的數據相匹配)來提供這一適配器。由於數據源可能是來自SQL Server,可能來自Oracle,也可能來自Access、DB2,這些數據在組織上可能有不同之處,但我們希望得到統一的DataSet(實質是XML數據),此時用DataAdapter就是非常好的手段,我們不必關注不同數據庫的數據細節,就可以靈活的使用數據。
17.6 扁鵲的醫術(詳情見書)
  如果能事先預防接口不同的問題,不匹配問題就不會發生;在有小的接口不統一問題發生時,及時重構,問題不至於擴大;只有碰到無法改變原有設計和代碼的情況時,才考慮適配
  事後控制不如事中控制,事中控制不如事前控制。適配器模式當然是好模式,但如果無視它的應用場合而盲目使用,其實是本末倒置了。

 

第18章 如果再回到從前——備忘錄模式
18.1 如果再給我一次機會......
  單機的PC遊戲,通常都是在打大Boss之前,先保存一個進度,然後如果通關失敗了,可以再返回剛纔那個進度來恢復原來的狀態,從頭來過。
  通常這種保存都是存在磁盤上了,以便日後讀取。但對於一些更爲常規的應用,比如我們下棋時需要悔棋、編寫文檔時需要撤銷、查看網頁時需要後退,這些相對頻繁而簡單的恢復並不需要存在磁盤中,只要將保存在內存中的狀態恢復一下即可。這是更普通的應用,很多開發中都會用到。
  實現場景,用代碼實現。一遊戲角色有生命力、攻擊力、防禦力等等數據,在打Boss前和後一定會不樣的,我們允許玩家如果感覺與Boss決鬥的效果不理想可以讓遊戲恢復到決鬥前。
18.2 遊戲存進度
  class GameRole{int vit;//生命力 int atk;//攻擊力 int def;//防禦力 void StateDisplay(){//狀態顯示};void GetInitState(){vit=atk=def=100//獲得初始狀態};void Fight(){vit=atk=def=0//戰鬥}};遊戲角色類,用來存儲角色的生命力、攻擊力、防禦力的數據。
  class Main{front = new GameRole();//大戰Boss前獲得初始角色狀態 backup = new GameRole();backup = front;//保存進度,通過遊戲角色的新實例,來保存進度 front.Fight();大戰Boss時,損耗嚴重所有數據全部損耗爲零 front = backup;遊戲結束不甘心,恢復之前進度,重新來玩};
  以上代碼問題很多,主要在於這客戶端的調用。因爲這樣寫就把整個遊戲角色的細節暴露給了客戶端,你的客戶端職責就太大了,需要知道遊戲角色的生命力、攻擊力、防禦力這些細節,還要對它進行備份。以後需要增加新的數據,例如增加魔法力或修改現在的某種力,例如生命力改爲經驗值,這部分就一定要修改。同樣的道理也存在於恢復時的代碼。
  顯然,我們希望的時把這些遊戲角色的存取狀態細節封裝起來,而且最好是封裝在外部的類當中。以體現職責分離。
18.3 備忘錄模式
  備忘錄(Memento):在不破壞封裝性的前提下,捕獲一個對象的內部狀態,並在該對象之外保存這個狀態。這樣以後就可將該對象恢復到原先保存的狀態。
  備忘錄模式結構圖:
    Originator(發起人。對應遊戲角色類):負責創建一個備忘錄Memento,用以記錄當前時刻它的內部狀態,並可使用備忘錄恢復內部狀態。Originator可根據需要決定Memento存儲Originator的哪些內部狀態。
 Memento(備忘錄):負責存儲Originator對象的內部狀態,並可防止Originator以外的其他對象訪問備忘錄Memento。備忘錄有兩個接口,Caretaker只能看到備忘錄的窄接口,它只能將備忘錄傳遞給其他對象。Originagor能夠看到一個寬接口,允許它訪問返回到先前狀態所需的所有數據。
 Caretakeer(管理者):負責保存好備忘錄Memento,不能對備忘錄的內容進行操作或檢查。
 就剛纔的例子,遊戲角色類其實就是一個Originator,而用了同樣的遊戲角色實例備份來做備忘錄,這在當需要保存全部信息時,是可以考慮的,而用clone(克隆)的方式來實現Memento的狀態保存可能是更好的辦法,但是如果是這樣的話,使得我們相當於對上層應用開放了Originator的全部(public)接口,這對於保存備份有時候是不合適的。
 如果不需要保存全部的信息以備使用時,這或許是更多可能發生的情況,我們需要保存的並不是全部信息,而只是部分,那麼就應該有一個獨立的備忘錄類Memento,它只擁有需要保存的信息的屬性。
18.4 備忘錄模式基本代碼
  發起人類:class Originator{string state;//需要保存的屬性,可能有多個 Memento CreateMemento(){return (new Memento(state));//創建備忘錄,將當前需要保存的信息導入並實例化一個Memento對象};SetMenento(Memento memento){state = memento.State;//恢復備忘錄,將Memento導入並將相關數據恢復}}。
  備忘錄類:class Memento{string state;Memento(string state){//構造方法,將相關數據導入};string State{get{return state;//需要保存的數據屬性,可以是多個};}}。
  管理者類:class Createker{Memento memento; Memento Memento{get{return memento;}set{memento = value;}//得到或設置備忘錄}}。
  客戶端類:class Main{Originator o = new Originator();o.State="on";//Originator初始狀態,狀態屬性爲On Caretakeer c = new Caretakeer();c.Memento = o.CreateMemento();//保存狀態時,由於有了很好的封裝,可能隱藏Originator的實現細節 o.State = "off";//Originator改變了狀態屬性爲off o.SetMenento(c.Memento);//恢復原初始狀態}
  這當中就是把要保存的細節給封裝在了Memento中了,哪一天要更改保存的細節也不用影響客戶端了。
  備忘錄模式都用在一些什麼場合呢?
  Memento模式比較適用於功能比較複雜的,但需要維護或記錄屬性歷史的類,或者需要保存的屬性只是衆多屬性中的一小部分時,Originator可以根據保存的Memento信息還原到前一狀態。
  命令模式也有實現類似撤銷的作用,如果在某個系統中使用命令模式時,需要實現命令的撤銷功能,那麼命令模式可以使用備忘錄模式來存儲可撤銷操作的狀態。有時一些對象的內部信息必須保存在對象以外的地方,但是必須要由對象自己讀取,這時,使用備忘錄可以把複雜的對象內部信息對其他的對象屏蔽起來,從而可以恰當地保持封裝的邊界。
  其最大的作用還是在當角色的狀態改變的時候,有可能這個狀態無效,這時候就可以使用暫時存儲起來的備忘錄將狀態復原。
18.5 遊戲進度備忘
  備忘錄模式也是有缺點的,角色狀態需要完整存儲到備忘錄對象中,如果狀態數據很大很多,那麼在資源消耗上,備忘錄對象會非常耗內存。所以也不是用得越多越好。

 

第19章  分公司=-部門——組合模式
19.1 分公司就就是一部門嗎?
  有一家公司,總部在北京,在全國幾大城市高有分公司,比如上海設有華東區分部,然後在一些省會城市還設有辦事處,比如南京辦事處、杭州辦事處。現在有個問題是,總公司的人力資源部、賬務部等辦公管理功能在所有的分公司或辦事處都需要有,該怎麼辦?
  之前講過簡單的複製是最糟糕的設計,所以想法是共享功能到各個分公司,也就是讓總部、分公司、辦事處用同一套代碼,只是根據ID的不同來區分。
  這種方法不可區,因爲他們的要求,總部、分部和辦事處是成樹狀結構的,也就是有組織結構的,不可以簡單的平行管理,因爲實際開發時就得一個一個的判斷它是總部,還是分公司的財務,然後再執行其相應的方法。
  這種情況很多見,例如賣電腦的商家,可以賣單獨配件也可以賣組裝整機,又如複製文件,可以一個一個文件複製粘貼還可以整個文件夾進行復制,再比如文本編輯,可以給單個字加粗、變色、改字體,當然也可以給整段文字做同樣的操作。其本質都是同樣的問題。
  其實分公司或辦事處與總公司的關係,就是部分與整體的關係。
  希望總公司的組織結構,比如人力資源部、財務部的管理功能可以複用於分公司。這其實就是整體與部分可以被一致對待的問題。
  來分析一下剛纔講到的這個項目,如果把北京總公司當做一棵大樹的根部的話,它的下屬分公司其實就是這棵樹的分析,至於各辦事處是更小的分支,而它們的相關的職能部門由於沒有分枝了,所以可以理解爲樹葉。
  也就是說,所希望的總部的財務管理功能也最好是能複用到子公司,那麼最好的辦法就是,我們在處理總公司的賬務管理功能和處理子公司的賬務管理功能的方法都是一樣的。這涉及到一個設計模式叫組合模式。
19.2 組合模式
  組合模式(Composite):將對象組合成樹形結構以表示“部分-整體”的層次結構。組合模式使得用戶對單個對象和組合對象的使用具有一致性。
  組合模式結構圖:
    Component爲組合中的對象聲明接口,在適當情況下,實現所有類共有接口的默認行爲。聲明一個接口用於訪問和管理Component的子部件。abstract class Component{void Add(Component c){};void Remove(Component c){};//通常都用Add和Remove方法來提供增加或移除樹葉或樹葉的功能 void Display(int depth){}}
 Leaf在組合中表示葉節點對象,葉節點沒有子節點。calss Leaf:Component{override void Add(Component c){};override void Remove(Component c){};};由於葉子沒有再增加分枝和樹葉,所以Add和Remove方法實現它沒有意義,但這樣做可以消除葉節點和枝節點對象在抽象層次的區別,它們具備完全一致的接口。
 Composite定義有枝節點行爲,用來存儲子部件,在Component接口中實現與子部件有關的操作,比如增加Add和刪除Remove。class Composite:Component{children=new List<Component>();//一個子對象集合用來存儲其下屬的枝節點和葉節點 override void Add(Component c){children.Add(c)};override void Remove(Component c){children.Remove(c)};override void Display(int depth){foreach(Component component in children){component.Display(depth + 2)}}//顯示其枝節點名稱,並對其下級進行遍歷}
  客戶端代碼,能通過Component接口操作組合部件的對象。class Main{Composite root = new Composite("root");//生成樹根據root,根上長出兩葉LeafA和LeafB}
19.3 透明方式與安全方式
  樹可能有無數的分枝,但只需要反覆用Composite就可以實現樹狀結構了。有點疑問,爲什麼Leaf類當中也有Add和Remove,樹葉不是不可以再長分枝嗎?
  是的,這種方式叫做透明方式,也就是說在Component中聲明所有來管理子對象的方法,其中包括Add、Remove等。這樣實現Component接口的所有子類都具備了Add的Remove。這樣做的好處就是葉節點和枝節點對於外界沒有區別,它們具備完全一致的行爲接口。但問題也很明顯,因爲Leaf類本身不具備Add()、Remove()方法的功能,所以實現它是沒有意義的。
  那麼如果不希望做這樣的無用功呢?也就是Leaf類當中不用Add和Remove方法,可以嗎?
  當然是可以的,那麼就需要安全方式,也就是在Component接口中不去聲明Add和Remove方法,那麼子類的Leaf也就不需要去實現它,而是在Composite聲明所有用來管理子類對象的方法,這樣做就不會出現剛纔提到的問題,不過由於不夠透明,所以樹葉和樹枝類將不具備有相同的接口,客戶端的調用需要做相應的判斷,帶來了不便。
19.4 何時使用組合模式
  什麼地方用組合模式比較好呢?
  當發現需求中是體現部分與整體層次的結構時,以及你希望用戶可以忽略組合對象與單個對象的不同,統一地使用組合結構中的所有對象時,就應該考慮用組合模式了。
  以前曾經用過的ASP.NET和TreeView控件就是典型的組合模式應用。還有自定義控件,也就是把一些基本的控件組合起來,通過編程寫成一個定製的控件,比如用兩個文本框和一個按鈕就可以寫一下自定義的登錄框控件,所有的Web控件的基類都是System.Web.UI.Control,而Control基類中就有Add和Remove方法,這就是典型的組合模式的應用。
19.5 公司管理系統
  公司抽象類或接口 abstract class Company{void Add(Company c);//增加 void Remove(Company c);//移除 void LineOfDuty();//履行職責,不同的部門需要履行不同的職責}
  具體公司類,實現接口 樹枝節點 class ConcreteCompany:Company{children = new List<Company>();override void LineOfDuty(){foreach(Company component in children){component.LineOfDuty();}}}
  人力資源部與賬務部類 樹葉節點 class HRDepartment:Company{override void LineOfDuty(){"{0}員工招聘培訓管理",name}}
  賬務部 樹葉節點 class FinanceDepartment:Company{override void LineOfDuty(){"{0}公司賬務收支管理",name}}
  客戶端調用 class Main{ConcreteCompany root = new ConcreteCompany("北京總公司");root.Add(new HRDepartment(“總公司人力資源部”));root.Add(new FinanceDepartment("總公司賬務部"));ConcreteCompany root = new ConcreteCompany("上海華東分公司")}
19.6 組合模式好處
  組合模式這樣就定義了包含人力資源部和財務部這些基本對象和分公司、辦事處等組合對象的類層次結構。基本對象可以被組合成更復雜的組合對象,而這個組合對象又可以被組合,這樣不斷地遞歸下去,客戶代碼中,任何用到基本對象的地方都可以使用組合對象了。
  還有,用戶是不用關心到底是處理一個葉節點還是處理一個組合組件,也就用不着爲定義組合而寫一些選擇判斷語句了。簡單點說,就是組合模式讓客戶可以一致地使用組合結構和單個對象。

 

第20章 想走?可以!先買票——迭代器模式
20.1 乘車買票,不管你是誰!
  每個售票員都在做一件重要的事,就是把車廂裏的所有人都遍歷了一遍,不放過一個不買票的乘客。這也是一個設計模式的體現。迭代器模式。
20.2 迭代器模式
  迭代器模式(Iterator):提供一種方法順序訪問一個聚合對象中各個元素,而又不暴露該對象的內部表示。
  售票員不管你上來的是人不是物(行李),不管是中國人還是外國人,不管是不是內部員工,甚至哪怕是馬上要抓走的小偷,只要是來乘車的乘客,就必須要買票。同樣道理,當你需要訪問一個聚集(集合)對象,而且不管這些對象是什麼都需要遍歷的時候,就應該考慮用迭代器模式。另外,售票員從車頭到車尾來售票,也可以從車尾向車頭售票,也就是說,你需要對聚集有多種方式遍歷時,可以考慮用迭代器模式。
  由於不管乘客是什麼,售票員的做法始終是相同的,都是從第一個開始,下一個是誰,是否結束,當前售到哪個人了,這些方法每天他都在做,也就是說,爲遍歷不同的聚集結構提供如開始、下一個、是否結束、當前哪一項等統一接口。
20.3 迭代器實現
  迭代器模式結構圖:
  abstract class Iterator{object First();object Next();boolean IsDone();object CurrentItem();} 迭代抽象類,用於定義得到開始對象、得到下一個對象、判斷是否到結尾、當前對象等抽象方法,統一接口。
  abstract class Aggregate{abstract Iterator CreateIterator();//創建迭代器}聚集(集合)抽象類
  class ConcreateIteator:Iterator{ConcreteAggregate aggregate;//定義一個具體的聚集(集合對象);override object First(){return aggregate[0]}}
  class ConcreateAggregte:Aggregate{private List<object> items = new List<object>;//聲明一個List泛型變量,用於存放聚合(集合)對象,用ArrayList同樣可以實現;override Iterator CreateIterator(){return new ConcreateIteator(this)}}
  客戶端類class Main{ConcreteAggregate a = new ConcreateAggregte();//公交車,即聚集(集合)對象 a[0]=老外;a[0]=小偷;//新上來的乘客,即對象數組 Iterator i = new ConcreateIteator(a);//售票員出場,先看好了上車的是哪些人,即聲明迭代器對象}
  爲什麼要用具體的迭代器ConcreteIterator來實現抽象的Iterator呢?感覺這裏不需要抽象,直接訪問ConcreteIterator不是更好嗎?
  那是因爲剛纔有一個迭代器的好處沒注意,當你需要對聚集(集合)有多種方式遍歷時,可以考慮用迭代器模式,事實上,售票員不一定要從車頭到車尾這樣售票,還可以從後向前遍歷。
20.4 .NET的迭代器實現
  不只有.NET,java也同樣提供有現成的迭代器框架相關接口,只需要去實現就好。例如ArrayList:
  我們不需要顯式的引用迭代器,但系統本身還是通過迭代器來實現遍歷的。總地來說,迭代器模式就是分離了集合對象的遍歷行爲,抽象出一個迭代器來負責,這樣即可以做到不暴露集合的內部結構,又可讓外部代碼透明地訪問集合內部的數據。
20.5 迭代高手

 

第21章 有些類也需要計劃生育——單例模式
21.1 類也需要計劃生育
  每點擊一次工具箱的菜單項,就產生一個新的工具箱窗體,但實際上,只希望它出現一次,或者乾脆不出現。
21.2 判斷對象是否是null
  這個其實不難辦,判斷一下,這個FromToolbox有沒有實例化過不就行了。
  爲什麼要在點擊按鈕時才聲明FormToolbox對象呢,完全可以把聲明的工作放到類的全局變量中完成。這樣就可以去判斷這個變量是否被實例化過了。
21.3 生不是不生是自己的責任
  夫妻已經有一個小孩子,下面是否生第二胎,這是誰來負責?
  當然是他們自己負責,要是超生了,違反了國家的政策,那也是他們自己的原因。
  再想想這種場景:領導問下屬,報告交了沒有,下屬可以說早交了,於是領導滿意地點點頭,下屬也可以說還剩下一點內容沒寫,很快上交,領導皺起眉頭說要抓緊。此時這份報告交還是沒交,由誰來判斷?
  當然是下屬自己的判斷,因爲下屬最清楚報告交了沒有,領導只需要問問就行了。
  同樣的,現在工具箱FromToolbox是否實例化都是在MDI主窗體From1的代碼裏判斷,這並不合邏輯,Form1裏應該只是通知啓動工具箱,至於工具箱窗體是否實例化過,應該由工具箱自己來判斷。
  實例化其實就是new的過程,但問題就是我怎麼讓人家不用new呢?
  是的,如果不對構造方法做改動的話,是不可能阻止他人不去用new的。所以我們完全可以直接把這個類的構造方法改成私有(private)。
  所有類都有構造方法,不編碼則系統默認生成空的構造方法,若有顯示定義的構造方法,默認的構造方法就會失效。於是只要將工具箱類的構造方法寫成是private的,那麼外部程序就不能用new來實例化它了。
  對於外部代碼,不能用new來實例化它,但是我們完全可以重寫一個public方法,叫做GetInstance(),這個方法的目的就是返回一個類實例,而此方法中,去做是否有實例化的判斷。如果沒有實例化過。由調用private的構造方法new出這個實例,之所以它可以調用是困爲它們在同一個類中,private方法可以被調用的。
  這樣一來,客戶端不再考慮是否需要去實例化的問題,而把責任都給了應該負責的類去處理。其實這就是一個很基本的設計模式:單例模式。
21.4 單例模式
  單例模式(singleton):保證一個類僅有一個實例,並提供一個訪問它的全局訪問點。
  通常我們可以讓一個全局變量使得一個對象被訪問,但它不能防止你實例化多個對象。一個最好的辦法就是,讓類自身負責保存它的唯一實例。這個類可以保證沒有其他實例可以被創建,並且它可以提供一個訪問該實例的方法。
  單例模式結構圖:
    Singleton類,定義一個GetInstance操作,允許客戶訪問它的唯一實例。GetInstance是一個靜態方法,主要負責創建自己的唯一實例。
  單例模式除了可以保證唯一的實例處,還有什麼好處呢?
  比如單例模式因爲Singleton類封裝它的唯一實例,這樣它可以嚴格地控制客戶怎樣訪問它以及何時訪問它。簡單地說就是對唯一實例的受控訪問。
  怎麼感覺單例有點像一個實用類的靜態方法,比如.net框架裏的Math類,有很數據計算方法,這兩者有什麼區別呢?
  它們之間的確很類似,實用類通常也會採用私有化的構造方法來避免其有實例。但它們還是有很多不同的:
  1、比如實用類不保存狀態,僅提供一些靜態方法或靜態屬性讓你使用,而單例類是有狀態的。
  2、實用類不能用於繼承多態,而單例雖然實例唯一,卻是可以有子類來繼承。
  3、實用類只不過是一些方法屬性的集合,而單例卻是有着唯一的對象實例。
21.5 多線程時的單例
  需要注意一些細節,比如說,多線程的程序中,多個線程同時,注意是同時訪問Singleton類,調用GetInstance()方法,會有可能造成創建多個實例的。
  可以給進程一把鎖來處理。這裏需要解釋一下lock語句(java中使用synchronized)的涵義,lock是確保當一個線程位於代碼的臨界區時,另一個線程不進入臨界區。如果其他線程試圖進入鎖定的代碼,則它將一直等待(即被阻止),直到該對象被釋放。
  class Singleton{private static Singleton instance;private static readonly object syncRoot = new object();//程序運行時創建一個靜態只讀的進程輔助對象 singleton GetInstance(){lock(syncRoot){if(instance == null){instance = new  Singleton();}}//在同一個時刻加了鎖的那部分程序只有一個線程可以進入}}
  這段代碼使得對象實例由最先進入的那人線程創建,以後的線程在進入時不會再去創建對象實例了。由於有了lock,就保證了多線程環境下的同時訪問也不會造成多個實例的生成。
  爲什麼不直接lock(instance),而是再創建一個syncRoot來lock呢?
  因爲在加鎖時,instance實例有沒有被創建過實例都不知道,怎麼對它加鎖呢。但是還有問題,就是每次調用GetInstance方法時都需要lock,這種做法是會影響性能的,所以對這個類再做改良。
21.6 雙重鎖定
  class Singleton{public static Singleton GetInstance(){if(instance == null)//先判斷實例是否存在,不存在再加鎖處理{lock(syncRoot){if(instance == null){instance = new Singleton();}}}}}
  現在這樣,我們不用讓線程每次都加鎖,而只是在實例未被創建的時候再加鎖處理。同時也能保證多線程的安全。這種做法被稱爲Double-Check Locking(雙重鎖定)。
  有個問題,在外面已經判斷了instance實例是否存在,爲什麼在lock裏面還需要做一次instance實例是否存在的判斷呢?
  仔細分析下,對於instance存在的情況,就直接返回,這沒有問題。當instance爲null並且同時有兩個線程調用GetInstance()方法時,它們將都可以通過第一重instance==null的判斷。然後由於lock機制,這兩個線程則只有一個進入,另一個在外排隊等候,必須要其中的一個進入並出來後,另一個才能進入。而此時如果沒有第二重的instance是否爲null的判斷,則第一個線程創建了實例,而第二個線程還是可以繼續再創建新的實例,這就沒有達到單例的目的。
21.7 靜態初始化
  由於構造方法是私有的,因此不能在類本身以外實例化Singleton類;因此,變量引用的是可以在系統中存在的唯一的實例。
  只能在靜態初始化期間或在類構造函數中分配變量,這種靜態初始化的方式是在自己被加載時就將自己實例化,所以被形象地稱之爲餓漢式單例類。
  原先的單例模式處理方法是要在第一次被引用時,纔會將自己實例化,所以就被稱爲懶漢式單例類。
  餓漢-懶漢,它們主要有什麼區別呢?
  由於餓漢式,即靜態初始化的方式,它是類一加載就實例化的對象,所以要提前佔用系統資源。
  懶漢式,又會面臨着多線程訪問的安全性問題,需要做雙重鎖定這樣的處理纔可以保證安全。所以到底使用哪一種方式,取決於實際的需求。從C#語言角度來講,餓漢式的單例類已經足夠滿足我們的需求了。

 

第22章 手機軟件何時統一——橋接模式
22.1 憑什麼你的遊戲我不能玩
  你的手機是M品牌的,我的是N品牌的,按道理我這裏的遊戲你是不能玩的。
  同品牌的手機、型號不同,軟件還算是基本兼容,可惜不同品牌,軟件基本還是不能整合在一起。
  然而,在計算機領域裏,就完全不一樣了。比如由於有了Window操作系統,使得所有的PC廠商不用關注軟件,而軟件製造商也不用過多關注硬件,這對計算機的整體發展是非常有利的。
22.2 緊耦合的程序演化
  手機硬件軟件和PC硬件軟件,其實蘊含兩種完全不同的思維方式。
  如果我現在有一個N品牌的手機,它有一個小遊戲,我要玩遊戲,程序應該如何寫?
  寫一個此品牌的遊戲類,再用客戶端調用即可。
  N品牌的手機中的遊戲:class HandsetNGame{void Run(){//運行N品牌手機遊戲}}
  客戶端代碼:class Main{game = new HandsetNGame();game.Run();}
  現在又有一個M品牌的手機,也有小遊戲,客戶端也可以調用。
  兩個品牌,都有遊戲,覺得從面向對象的思想來說,應該有一個父類手機品牌遊戲,然後讓N和M品牌的手機遊戲都繼承於它,這樣可以實現同樣的運行方法。
  由於手機都需要通訊錄功能,於是N品牌和M品牌都增加了通訊錄的增刪改查功能。如何處理?
  那就意味着,父類應該是手機品牌,下有手機品牌M和手機品牌N,每個子類下各有通訊錄和遊戲子類。
  代碼結構圖:
    手機類
    手機品牌類:class HandsetBrand{void Run(){}}
 手機品牌N和手機品牌M類:class HandsetBrandM:HandsetBrand{void Run(){}}
 下屬的各自通訊錄類和遊戲類
 手機品牌M的遊戲類:class HandsetBrandMGame:HandsetBrandM{override void Run(){//運行M品牌手機遊戲}}
 手機品牌M的通訊錄類:class HandsetBrandMAddressList:HandsetBrandM{override void Run(){//運行M品牌手機通訊錄}}
 手機品牌N同上
  如果現在需要每個品牌都增加一個MP3音樂播放功能,如何做?
  那就在每個品牌的下面都增加一個子類。如果又來了一家新的手機品牌S,它也有遊戲、通訊錄、MP3音樂播放器功能,那就得再增加手機品牌S類和三個下屬功能子類,這顯然有點麻煩了。
  感覺一直在用面向對象的理論設計的,先有一個品牌,然後多個品牌就抽象出一個品牌抽象類,對於每個功能,就都繼承各自的品牌。或者,不從品牌,從手機軟件的角度去分類,這有什麼問題呢?
  問題是有的,這就好比是有了新錘子,所有的東西看上去都成了釘子。但事實上,很多情況有繼承會帶來麻煩比如,對象的繼承關係是在編譯時就定義好了,所以無法在運行時改變從父類繼承的實現。子類的實現與它的父類有非常緊密的依賴關係,以至於父類實現中的任何變化必然會導致子類發生變化。當你需要複用子類時,如果繼承下來的實現不適合解決新的問題,則父類必須重寫或被其他更適合的類替換。這種依賴關係限制了靈活性並最終限制了複用性。
  如果按照前面的繼承結構,不斷地增加新品牌或新功能,類會越來越多的。
  在面向對象設計中,我們還有一個很重要的設計原則,那就是合成(組合)/聚合複用原則。即優先使用對象合成(組合)/聚合,而不是類繼承。
22.3 合成(組合)/聚合複用原則
  合成(組合)/聚合複用原則:儘量使用合成/聚合,儘量不要使用類繼承。
  合成(Composition,也有翻譯成組合)和聚合(Aggregation)都是關聯的特殊種類
  聚合表示一種弱的‘擁有’關係,體現的是A對象可以包含B對象,但B對象不是A對象的一部分;
  合成則是一種強的‘擁有’關係,體現了嚴格的部分和整體的關係,部分和整體的生命週期一樣。
  比方說,大雁有兩個翅膀,翅膀與大雁是部分和整體的關係,並且它們的生命週期是相同的,於是大雁和翅膀就是合成關係。
  而大雁是羣居動物,所以每隻大雁都是屬於一個雁羣,一個雁羣可以有多隻大雁,所以大雁和雁羣是聚合關係。
  合成/聚合複用原則的好處是,優先使用對象的合成/聚合將有助於你保持每個類被封裝,並被集中在單個任務中。這樣類和類繼承層次會保持較小規模,並且不太可能增長爲不可控制的龐然大物。
  就剛纔的例子,需要學會用對象的職責,而不是結構來考慮問題。其實答案就在之前我們聊到的手機與PC電腦的差別上。
  手機是不同的品牌公司,各自做自己的軟件,就像現在的設計一樣,而PC卻是硬件廠商做硬件,軟件廠商做軟件,組合起來纔是可以用的機器。
  實際上,像遊戲、通訊錄、MP3音樂播放這些功能都是軟件,如果我們可以讓其分離與手機的耦合,那麼就可以大大減少面對新需求是改動過大的不合理情況。
  意思其實就是應該有個手機品牌(硬件)抽象類和手機軟件(軟件)抽象類,讓不同的品牌和功能都分別繼承於它們,這樣要增加新的品牌或新的功能都不用影響其他類了。
  手機品牌和手機軟件之間的關係,是手機品牌包含有手機軟件,但軟件並不是品牌的一部分,所以它們之間是聚合關係。
22.4 鬆耦合的程序
  手機軟件抽象類
  手機軟件:abstract class HandsetSoft{abstract void Run();}
  遊戲、通訊錄等具體類
  手機遊戲:class HandsetGame:HandsetSoft{override void Run(){//運行手機遊戲}}
  手機品牌(硬件類)
  手機品牌:abstract class HandsetBrand{HandsetSoft soft;void SetHandsetSoft(HandsetSoft soft){this.soft = soft;}//品牌需要關注軟件,所以可以機器中安裝軟件(設置手機軟件),以備運行 abstract void Run();}
  品牌N品牌M具體類
  手機品牌N:class HandsetBrandN:HandsetBrand{override void Run(){soft.Run();}}
  客戶端調用:
  現在如果要增加一個功能,比如MP3音樂播放功能,那麼只要增加這個類就和地。不會影響其他任何類。類的個數增加也只是一個。
  如果是要增加S品牌,只需要增加一個品牌子類就可以了。個數也是一個,不會影響其他類的改動。
  這顯然是也符合了我們之前的一個開放-封閉原則。這樣的設計顯然不會修改原來的代碼,而只是擴展類就行了。但最深的是合成/聚合複用原則,也就是優先使用對象的合成或聚合,而不是類繼承。聚合的魅力無限。相比,繼承的確很容易造成不必要的麻煩。
  盲目使用繼承當然就會造成麻煩,而其本質原因主要是什麼?
  應該是,繼承是一種強耦合的結構。父類變,子類就必須要變。
  所以我們在使用繼承時,一定要在是is-a的關係時再考慮使用,而不是任何時候都去使用。今天這個例子應該的一個設計模式叫做橋接模式。
22.5 橋接模式(Bridge)
  橋接模式:將抽象部分與它的實現部分分離,使它們都可以獨立地變化。
  這裏需要理解一下,什麼叫抽象與它的實現分離,這並不是說,讓抽象類與派生類分離,因爲這沒有任何意義。實現指的是抽象類和它的派生類用來實現自己的對象。就剛纔的例子,就是讓手機即可以按照品牌來分類,也可以按照功能來分類。
  橋接模式的核心意思就是把這些實現獨立出來,讓它們各自地變化。這就使得每種實現的變化不會影響其他實現,從而達到應對變化的目的。
22.6 橋接模式基本代碼
  橋接模式所說的將抽象部分與它的實現部分分離,還是不好理解。通俗的理解就是實現系統可能有多個角度分類,每一種分類都有可能變化,那麼就把這種多角度分離出來讓它們獨立變化,減少它們之間的耦合。
  也就是說,在發現我們需要多角度去分類實現對象,而只用繼承會造成大量的類增加,不能滿足開放-封閉原則時,就應該要考慮用橋接模式了。
  感覺只要真正深入地理解了設計原則,很多設計模式其實就是原則的應用而已,或許在不知不覺中就在使用設計模式了。
22.7 我要開發好的遊戲

 

第23章 烤羊肉串引來的思考——命令模式
23.1 吃烤羊肉串
  打游擊烤羊肉串和烤肉店哪個更賺錢。其實門店好過馬路游擊隊,還可以對應一個很重要的設計模式。
23.2 燒烤攤vs燒烤店
  燒烤攤吃烤串的人太多,都希望能最快吃到肉串,烤肉老闆一個人,所以有些混亂。人一多,他就未必記得信誰交過錢,要幾串,需不需要放辣等等。
  由於客戶和烤羊肉串老闆的緊耦合所以使得容易出錯,容易混亂,也容易挑剔。
  這其實就是行爲請求者與行爲實現者的緊耦合。我們需要記錄哪個人要幾串羊肉串,有沒有特殊要求(放辣不放辣),付沒付過錢,誰先誰後,這其實都相當於對請求做記錄,應該是做日誌。
  那麼如果有人需要退回請求,或者要求燒肉重烤,這其實就是相當於撤銷和重做。
  所以,對請求排隊或記錄請求日誌,以及支持可撤銷的操作等行爲時,行爲請求者與行爲實現者的緊耦合是不太適合的。
  其實,我們不用去認識燒肉者是誰,連他的面都不用見到,我們只需要給接待我們的服務員說我們要什麼就可以了。他可以記錄我們的請求,然後再由他去通知烤肉師傅做。
  而且,由於我們所做的請求,其實也就是我們點肉的訂單,上面有很詳細的我們的要求,所有的客戶都有這一份訂單,烤肉師傅可以按先後順序操作,不會混亂,也不會遺忘了。
  還有,如果想到更改訂單,只需通知服務員,然後服務員會在小本上劃一上,再去通知烤肉師傅了。這其實是在做撤銷行爲的操作。由於有了記錄,所以最終算賬還是不會錯的。這種就是利用一個服務員來解耦客戶和烤肉師傅的處理好處。
23.3 緊耦合設計
  路邊烤羊肉串的實現:class Barbecuer{void BackMutton(){//烤羊肉串}}
  客戶端調用:class Main(){boy = new Barbecure();//客戶端程序與烤肉串者緊耦合,儘管簡單,但卻極爲僵化,有許許多多的隱患}
  如果用戶多了,請求多了,就容易亂了。這裏需要增加服務員類。
  要知道,不管是烤羊肉串,還是烤雞翅,還是其他燒烤,這些都是烤肉串者類的行爲,也就是他的方法,具體怎麼做都是由方法內部來實現,我們不用管它。但是對於服務員類來說,他其實就是根據用戶的需要,發個命令,說:有人要十個羊肉串,有人要兩個雞翅,這些都是命令。
  意思是把烤肉串者類當中的方法,分別寫成多個命令類,那麼它們就可以被服務員來請求了。這些命令其實差不多都是同一個樣式,於是就可以泛化出一個抽象類,記服務員只管對抽象的命令發號施令就可以了。具體是什麼命令,即是烤什麼,由客戶來決定吧。
23.4 鬆耦合設計
  代碼結構圖:
  抽象命令類
  abstract class Command{Barbecure receiver;public Command(Barbecure receiver){this.receiver = receiver;}//抽象命令類,只需要確定烤肉串者是誰 abstract void ExcuteCommand();//執行命令}
  具體命令類
  烤羊肉串命令:class BakeMuttonCommand:Command{override void ExcuteCommand(){receiver.BakeMutton();//具體命令類,執行命令時,執行具體的行爲}}
  烤雞翅命令:class BakeChickenWingCommand:Command{override void ExcuteCommand(){receiver.BakeChickenWing();}}
  服務員類:class Waiter{Command command;//設置訂單void SetOrder(Command command){this.command = command;//服務員類,不用管用戶想要什麼烤肉,反正都是命令,只管記錄訂單,然後通知烤肉串者執行即可}//通知執行void Notify(){command.ExcuteCommand();}}
  客戶端實現:class Main{//開店前的準備 Barbecuer boy = new Barbecuer();其他略......}
  基本代碼已經實現,但有幾個問題,
  第一,真實的情況其實並不是用戶點一個菜,服務員就通知廚房去做一個,那樣不科學,應該是點完燒烤後,服務員一次通知製作;
  第二,如果此時雞翅沒了,不應該是客戶來判斷是否還有,客戶哪知道有沒有,應該是服務員或烤肉串者來否決這個請求;
  第三,客戶到底點了哪些燒烤或飲料,這是需要記錄日誌的,以備收費,也包括後期的統計;
  第四,客戶完全有可能因爲點的肉串太多而考慮取消一些還沒有製作的肉串。
  這些問題都需要得到解決。
23.5 鬆耦合後
  服務員類:
  class Waiter{orders = new List<Command>();//增加存放具體命令的容器;設置訂單void SetOrder(Command command){if("雞翅沒了"){syso.print("雞翅沒了,點別的燒烤");}else{orders.Add(command);syso.print("增加訂單+時間");//記錄客戶所點的燒烤的日誌,以備算賬收錢};//取消訂單void CancelOrder(Command command){orders.Remove(command)};//通知全部執行void Notify(){foreach(Command cmd in orders){cmd.ExcuteCommand();//根據用戶點好的燒烤訂單通知廚房製作}}}
  這就是大名鼎鼎的命令模式。
23.6 命令模式  
  命令模式(Command):將一個請求封裝爲一個對象,從而使你可用不同的請求對客戶進行參數化;對請求排隊或記錄請求日誌,以及支持可撤銷的操作。

  命令模式結構圖:
  Command類,用來聲明執行操作的接口。abstract class Command{Receiver receiver;Command(Receiver receiver){this.receiver = receiver;} abstract void Execute();}
  ConcreteCommand類,將一個接收者對象綁定於一個動作,調用接收者相應的操作,以實現Execute。class ConcreteCommand:Command{ConcreteCommand(Receiver receiver){base(receiver)};override void Execute(){receiver.Action();})}
  Invoker(服務員)類,要求該命令執行這個請求。class Invoker{Command command;void setCommand(Command command){this.command= command};void ExceutCommand(){command.Execute();}}
  Receiver(烤肉師傅)類,知道如何實施與計劃一個與請求相關的操作,任何類都可能作爲一個接收者。class Receiver{void Action(){//執行請求}}
  客戶端代碼,創建一個具體命令對象並設定它的接收者。class Main{r = new Receiver();c = new ConcreteCommand(r);i = new Invoker();i.SetCommand(c);i.ExceutCommand();}
23.7 命令模式作用
  總結命令模式的優點。
  第一,它能較容易地設計一個命令隊列;
  第二,在需要的情況下,可以較容易地將命令記入日誌;
  第三,允許接收請求的一方決定是否要否決請求;
  第四,可以容易地實現對請求的撤銷和重做;
  第五,由於加進新的具體命令類不影響其他的類,因此增加新的具體命令類很窠。
  最關鍵的優點就是命令模式把請求一個操作的對象與知道怎麼執行一個操作的對象分割開。
  是否是碰到類似情況就一定要實現命令模式呢?
  這就不一定,比如命令模式支持撤銷/恢復操作功能,但還不清楚是否需要這個功能時,要不要實現命令模式?
  其實應該是不要實現。敏捷開發原則告訴我們,不要爲代碼添加基於猜測的、實際不需要的功能。如果不清楚一個系統是否需要命令模式,一般就不要着急去實現它,事實上,在需要的時候通過重構實現這個模式並不困難,只有在真正需要如撤銷/恢復操作等功能時,把原來的代碼重構命令模式纔有意義。

 

第24章 加薪非要老總批?——職責鏈模式
24.1 老闆,我要加薪
  滿試用期馬上要辦轉正手續,提提加薪的事,向經理提出,經理去找人力資源總監,總監又去找總經理。
24.2 加薪代碼初步
  無論加薪還是請假,都是一種申請。申請就應該有申請類別、申請內容和申請數量。
  申請:class Request{string requestType;//申請類別 string requestContent;//申請內容 int number;//數量}
  然後經理、總監、總經理都是管理者。
  管理者:class Manager{string name;void GetResult(string managerLevel,Request request){if(managerLevel=='經理'){}else if(managerLevel=='總監'){}else if(managerLevel=='總經理'){}}}
  客戶端:class Main{jinli = new Manager('1');zongjian = new Manager('2');//兩個管理者 request=new Reqeust()//加薪請求;jinli.GetResult('經理',request)request=new Reqeust()//請假請求;jinli.GetResult('經理',request)}
  管理者類,裏面的結果方法比較長,加上有太多的分支判斷,其實是非常不好的設計。因爲如果再增加其他的管理類加,比如項目經理、部門經理等等。那就意味着都需要去更改這個類,這個類承擔了太多的責任,違背了單一職責原則,增加新的管理類別,需要修改這個類,違背了開放-封閉原則。
  需要重構,剛提到可能會增加管理類別,那就意味着這裏容易變化,可以把這些公司管理者的類別各做成管理者的子類,這就可以利用多態性來化解分支帶來的僵化。
  如何解決經理無權上報總監,總監無權再上報總經理這樣的功能呢?它們之間有一定的關聯,把用戶的請求傳遞,直到可以解決這個請求爲止。這裏引用一個行爲設計模式職責鏈模式。
24.3 職責鏈模式
  職責鏈模式(Chain of Responsibility):使多個對象都有機會處理請求,從而避免請求的發送者和接收者之間的耦合關係。將這個對象連成一條鏈,並沿着這條鏈傳遞該請求,直到有一個對象處理它爲止。
  這裏發出這個請求的客戶端並不知道這當中的哪一個對象最終處理這個請求,這樣系統的更改可以在不影響客戶端的情況下動態地重新組織和分配責任。
  職責鏈模式結構圖:
    Handle類,定義一個處理請求的接口。
 abstract class Handle{Handle successor;void setSuccessor(Handle successor){this.successor=successor;}//設置繼任者 abstract void HandleReqeust(int request);//處理請求的抽象方法}
 ConcreteHandler類,具體處理者類,處理它所負責的請求,可訪問它的後繼者,如果可處理該請求,就處理之,否則就將該請求轉發給它的後繼者。
 ConcreteHandler1,當請求數在0到10之間則有權處理,否則轉到下一位。
 class ConcreteHandler1:Handle{override HandleReqeust(int request){if(request>=0 && request <10){//處理請求}else if(successor != null){successor.HandleReqeust(request);//轉移到下一位}}}
 ConcreteHandler2,當請求數在10到20之間則有權處理,否則轉到下一位。
 class ConcreteHandler2:Handle{override HandleReqeust(int request){if(request>=10 && request <20){//處理請求}else if(successor != null){successor.HandleReqeust(request);//轉移到下一位}}}
 客戶端代碼,向鏈上的具體處理者對象提交請求。
 class Main{h1 = new ConcreteHandler1();h2 = new ConcreteHandler2();h1.SetSuccessor(h2);//設置職責鏈上家與下家 int[]reqeusts = {2,5,8,13,27};foreach(int request in reqeusts){h1.HandleReqeust(request);//循環給最小處理者提交請求,不同的數額由不由權限處理者處理}}
24.4 職責鏈的好處
  這當中最關鍵的是當客戶提交一個請求時,請求是沿鏈傳遞直至有一個ConcreteHandler對象負責處理它。
  這樣做的好處就是請求都不用管理哪個對象來處理,反正該請求會被處理就對了。
  這就使得接收者和發送者都沒有對方的明確信息,且鏈中的對象自己並不知道鏈的結構。結果是職責鏈可簡化對象的相互連接,它們僅需保持一個指向其後繼者的引用,而不需保持它所有的候選接受者的引用。這也就大大降低了耦合度了。
  由於是在客戶端來定義鏈的結構,也就是說,可以隨時地增加或修改處理一個請求的結構。增強了給對象指派職責的靈活性。
  這的確是很靈活,不過也要當心,一個請求極有可能到了鏈的末端都得不到處理,或者因爲沒有正確配置而得不到處理,需要事先考慮全面。這就跟現實中郵寄一封信,因地址不對,最終無法送達一樣。
  就剛纔的例子而言,最重要的有兩點,一個是你需要事先給每個具體管理者設置他的上司是哪個類,也就是設置後繼者另一點是你需要在每個具體管理者處理請求時,做出判斷,是可以處理這個請求,還是必須要推卸責任,轉移給後繼者去處理
  其實就是把現在寫的這個管理者類當中的那些分支,分解到每一個具體的管理者類當中,然後利用事先設置的後繼者來實現請求處理的權限問題。
24.5 加薪代碼重構
  先來改造這個管理者類,此時它將成爲抽象的父類,其實就是Handler;
  代碼結構圖:
    管理者:abstract class Manager{string name;Manager superior;//管理者上級 void SetSuperior(Manager superior){this.superior = superior;}關鍵的方法,設置管理者的上級 abstract void ReqeustApplications(Reqeust reqeusts);//申請請求}
 經理類就可以繼承這個管理者類,只需要重寫申請請求的方法就可以。
 經理:class CommonManager:Manager{override void ReqeustApplications(Reqeust reqeusts){if(request.RequestType=='請假'&&request.Number<=2){//被批准}else{if(superior != null){superior.ReqeustApplications(request);//其餘的申請都需要轉到上級}}}}
 總監類同樣繼承管理者類:略
 總經理的權限就全部都需要處理:略
 由於把原來的一個管理者類改成了一個抽象類和三個具體類,此時類之間的靈活性就大大增加了。如果我們需要擴展新的管理者類別,只需要增加子類就可以。

注:經常看到這樣的代碼,一連串類似的行爲,只是數據或者行爲不一樣。如一堆校驗器,如果成功怎麼樣、失敗怎麼樣;或者一堆對象構建器,各去構造一部分數據。碰到這種場景,我總是喜歡定義一個通用接口,入參是完整的要校驗/構造的參數,出參是成功/失敗的標示或者是void。然後有很多實現器分別實現這個接口,再用一個集合把這堆行爲串起來。最後,遍歷這個集合,串行或者並行的執行每一部分的邏輯。

這樣做的好處是:

很多通用的代碼可以在責任鏈原子對象的基類裏實現;

代碼清晰,開閉原則,每當有新的行爲產生的時候,只需要定義行的實現類並添加到集合裏即可;

爲並行提供了基礎。


24.6 加薪成功

 

第25章 世界需要和平——中介者模式
25.1 世界需要和平
  中介者模式又叫做調停者模式。其實就是中間人或者調停者的意思。
  由於各國之間代表的利益不同,所以矛盾衝突是難免的,但如果有這樣一個組織,由各國的代表組成,用來維護國際和平與安全,解決國際間經濟、社會、文化和人道主義性質的問題,這就是聯合國組織它就是一個調停者、中介者的角色。
  國與國之間的關係,就類似於不同的對象與對象之間的關係,這就要求對象之間需要知道其他所有對象,儘管將一個系統分割成許多對象通常可以增加可複用性,但是對象間相互連接的激增又會降低其可複用性了。爲什麼會這樣?
  因爲大量的連接使得一個對象不可能在沒有其他對象的支持下工作,系統表現爲一個不可分割的整體,所以,對系統的行爲進行任何較大的改動就十分困難了。
  要解決這樣的問題,需要應用之前講過的迪米特法則,如果兩個類不必彼此直接通信,那麼這兩個類就不應當發生直接的相互作用。如果其中一個類需要調用另一個類的某一個方法的話,可以通過第三者轉發這個調用。在這裏,意思就是說,國與國之間完全可以通過聯合國這個中介來發生關係,而不用直接通信。
  通過中介者對象,可以將系統的網狀結構變成以中介者爲中心的星形結構,每個具體對象不再通過直接的聯繫與另一個對象發生相互作用,而是通過中介者對象與另一個對象發生相互作用
  中介者對象的設計,使得系統的結構不會因爲新對象的引入造成大量的修改工作。
25.2 中介者模式
  中介者模式(Mediator):用一箇中介對象來封閉一系列的對象交互。中介者使各對象不需要顯式地相互引用,從而使其耦合鬆散,而且可以獨立地改變它們之間的交互。
  中介者模式結構圖:
    Colleague叫做抽象同事類;
 ConcreteColleague是具體同事類,每個具體同事只知道自己的行爲,而不瞭解其他同事類的接口,但它們卻都認識中介者對象。
 Mediator是抽象中介者,定義了同事對象到中介者對象的接口。
 ConcreteMediator是具體中介者對象,實現抽象類的方法,它需要知道所有具體同事類,並從具體同事接收消息,向具體同事對象發出命令。
 Mediator類抽象中介者類:abstract class Mediator{abstract void Send(string message,Colleague colleague);//定義一個抽象的發送消息方法,得到同事對象和發送消息}
 Colleague類抽象同事類:abstract class Colleague{Mediator mediaotr;Colleague(Mediator mediaotr){this.mediaotr=mediaotr;//構造方法,得到中介者對象}}
 ConcreteMediator類具體中介類:class ConcreteMediaotr:Mediator{ConcreteColleague1 Colleague1{set{colleague1 = vlaue;}ConcreteColleague2 Colleague2{set{colleague2=value;}}override void send(){if(colleague==colleague1){colleague2.Notify(message);}else{colleague1.Notify(message;)//重寫發送信息的方法,根據對象做出選擇判斷,通知對象}}}}
 ConcreteColleague1和ConcreteColleague2等各種同對象:class ConcreteColleague1:Colleague{//構造ConcreteColleague1(Mediator mediaotr){mediaotr;}void Send(string message){mediaotr.Send(message,this);//發送信息時通常是中介者發送出去的}void Notify(string message){syso.print('同事1得到消息'+message);}}
 客戶端調用:class Main{//略}
 由於有了Mediator,使得ConcreteColleague1和ConcreteColleague2在發送消息和接收信息時其實是通過中介者來完成的,這就減少了它們之間的耦合度了。
 這裏人個問題,美國和伊拉克都是國家,有一個國家抽象類和兩個具體國家類就可以了,但聯合國到底是Mediator還是ConcreteMediator呢?
 這要取決於未來是否有可能擴展中介者對象,比如覺得聯合國除了安理會,還有如國際勞工組織、教科文組織、世界衛生組織等等,所以Mediator應該是聯合國機構,而安理會是一個具體的中介者。
 如果不存在擴展情況,那麼Mediator可以與ConcreteMediator合二爲一。
25.3 安理會做中介
  聯合國機構類,相當於Mediagor類
  聯合國機構類:abstract class UnitedNations{abstract void Declare(string message,Country colleague);//聲明}
  國家類,相當於Colleague類
  國家類:abstrace class Country{UnitedNations mediaotr;Country(UnitedNations mediaotr){this.mediaotr = mediaotr;}}
  美國類,相當於ConcreteColleague1類
  美國類:class USA:Country{USA(UnitedNations mediaotr){base(mediaotr)};void Declare(string message){mediaotr.Declare(mess,this);//聲明}void GetMessage(string message){syso.print('美國獲取對方信息'+message);}}
  伊拉克類,相當於ConcreteColleague2類
  伊拉克類:class Iraq:Country{Iraq(UnitedNations mediaotr){base(mediaotr)};void Declare(string message){mediaotr.Declare(mess,this);//聲明}void GetMessage(string message){syso.print('伊位克獲取對方信息'+message);}}
  聯合國安理會,相當於ConcreteMediator類
  聯合國安理會:class UnitedNationsSecurityCouncil:UnitedNations{USA colleague1;Iraq colleague2;//美國set{colleague1=value;}//伊拉克set{colleague2=value;}//聯合國安理會了解所有的國家,所以擁有美國和伊拉克的對象屬性 override void Declare(string message,Country colleague){if(colleague==colleague1){colleague2.GetMessage(message);//重寫了聲明方法,實現了兩個對象間的通信}else{colleague1.GetMessage(message);}}}
  客戶端類:略
  有個問題需要思考,最關鍵的問題在於ConcreteMediator這個類必須要知道所有的ConcreteColleague,儘管這樣的設計可以減少了ConcreteColleague類之間的耦合,但這又使得ConcreteMediator責任太多了,如果出了問題,則整個系統都會有問題。
25.4 中介者模式優缺點
  是的,如果聯合國安理會出了問題,當然會對世界都造成影響。所以說,中介者模式很容易在系統中應用,也很容易在系統中誤用。當系統出現了多對多交互複雜的對象羣時,不要急於使用中介者模式,而要先反思你的系統在設計上是不是合理。總結下中介者模式的優缺點。
  中介者的優點:首先是Mediator的出現減少了各個Colleague的耦合,使得可以獨立地改變和複用各個Colleague類和Mediator,比如任何國家的改變不會影響到其他國家,而只是與安理會發生變化。其次,由於把對象如何協作進行了抽象,將中介作爲一個獨立的概念並將其封裝在一個對象中,這樣關注的對象就從對象各自本身的行爲轉移到它們之間的交互上來,也就是站在一個更宏觀的角度去看待系統。
  中介者的缺點:剛纔提到的,具體中介者類ConcreteMediator可能會因爲ConcreteColleague的越來越多,而變得非常複雜,反而不容易維護。由於ConcreteMediator控制了集中化,於是就把交互複雜性變爲了中介者的複雜性,這就使得中介者變得比任何一個ConcreteColleague都複雜。中介者模式的優點來自集中控制,其缺點也是它,使用時要考慮清楚。
  中介者模式應用於平時用.NET寫的Windows應用程序中的Form或Wdb網站程序的aspx就是典型的中介者。
  比如計算器程序,它上面有菜單控件、文本控件、多個按鈕控件和一個Form窗體,每個控件之間的通信都是通過Form窗體來完成的。因爲每個控件類代碼都被封裝了,所以它們的實例是不會知道其他控件對象的存在的,比如點擊數字按鈕要在文本框中顯示數字,按照以前的想法就應該要在Button類中編寫給TextBox類實例的Text屬性賦值的代碼,造成兩個類有耦合,這顯然是非常不合理的。但實際情況是它們都有事件機制,而事件的執行都是在Form窗體的代碼中完成,也就是說所有的控件的交互都是由Form窗體來作中介,操作各個對象,這就是典型的中介者模式應用。
  中介者模式一般應用於一組對象以定義良好但是複雜的方式進行通信的場合,比如剛纔得到的窗體Form對象或Web頁面aspx,以及想定製一個分佈在多個類中的行爲,而又不想生成太多的子類的場合。

 

第26章 項目多也別傻做——享元模式
26.1 項目多也別傻做
  私營業主做網站,有100家類似的商家客戶,要求基本上都是信息發佈、產品展示、博客留言、論壇功能等。難道去申請100個空間,用100個數據庫,然後用類似的代碼複製100遍?如果有Bug或是新的需求改動,維護量就太可怕了。
  現在的大型博客網站、電子商務網站,裏面每一個博客或商家也可以理解爲一個小的網站,但它們是如何做的?
  利用用戶ID號的不同,來區分不同的用戶,具體數據和模板可以不同,但代碼核心和數據庫卻是共享的。
  如何共享一份實例呢?
26.2 享元模式
  享元模式(Flyweight):運用共享技術有效地支持大量細粒度的對象。
  享元模式結構圖:
    Flyweight類,它是所有具體享元類的超類或接口,通過這個接口,Flyweight可以接受並作用於外部狀態。
   abstract class Flyweight{abstract void Operation(int extrinsicstate);}
 ConcreteFlyweight是繼承Flyweight超類或實現Flyweight接口,併爲內部狀態增加存儲空間。
   class ConcreteFlyweight:Flyweight{override void Operation(int extrinsicstate){syso.print("具體Flyweight:"+extrinsicstate);}}
 UnsharedConcreteFlyweight是指那些不需要共享的Flyweight子類。因爲Flyweight接口共享成爲可能,但它並不強制共享。
   class UnsharedConcreteFlyweight:Flyweight{override void Operation(int extrinsicstate){syso.print("不共享的具體Flyweight"+extrinsicstate);}}
 FlyweightFactory,是一個享元工廠,用來創建並管理Flyweight對象。它主要用來確保合理地共享Flyweight,當用戶請求一個Flyweight時,FlyweightFactory對象提供一個已創建的實例或者創建一個(如果不存在的話)。
   class FlyweightFactory{Hashtable flyweights = new Hashtable();FlyweightFactory(){flyweights.Add("X",new ConcreteFlyweight());flyweights.Add("Y",new ConcreteFlyweight();)}//根據客戶端請求,獲得已生成實例Flyweight GetFlyweight(String key){return ((Flyweight)flyweights[key];)}}
  客戶端代碼:
    class Main{int extrinsicstate =22;//代碼外部狀態 f = new FlyweightFactory();fx = f.GetFlyweight("x");fx.Operation(--extrinsicstate);fy.Operation(--extrinsicstate);uf = new UnsharedConcreteFlyweight();uf.Operation(--extrinsicstate);}

有個問題,FlyweightFactory根據客戶需要返回早已生成好的對象,但一定要事先生成對象實例嗎?
  實際上是不一定需要的,完全可以初始化時什麼也不做,到需要時,再去判斷對象是否爲null來決定是否實例化。
  還有個問題,爲什麼要有UnsharedConcreteFlyweight的存在呢?
  這是因爲儘管我們大剖分時間都需要共享對象來降低內存的損耗,但個別時候也有可能不需要共享的,那麼此時的UnsharedConcreteFlyweight子類就有存在的必要了,它可以解決那些不需要共享對象的問題。
26.3  網站共享代碼
  網站抽象類:abstract class WebSite{abstract void Use();}
  具體網站類:class ConcreteWebSite:WebSite{ConcreteWebSite(String name){this.name=name;} override void Use(){sys.print("網站分類""+name)}}
  網站工廠類:class WebSiteFactory{flyweights = new Hahstable();//獲得網站分類,判斷是否存在這個對象,如果存在,則直接返回,若不存在,則實例化它再返回WebSite GetWebSiteCategory(string key){if(!flyweights.Containskey(key)flyweights.Add(key,new ConcreteWebSite(key));return ((WebSite)flyweights[key]);)}//獲取網站分類總數,得到實例的個數int GetWebSiteCount(){return flyweights.Count();}}
  客戶端代碼:class Main{f = new WebSiteFactory();//實例化產品展示的網站對象WebSite fx = f.GetWebSiteCategory("產品展示");fx.Use();//共享上方生成的對象,不再實例化WebSite fy = f.GetWebSiteCategory("產品展示");fy.Use();//博客等其它分類以此爲例}}
  這樣寫算是基本實現了享元模式的共享對象的目的,也就是說,不管建幾個網站,只要是產品展示,都是一樣的,只要是博客,也是完全相同的,但這樣是有問題的,你給企業建的網站不是一家企業的,它們的數據不會相同,所以至少它們都應該有不同的賬號,那該怎麼辦?
  實際上這樣寫沒有體現對象間的不同,只體現了它們共享的部分。
26.4  內部狀態與外部狀態
  在享元對象內部並且不會隨環境改變而改變的共享部分,可以稱爲是享元對象的內部狀態,而隨環境改變而改變的、不可以共享的狀態就是外部狀態了。
  事實上,享元模式可以避免大量非常相似類的開銷。在程序設計中,有時需要生成大量細粒度的類實例來表示數據。如果能發現這些實例除了幾個參數外基本上都是相同的,有時就能夠受大幅度地減少需要實例化的類的數量。如果能把那些參數移到類實例的外面,在方法調用時將它們傳遞過來,就可以通過共享大幅度地減少單個實例的數目。
  也就是說,享元模式Flyweight執行時所需的狀態是內部的也可能是外部的,內部狀態存儲於ConcreteFlaweight對象之中,而外部對象則應該考慮由客戶端對象存儲或計算,當調用Flyweight對象的操作時,將該狀態傳遞給它。
  例如,客戶的賬號就是外部狀態,應該由專門的對象來處理。
  代碼結構圖:
    用戶類,用於網站的客戶賬號,是網站類的外部狀態:class User{string name;//提供get-set}
 網站抽象類:abstract class WebSite{abstract void Use(User user);//使用方法需要傳遞用戶對象}
 具體網站類:略
 網站工廠類:略
 客戶端代碼:class Main{f = new WebSiteFactory();WebSite fx = f.GetWebSiteCategory("產品展示"");fx.Use(new User("嬌嬌");fy = f.GetWebSiteCategory("產品展示");fy.Use(new User("老童"));//博客略}
 儘管有不同用戶使用網站,但實際上只有兩個網站實例。這樣就可以協調內部與外部狀態了。
 由於用了享元模式,哪怕你接手了100個網站的需求,只要要求相同或類似,你的實際開發代碼也就是分類的那幾種,對於服務器來說,佔用硬盤空間、內存、CPU資源都是非常少的,這確實是很好的一個方式。
26.5 享元模式應用
  現實中什麼時候才應該考慮使用享元模式呢?
  如果一個應用程序使用了大量的對象,而大量的這些對象造成了很大的存儲開銷時就應該考慮使用;還有就是對象的大多數狀態可以外部狀態,如果刪除對象的外部狀態,那麼可以用相對較少的共享對象取代很多組對象,此時可以考慮使用享元模式。
  在實際使用中,享元模式到底能達到什麼效果呢?
  因爲用了享元模式,所以有了共享對象,實例總數就大大減少了,如果共享的對象越多,存儲節約也就越多,節約量隨着共享狀態的增多而增大。
  有些什麼具體是用到享元模式的?
  實際上在.NET中(包括java),字符串string就是運用了Flyweight模式。舉個例子。Object.equals(object objA,object objB)方法是用來確定objA與objB是否相同的實例,返回值爲bool值。
  string titleA = "大話設計模式";string titleB = "大話設計模式";syso.print(Object.equals(titleA,titleB));返回值是true,這兩個字符串是相同的實例。
  如果每次創建字符串對象時,都需要創建一個新的字符串對象的話,內存的開銷會很大。所以如果第一次創建了字符串對象titleA,下次再創建相同的字符串titleB時只是把它的引用指向大話設計模式,這樣就實現了大話設計模式在內存中的共享。
  雖說享元模式更多的時候是一種底層的設計模式,但現實中也是有應用的。比如說休閒遊戲開發中,像圍棋、五子棋、跳棋等,它們都有大理的棋子對象,分析下,它們的內部狀態和外部狀態各是什麼?
  圍棋和五子棋只有黑白兩色、跳棋顏色略多一些,但也是不太變化的,所以顏色應該是棋子的內部狀態,而各個棋子之間的差別主要就是位置的不同,所以方位座標應該是棋子的外部狀態。
  在某些情況下,對象的數量可能會太多,從而導致了運行時的資源與性能損耗。那麼我們如何去避免大量細粒度的對象,同時又不影響客戶程序,是一個值得去思考的問題,享元模式,可以運用共享技術有效地支持大量細粒度的對象。不過,使用享元模式需要維護一個記錄了系統已有的所有享元的列表,而這本身需要耗費資源,另外享元模式使得系統更加複雜。爲了使用對象可以共享,需要將一些狀態外部化,這使得程序的邏輯複雜化。因此,應當在有足夠的對象實例可供共享時才值得使用享元模式。


第27章  其實你不懂老闆的心——解釋器模式
  27.1 其實你不懂老闆的心
    老闆私下對某員工大加誇獎時,多半是最近有更多的任務需要你去完成的意思。
  通常老闆說某個員工是普通員工,其實他的意思是說,這個員工不夠聰明,工作能力不足。
  要是有一個翻譯機,或解釋器就好了,省得每次講話還需要多動腦筋。
  27.2 解釋器模式
    解釋器模式(interpreter):給定一個語言,定義它的文法(語法)的一種表示,並定義一個解釋器,這個解釋器使用該表示來解釋語言中的句子。
 解釋器模式需要解決的是,如果一種特定類型的問題發生的頻率足夠高,那麼可能就值得將該問題的各個實例表述爲一個簡單語言中的句子。這樣就可以構建一個解釋器,該解釋器通過解釋這些句子來解決該問題。比方說,我們常常會在字符串中搜索匹配的字符或判斷一個字符串是否符合我們規定的格式,此時一般我們會用正則表達式。
 其實像IE、Firefox這些瀏覽器,其實也是在解釋HTML文法,將下載到客戶端的HTML標記文本轉換成網頁格式顯示到用戶,不過編寫一個瀏覽器的程序,當然要複雜很多。
    解釋器模式結構圖:
   AbstractExpression(抽象表達式),聲明一個抽象的解釋操作,這個接口爲抽象語法樹中所有的節點所共享:
    abstract class AbstractExpression{abstract void Interpret(Context context);}
   TerminalExpression(終結符表達式),實現與文法中的終結符相關聯的解釋操作。實現抽象表達式中所要求的接口,主要是一個interpret()方法。文法中每一個終結符都有一個具體終結表達式與之相應。
    class TerminalExpression:AbstractExpression{override void Interpret(Context context){syso.print("終端解釋器")}}
   NoterminalExpression(非終結符表達式),爲文法中的非終結符實現解釋操作。對文法中每一條規則R1、R2...Rn都需要一個具體的非終結符表達式類。通過實現抽象表達式的interpret()方法實現解釋操作。解釋操作以遞歸方式調用上面所提到的代表R1、R2...Rn中各個符號的實例變量。
    class NoterminalExpression:AbstractExpression{override void Interpret(Context context){syso.print("非終端解釋器");}}
   Context,包含解釋器之外的一些全局信息
    class Context{string input;//提供get-set string output;//提供get-set}
   客戶端代碼,構建表示該文法(語法)定義的語言中一個特定的句子的抽象語法樹。調用解釋操作。
    class Main{context = new Context();list = new List<AbstractExpression>();list.Add(new TerminalExpression());list.Add(new NoterminalExpression();)for(AbstractExpression exp in list){exp.Interpret(context)}}
  27.3 解釋器模式好處
    用解釋器模式,就如同你開發了一個編程語言或腳本給自己或別人用。
 解釋器模式就是用迷你語言來表現程序要解決的問題,以迷你語言寫成迷你程序來表現具體的問題。
 通常當有一個語言需要來解釋執行,並且你可將該語言中的句子表示爲一個抽象語法樹時,可使用解釋器模式。
 解釋器模式有什麼好處呢?
 用瞭解釋器模式,就意味着可以很容易地改變和擴展文法,因爲該模式使用類來表示文法規則,你可使用繼承來改變或擴展該文法。也比較容易實現文法,因爲定義抽象語法樹中各個節點的類的實現大體類似,這些類都易於直接編寫。
 解釋器模式就是將一句話,轉變成實際的命令程序執行而已。而不用解釋器模式本來也可以分析,但通過繼承抽象表達式的方式,由於依賴倒轉原則,使得對文法的擴展和維護都帶來了方便。
 解釋器模式也有不足的,解釋器模式爲文法中的每一條規則至少定義了一個類,因此包含許多規則的文法可能難以管理和維護。建議當文法非常複雜時,使用其他的技術如語法分析程序或編譯器生成器來處理。
  27.4 音樂解釋器
    以前用的手機裏就有編輯鈴聲的功能,通過輸入一些簡單的字母數字,就可以讓手機發出音樂。用QB或者手機說明書中定義的規則去編寫音樂程序,就是一段文法讓QB或手機去翻譯成具體的指令來執行。
  27.5 音樂解釋器實現
    代碼結構圖:
 演奏內容類(context):class PlayContext{//演奏文本string text;//提供get-set}
 表達式類(AbstractExpression):abstract class Expression{//解釋器void Interpret(PlayContext context){//略} abstract void Excute(string key,double value);//抽象方法執行,不同的文法子類,有不同的執行處理}
 音符類(TerminalExpression):class Note:Expression{override void Excute(string key,double value){string note="";switch(key){case "C":note=1;//表示如果獲得的key是C則演奏1(do),如果是D則演奏2(Re)}}}
 音符類(TerminalExpression):class Scale:Expression{override void Excute(string key,double value){string scale="";switch(value){case 1:scale="低音";//表示如果獲得的key是O並且value是1則演奏低音,2則是中音,3則是高音}}}
 客戶端代碼:class Main{//略}
 現在需要增加一個文法,就是演奏速度,要求是T代表速度,以毫秒爲單位,T1000表示每節拍一秒,T500表示每節拍半秒。
 首先加一個表達式的子類叫音速。然後在客戶端的分支判斷中增加一個case分支就可以了。
 音速類:class Speed:Expression{override void Excute(string key,double value){string speed;if(value < 500){speed="快速"}}}
 其實這個例子是不能代表解釋器模式的全貌的,因爲它只有終結符表達式,而沒非終結符表達式的子類,因爲如果想真正理解解釋器模式,還需要去研究其他的例子。另外這是個控制檯的程序,如果給出鋼琴所有按鍵的聲音文件,MP3格式,可利用Media Player控件,寫出真實的音樂語言解釋器。只要按照簡譜編好這樣的語句,就可以讓電腦模擬鋼琴彈奏出來。
  27.6 料事如神

 

第28章 男人和女人——訪問者模式
  28.1 男人和女人
    訪問者模式講的是表示一個作用於某對象結構中的各元素的操作。它使你可以在不改變各元素的類的前提下定義作用於這些元素的新操作。
 男女對比這麼多的原因主要就是因爲人類在性別上就只有男人和女人兩類。人類只分爲男人和女人,所以纔會有這麼多的對比。
  28.2 最簡單的編程實現
    class Main{syso.print("男人成功時,背後多半有一個偉大的女人");syso.print("女人成功時,背後多半有一個不成功的男人");//其它對比略}
  28.3 簡單的面向對象實現
    人類是男人和女人的抽象類。
 abstract class Person{string action;//提供get-set abstract void GetConlusion();//得到結論或反應}
 男人類:class Man:Person{//得到結論或反應 override void GetConlusion(){if(action=="成功"){//背後多半有一個偉大的女人}};}
 女人類:class Woman:Person{//得到結論或反應 override void GetConlusion(){if(action=="成功"){//背後多半有一個不成功的男人}};}
 客戶端:class Main{persons = new List<Person>;man1 = new Man();man1.Action="成功";persons.Add(man1);//其它略}
 以上代碼還是有問題的,在男人類與女人類當中的那些if...else...很是礙眼,如果現在要增加一個結婚的狀態,那這兩個類都需要增加分支判斷了。如果把這些狀態寫類,那又如何處理呢?
  28.4 用了模式的實現
    狀態抽象類:
   abstract class Action{//得到男人結論或反應 abstract void GetManConclusion(Man concreteElementA);//得到女人結論或反應 abstract void GetWomanConclusion(Woman concreteElementB);}
 人的抽象類:
   abstract class Person{//接受 abstract void Accept(Action visitor//它是用來獲得狀態對象的);}
 這裏關鍵就在於人就只分爲男人和女人,這個性別的分類是穩定的,所以可以在狀態類中,增加男人反應和女人反應兩個方法,方法個數是穩定的,不會很容易的發生變化。而人抽象類中有一個抽象方法接受,它是用來獲得狀態對象的。每一種具體狀態都繼承狀態抽象類,實現兩個反應的方法。
 具體狀態類:
  成功狀態類:class Success:Action{override void GetManConclusion(Man man1){syso.print("男人成功時....");}override void GetWomanConclusion(Woman woman1){syso.print("女人成功時......")}}
  失敗狀態類:class Failing:Action{//與上面成功狀態類同,省略}
  戀愛狀態類:class Amativeness:Action{//與上面成功狀態類同,省略};
 男人和女人類
   男人類:class Man:person{override void Accept(Action visitor){visitor.GetManConclusion(this);}}
   女人類:class Woman:person{override void Accept(Action visitor){visitor.GetWomanConclusion(this);}}
 這裏需要提一下當中用到到一種分派的技術,首先在客戶程序中將具體狀態作爲參數傳遞給男人類完成了一次分派,然後男人類調用作爲參數的具體狀態中的方法男人反應,同時將自己(this)作爲參數傳遞進去。這便完成了第二次分派。
 雙分派意味着得到執行的操作決定於請求的種類和兩個接收者的類型。接受方法就是一個雙分派的操作。它得到執行的操作不僅決定於狀態類的具體狀態,還決定於它訪問的人的類別。
 對象結構類由於總是需要男人與女人在不同狀態的對比,所以我們需要一個對象結構類來針對不同的狀態遍歷男人與女人,得到不同的適應。
 對象結構類:class ObjectStructure{elements = new List<Person>();//增加 void Attach(Prrson element){elements.Add(element);}//移除 void Detach(Person element){elements.Remove(element);//查看顯示 void Display(Action visitor){foreach(person p in elements){e.Accept(visitor);//遍歷方法}}}}
 客戶端類:class Main{o = new ObjectStructure();o.Attach(new Man());o.Attach(new Woman());//在對象結構中加入要對比的男人和女人;//成功時的反應v1 = new Success();o.Display(v1);//查看在各種狀態下,男人和女人的反應;//失敗時的反應 v2 = new Failing();o.Display(v2);//其它略}
 這樣做到底有什麼好處呢?
 如果我們現在要增加結婚的狀態來考查男人和女人的反應,由於用了雙分派,使得只需要增加一個狀態子類,就可以在客戶端調用來查看,不需要改動其他任何類的代碼。
 這下完美的體現了開放-封閉原則,這種模式叫做和訪問者模式。
  28.5 訪問者模式
    訪問者模式(Visitor):表示一個作用於某對象結構中的各元素的操作。它使你可以在不改變各元素的前提下定義作用於這些元素的新操作。
 訪問者模式結構圖:
   爲該對象結構中ConcreteElement的每一個類聲明一個Visit操作:
   abstract class Visitor{abstract void VisitConcreteElementA(ConcreteElementA a);bstract void VisitConcreteElementB(ConcreteElementB b);}
   具體訪問者,實現每個由Visitor聲明的操作。每個操作實現算法的一部分,而該算法片斷乃是對應於結構中對象的類:
   class ConcreteVisitor1{void VisitConcreteElementA(ConcreteElementA a){};void VisitConcreteElementB(ConcreteElementB b){}};
   class ConcreteVisitor2{void VisitConcreteElementA(ConcreteElementA a){};void VisitConcreteElementB(ConcreteElementB b){}};
   定義一個Accept操作,它以一個訪問者爲參數:
   abstract class Element{void Accept(Visitor visitor);};
   具體元素,實現Accept操作
   class ConcreteElementA{void Accept(Visitor visitor){};void OperatorA(){}};
   class ConcreteElementB{void Accept(Visitor visitor){};void OperatorB(){}};
   對象結構類,能枚舉它的元素,可以提供一個高層的接口以允許訪問者訪問它的元素
   class ObjectStructure{}
 在這裏,Element就是我們的人類,而ConcreteElementA和ConcreteElementB就是男人和女人,Visitor就是我們寫的狀態類,具體ConcreteVisitor就是那些成功、失敗、戀愛等等狀態。至於ObjectStructure就是對象結構類了。
 男女對比這麼多的原因主要就是因爲人類在性別上就只有男人和女人兩類。而這也正是訪問者模式可以實施的前提。
 這個前提是什麼呢?
 如果人類的性別不止是男和女,而是可有多種性別,那就意味狀態類中的抽象方法就不可能穩定了,每加一種類別,就需要在狀態類和它的所有下屬類中都增加一個方法,這就不符合開放-封閉原則了。
 也就是說,訪問者模式適用於數據結構相對穩定的系統,它把數據結構和作用於結構上的操作之間的耦合解脫開,使得操作集合可以相對自由地演化。
 訪問者模式的目的是什麼?
 訪問者模式的目的是要把處理從數據結構分離出來。很多系統可以按照算法和數據結構分開,如果這樣的系統有比較穩定的數據結構,又有易於變化的算法的話,使用訪問者模式就是比較合適的,因爲訪問者模式使得算法操作的增加變得容易。反之,如果這樣的系統的數據結構對象易於變化,經常要有新的數據對象增加進來,就不適合使用訪問者模式。
 其實訪問者模式的優點就是增加新的操作很容易,因爲增加新的操作就意味着增加一個新的訪問者。訪問者模式將有關的行爲集中到一個訪問對者對象中。
 通常ConcreteVisitor可以單獨開發,不必跟ConcreteElementA或ConcreteElementB寫在一起。正因爲這樣,ConcreteVisitor能提高ConcreteElement之間的獨立性,如果把一個處理動作設計成ConcreteElementA和ConcreteElementB類的方法,每次想新增處理以擴充功能時就得去修改ConcreteElementA和ConcreteElementB了。這也就是之前寫的代碼,在男人和女人類中加了對成功、失敗等狀態的判斷,造成處理方法和數據結構的緊耦合。
 訪問者的缺點其實也就是使用增加新的數據結構變得困難了。
 Gof四人中的一個作者就說過:大多數時候你並不需要訪問者模式,但當一旦你需要訪問者模式時,那就是真的需要它了。事實上,我們很難找到數據結構不變化的情況,所以用訪問者模式的機會也就不太多了。這也就是爲什麼談到男人女人對比時討論的原因,因爲人類性別這樣的數據結構是不會變化的。
  28.6 訪問者模式基本代碼
    Visitor類,爲該對象結構中ConcreteElement的每一個類聲明一個Visit操作。
 abstract class Visitor{abstract void VisitConcreteElementA{ConcreteElementA concreteElementA};abstract void VisitConcreteElementB{ConcreteElementB concreteElementB};}
 ConcreteVisitor1和ConcreteVisitor2類,具體訪問類,實現每個由Visitor聲明的操作。每個操作實現算法的一部分,而該算法片斷乃是對應於結構中對象的類。
 class ConcreteVisitor1:Visitor{override void VisitConcreteElementA(ConcreteElementA ca){syso.print(ca.GetType().Name被this.GetType.Name訪問);override void VisitConcreteElementB(ConcreteElementB cb){syso.print(cb.GetType.Name被this.GetType.Name訪問);}}}
 class ConcreteVisitor2:Visitor{//代碼與上類類似,省略}
 Element類,定義一個Accept操作,它以一個訪問者爲參數
 abstract class Element{abstract void Accept(Visitor visitor);}
 ConcreteElementA和ConcreteElementB類,具體元素,實現Accept操作。
 class ConcreteElementA:Element{override void Accept(Visitor visitor){visitor.VisitConcreteElementA(this);//充分利用雙分派技術,實現處理與數據結構的分離};void OperatorA(){};//其他的相關方法}
 class ConcreteElementB:Element{//代碼與上類類似,省略}
 ObjectStructure類,能枚舉它的元素,可以提供一個高層的接口允許訪問者訪問它的元素
 class ObjectStructure{elements = new List<Element>();void Attach(Element element){elements.Add(element);}void Detach(Element element){elements.Remove(element);}void Accept(Visitor visitor){for(Element e in elements){e.Accept(visito);}}}
 客戶端代碼:
 class Main{ObjectStructure o = new ObjectStructure();o.Attach(new ConcreteElementA());o.Attach(new ConcreteElementB());ConcreteVisitor1 v1 = new ConcreteVisitor1();ConcreteVisitor2 v2 = new ConcreteVisitor2();o.Accept(v1);o.Accept(v2);}
  28.7 比上不足,比下有餘
    訪問者模式的能力和複雜性是把雙刃劍,只有當你真正需要它的時候,才考慮使用它。有很多的程序員爲民展示自己的面向對象的能力或是沉迷於模式當中,往往會誤用這個模式,所以一定要好好理解它的適用性。

 

總結:最常用的設計模式 1、工廠方法模式  2、外觀模式 3、觀察者模式 4、策略模式 5、適配器模式

 開發程序步驟總結(個人觀點):
  1、確定開發程序使用的語言
  2、瞭解開發語言的特性
  3、以使用java爲例:java語言的特性是面向對象
  4、既然是面向對象那麼就要知道面向對象的特性
  5、面向對象的三大特性:封裝、繼承、多態
  6、瞭解這些特性的使用,其主要作用是什麼
  7、寫程序之前加入設計模式思想
  8、使用設計模式之前需要了解設計模式的四大原則,以保證設計程序的架構。
  9、設計模式的四大原則:單一職責原則(減少類的職責)、開放-封閉原則(對擴展開放-對修改關閉)、依賴倒轉原則(針對接口編程,不要對實現編程)、迪米特原則(強調了類之間的鬆耦合)
  10、開始架構

 

23種設計模式代碼見附件:----------------

                                                              -

                                                              -

                                                              -

                                                              -

                                                              -

                                                              !

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