閱讀340 返回首頁    go 小米 go 小米6


架構那點事係列一 - 設計模式前章

                                        ----能夠正視失敗和錯誤,而不回避和掩蓋,正是職業架構師的寶貴素養!!!

      首先,我們需要明白的是何為架構,以我看來,架構更多關注的是高層設計(所謂的high-level Design),它是一組有關如何確定軟件係統的組織機構的重要決策。感興趣的朋友可以查看這篇文章來深入理解一下架構的概念(https://baike.baidu.com/view/1147116.htm?fr=ala0_1)。那麼,如何達到傳說中的“架構之美”呢?我把它歸為:軟知識+想象力+創造力。軟知識的積累過程中,必然少不了設計模式的學習。自己寫這篇博文的目的,是希望和大家一同分享設計模式的美感,進行有效地模式挖掘。同時,在開源軟件的今天,建造屬於自己的輪子(我想這就是想象力加創造力最完美的結合吧) 

       正如題目所言,這篇博文是設計模式學習的前章。作為引子,我們必須精通OO理念,也為後續設計模式的學習,打下良好的基礎。 

       我們都知道,麵向對象設計的5大原則是指開閉原則(Open-Close Principle),單一職責原則(Single-Responsibility Principle),接口隔離原則(Interface Isolation Principle),裏氏替換原則(Liskov Substitution Principle),依賴倒置原則(Dependence Inversion Principle),可是,我們當中又有多少人真正能領會到這5大原則之間不是相互獨立的呢?真正領會到:其中的一個原則是另一個原則的基礎或是加強。違反其中的任何一個原則,都有可能同時違反了其餘的原則。而把麵向對象設計理念運用到極致的就是我們所熟悉的GOF的設計模式,它是將OO思想運用到具體的軟件編程開發實踐中的最佳範例。下麵,隨我一起,走入OO世界吧。

 
       讓我們首先看看開閉原則的一些基本概念,而後,我們會看到,在真實的項目中,我對它的理解與應用。隨後的幾大原則,我也打算用這種模式去敘述,同時將自己的感悟寫下來,為了不誤人子弟,希望大家帶著批判的思維去審視我的設計。畢竟,敢於懷疑,才能獲得真正的進步嘛,而我也很是需要這樣的朋友。


       開閉原則(Open for extension. Closed for modification)中的Open,具體含義是:軟件實體應該是可擴展的。Close則告訴我們,當變化來臨時,我們應該避免對其進行修改。恩,避免對其修改?那怎麼能麵對變化,對其進行擴展呢,嗬嗬,這就是思想最為精妙之處。細細品來,它是說:軟件設計人員應該擁抱變化,封裝變化。不用修改原有實體(之所以我用實體來表述這個概念,是因為我覺得這個詞的抽象層次很高,它包括小到類,大到模塊等概念)就能對其進行擴展。具體點就是應用此原則,我們可以在不用修改代碼(源代碼,二進製可執行代碼等)的前提下,對現有係統進行擴展和升級。想想看,在現實世界中,這樣的例子真的又有多少呢?比方說,我們最為熟悉的機頂盒,使用它,我們可以將模擬電視擴展為數字電視,而不用對電視機內部結構進行調整,這就意味著,你大可不必去購物商場對你的視覺進行二次投資,省錢省心。再比如說,計算機主板上的內存擴展槽,嗬嗬,這個就不用我囉嗦了吧。總之,開閉原則其實就是對“可變性”的封裝,遵守該原則,一方麵要在軟件係統中找出各種可能的“可變因素”,將它們封裝起來;另一方麵,在具體的編程實現中,一種可變性因素不應當散落在多個不同代碼模塊中,我們應當將它們封裝到一個對象裏。好,下麵,我們看看,為了能夠滿足或者說遵守此原則,作為第一線的我們,又會采取什麼措施呢?恩,在這裏,我們通常采用麵向接口編程或者是利用模板方法。


       麵向接口編程,其實也遵從了依賴倒置原則。由於接口是相對穩定的,當需求發生變化時,可以提供該接口的新的實現類,以求適應變化。由於接口沒有變化,所以依賴於該接口的客戶端代碼不需要被動的發生變化。這裏麵的設計技巧遍布Java Web的三大框架,最為經典是的Spring的Inversion of Control了,在更為一般的IOC設計理念中,通常采用Dependency Injection,或者是Service Locator,而Spring運用了前者,即我們所熟知的依賴注入(保留抽象的接口,讓組件依賴於抽象接口,當組件要與其它實際的對象發生依賴關係時,可以通過抽象接口注入其所依賴的實際對象)。 

   

       利用模板方法模式編程。其實呢,模板方法模式中的模板類是一個抽象類。由於模板方法模式依賴於一個固定的模板類,因此它對於修改是關閉的。同時,通過從這個抽象類派生,也可以對此模板類的行為進行擴展。

 
       好,為了省事期間,我就沒有附上諸多實例代碼,畢竟,對於大多數人來說,如果有代碼配合,可能理解的會更深。送走了第一個準則,我們迎來了單一職責。恩,單一職責,有點味道,這不是要求我們不要越俎代庖嗎?嗬嗬,是啊,道理很相近。所謂SRP,它要求應用係統中的一個具體的類隻完成某一類功能,並且盡可能避免出現一個“胖”(在一個類中完成多個不同的功能)功能的類。按照此原則設計應用係統中的類時,對於某個具體的類,應該僅有一個引起它變化的原因。很顯然,如果我們嚴格遵守單一職責原則,那麼就可以避免相同的職責分散到不同的類中,同時還可以避免一個類承擔過多的職責。(注意:單一職責原則揭示了係統設計中“內聚性”和“耦合性”之間的正反關係。如何做到恰到好處的內聚,避免軟件實體之間過高的耦合性,單一職責原則給出了我們最佳答案。嗬嗬,在工程實踐中,這也取決於需求和軟件設計師的經驗)


       在應用係統的持久層設計中,我們通常將其拆分為數據實體類,數據訪問邏輯,數據連接邏輯(通常情況下,我們使用XML文件或者是屬性文件進行配置,好處自不待言)。為什麼要這樣設計呢?嗬嗬,我想每個編程老手都經曆過一個蛻變的過程,在早期,它們都寫過這樣的代碼:一個類文件,包含了對數據庫操作的全部語句,比方說,數據庫連接,數據庫CRUD操作,數據庫連接釋放等。記得,在剛開始接觸JDBC編程時,自己曾有過做萬能連接的想法(主要是方便自己手動的配置數據庫連接而已!),嗬嗬,當時也實現了部分數據庫的部分操作(MS SQL 2000,Mysql,Access等)。簡簡單單一個類,可讀性也不差,可擴展性,可維護性也都還好(盡管如此,但我還是不推薦這樣的做法,要知道,多花點時間在前期的設計上,這樣的投資絕對值,它會給是你的後期維護變得不那麼舉步維艱!)。嗬嗬,可是後來發現很多持久層框架,都已經為我們做了這些工作,不僅如此,它們通常還提供數據庫連接池功能,並且使用了配置文件。其實背後都蘊藏著單一職責的設計原則,隻是當時才疏學淺,未能吃透。嗬嗬,慢慢來嘛,大師曾經都是小工的!值得我們注意的時,單一職責原則在模板方法模式中的具體應用主要體現在其模板類的設計上,通常它定義出所有子類的共同行為,然後由它的各個子類單獨完成各自的行為。模板方法模式解決問題的思路為“通用的功能我來實現,特殊的需求交由你處理”!


       利用麵向對象技術中的繼承機製,能夠減少代碼的重複編程實現,從而實現係統中的代碼複用。如何合理而正確地進行繼承,Liskov替換原則給出了我們良好的指導。在進行派生類的設計時,我們要保證它能與基類相容。具體說,就是基類的抽象方法都要在其子類中實現,並且一個具體的派生類隻實現其抽象類中聲明的方法,而不應當給出多餘的方法定義或實現,這樣可以實現動態綁定。(經驗之談:隻有派生類完全替換基類,才能保證係統在運行期動態進行類的類型識別,也就是可以實現動態多態形式的調用)。


       再囉嗦幾句吧!由於麵向對象編程技術中的繼承在具體的編程中過於簡單,在許多係統的設計和編程實現中,我們並沒有認真地,理性地思考應用係統中各個類之間的繼承關係是否合適,派生類是否能正確地對其基類中的某些方法進行重寫等問題。因此經常出現濫用繼承或者錯誤的進行了繼承等現象,給係統的後期維護帶來難以估計的麻煩。所以我們應當在自己的編程中,不斷思考,不斷提煉,這樣才能做出真正的OO產品來。

 
       好了,讓我們看看什麼是接口隔離原則吧!其主要思想為:一個類對另一個類的依賴關係應當建立在最小接口上,使用多個專門的接口比使用單一複合總接口要優越。可以看出,接口隔離原則其實是單一職責應用於接口設計上的細化準側。那麼,我們如何做,才能更好地遵守此原則呢?我覺得,應當將完成一類相關功能的各個方法放在同一個接口中,形成高內聚的職責。大接口可以根據功能分類分割成若幹個不同的小接口,具體可以應用接口的多重繼承來分離接口(可以看看Spring框架的設計大師們是怎麼做的)。


      傳統麵向過程的軟件係統設計方法是“自頂向下,逐步細分”的,這種設計方法的核心思想是將一個大的業務功能分解為一些小的功能。從具體的實現來看,設計者往往用一個main函數概括出整個應用程序需要完成的各個功能模塊(子函數),各個功能模塊的實現又可以細化為更小的子函數。函數之間通過調用關係來實現係統的整體功能。但是這樣的設計思想和設計方案將導致整個應用係統的核心邏輯依賴與其外延的細節,程序本應該是比較穩定的核心邏輯,但因為依賴於易於變化的部分,變得不穩定,一個細節上的功能實現的小小改動,也有可能在依賴關係上引發一係列的變動和修改。這種依賴關係也是麵向過程程序設計不能良好地處理變化的原因之一,而一個合理的依賴關係,應該是倒過來的,即細節實現依賴與核心邏輯,如何達到這樣的設計目標呢,好,該是依賴倒置原則出場的時候了。

 
       依賴倒置原則是指應用係統中的高層模塊不應該依賴於底層模塊,抽象不應該依賴於細節。為了使應用係統中的各個類在設計時能夠依賴於抽象而不是依賴於具體的實現,我們應該針對接口編程,而不是針對具體的實現類進行編程。具體說來,消除兩個模塊間的依賴關係,應該在這兩個模塊之間定義一個抽象的接口,上層模塊調用抽象接口中定義的方法,下層模塊則具體實現該接口。 


       依賴倒置原則的本質就是使用抽象來使應用係統中的高層模塊獨立於係統中的低層模塊,以實現應用係統中高層模塊的自由複用。由於接口是屬於高層的,而低層要實現高層提供的接口。因此,我們現在的設計思想是低層依賴於高層,是依賴關係和接口所有權的倒置。


       我們知道,Java EE平台中有許多輕量級的框架係統,在設計初期,它們就很好地考慮到了自身的擴展問題。從滿足依賴倒置原則的角度來看,需要框架的核心功能是不應依賴於框架具體使用者的不同功能需求(正所謂“抽象不依賴於實現”)。比方說Struts中的PlugIn接口,Spring中的眾多模板方法模式的應用,所有這些都很好的詮釋了本原則。 
       由於抽象類具有提供默認實現的優點,而接口具有鬆散的擴展等優點,因此我推薦大家綜合使用麵向對象語言中的接口和抽象類,那麼如何才能更好地結合兩者以設計出更為優秀的代碼呢?首先,讓接口負責方法的定義,但同時給出一個默認實現(使用抽象類,類似於適配器模式中的適配器類),其它同屬於這個抽象類的具體類可以選擇實現這個接口,或者選擇繼承這個抽象類(這兩種實現方式有區別嗎?你是怎麼理解的?可以給我留言,寫下自己的觀點,嗬嗬)。如果需要對係統中的功能進行擴展,則隻需要向接口中加入新的方法,同時向這個抽象類中添加方法的具體實現即可,因為繼承自這個抽象類的子類都會從這個抽象類中得到具體的方法。

 
       但是,如果我們嚴格遵守依賴倒置原則,則可能導致在整個係統中產生大量的接口,此外,由於依賴倒置原則假定所有具體類都是可能變化的,這在實際係統開發過程中也不總是正確的。如果所依賴的某個具體類是比較穩定的,那麼直接使用或者依賴於這個具體類也不會給係統帶來不良後果。這一點,不是正如我們對Java平台中JDK API的依賴嗎?(或者說對.Net平台的SDK API依賴)


       好了,我們的OO之旅暫且告一段落,還等什麼,趕緊拿起上述準則,對原有係統進行重構吧!嗬嗬,如果你能將5大準則銘記於心,並能靈活運用,那麼我要恭喜你了(明天的OO大師就是你),嗬嗬!然而,要想成為真正的大師,還是有很長的路要走的。樂觀固然是好事,但是要正視眼前的困難。同樣,要想設計出多層架構,鬆耦合的係統,還有很多很多要學,下麵,我就將自己在設計模式上的學習經驗,毫無保留地拿出來與大家分享,希望能夠拋磚引玉!


      在某個具體的軟件項目中我們可以不使用門麵模式,不使用橋模式,不使用觀察者等其它模式,就能夠完成係統功能的實現,隻不過可能會在代碼實現方麵出現一定的複雜性。但是,我們必須將現實世界的實體和業務功能進行抽象,並分配各個類的具體職責。而類和對象的設計,關聯關係的確定以及具體的職責分配又依據什麼呢?是否有一定的設計準則來指導軟件設計人員呢?經典的通用職責分配軟件模式(General Responsibility Assignment Software Pattern),可以給予我們方向性的指導。


      GoF設計模式是一種高效,靈活的設計準則,但問題是在具體應用此模式之前,設計人員必須先設計和確定本應用係統中應該有哪些類,並確定類與類之間的關聯關係,類職責的分配等,才能應用GoF設計模式來優化實現的代碼。所以我想在講述GoF設計模式之前先來回顧一下GRASP模式。通用職責分配軟件模式是描述如何把職責分配給不同對象的有效經驗和基本準則,它包含5個基本模式(信息專家模式,創建者模,高內聚模式,低耦合模式和控製器模式)和4個擴展模式(多態模式,純虛構模式,中介模式,受保護變化模式)。

 
       軟件係統的設計人員在完成類職責的設計時,如果發現某個類擁有完成該職責需要的所有信息(數據),那麼這個職責就應該分配給這個類,這個類就是相對於這個職責的信息專家(Information Expert)類。在實際的軟件開發中,我們還要辨別類的職責是協作完成還是應該獨立完成(即需要一個信息專家還是需要多個不同的信息專家協同完成)。

 
       再來看看創建者模式,創建者模式的主要作用可以概括為如下兩點:封裝創建邏輯的細節和封裝創建邏輯的變化。我個人的理解是:GoF設計模式中的單例模式,工廠方法模式,抽象工廠模式,原型模式,生成器模式都屬於創建者模式的具體實現。別急,關於這些模式的理解,我會在後麵娓娓道來。

 
       至於低耦合和高內聚模式,我就不多做解釋了。我覺得這裏麵更多是經驗的總結。如何做到低耦合,如何做到高內聚,這本身就是個很難解答的問題(一切服從於需求嘛),我們要做的就是不斷從實踐中提取設計準則,所謂的最佳實踐,無非是前人大量經驗的總結。在這,我給大家一個指導性的方針,不過在實際應用中,大家要慎重!好了,準則如下:對於那些對係統穩定性要求比較高且係統不容易發生變化的需求,我們完全可以將其設計成緊耦合的。這樣可以提高效率,提高代碼的內聚性,減少類的數量。反之,我們在設計上,盡量考慮鬆耦合。而降低類與類之間耦合度的一個常用技術手段就是應用抽象層次。

 
      再囉嗦幾句,在應用係統的類設計中,為了達到鬆耦合的設計目標,我們經常采用的設計策略一般包括:依賴倒置原則,控製反轉等。

 
      但是,請注意:依賴倒置原則描述的是類與類之間代碼級的依賴關係。如果應用係統是基於某種框架係統開發的,則該應用係統中的類對目標框架的依賴關係就會更強。這時,控製反轉的思想應運而生。那麼控製反轉又為何物呢?它是說將控製權(原本由程序來控製對象之間的依賴關係)由應用程序轉移到了外部容器。利用控製反轉能夠減少對象請求者對服務提供者的特定實現的邏輯依賴,因為組件類不需要查找或者實例化它們所依賴的組件類。


      好了,今天就先囉嗦到這吧~


參考文獻:

1.https://www.ibm.com/developerworks/cn/education/java/j-patterns201/index.html

最後更新:2017-04-02 22:16:33

  上一篇:go HttpComponents組件探究 - HttpClient篇
  下一篇:go 轉換數據庫時間字段格式函數