1. 程式人生 > >程式碼重構 +設計模式六大原則 + 23種設計模式

程式碼重構 +設計模式六大原則 + 23種設計模式

類的設計  類的使用(組合 泛華)

1 讓它的職責儘量單一  2 讓它儘可能通過方法引數的輸入輸出就能完成相關的功能  3 讓依賴的類都儘量改為介面而不是例項()

學會單元測試,培養你的重構意識

可能上面說了這麼多,還是有很多人並不理解重構。沒關係,在這裡我教你們一個快速入門的辦法,就是單元測試。什麼是單元測試,請自行google。單元測試有什麼要求?就是要求你要把每個方法都弄成儘量可以測試的。儘量讓你的方法變成是可測試的,就是培養你重構意識的利器。

在你要求把方法變成可測試的過程,你就會發現你必須得不斷的修改你的方法,讓它的職責儘量單一,讓它儘量的與上下文無關,讓它儘可能通過方法引數的輸入輸出就能完成相關的功能,讓依賴的類都儘量改為介面而不是例項。最終,你就會發覺,這就是重構!而且是在不知不覺中,你重構的功力就會大大提升,你程式設計的水平也會大大提升!

看到這裡,有經驗的程式設計師就會問,你這是在鼓勵我使用TDD嗎?不,不是的。TDD(Test-Driven Development)鼓勵的是測試驅動開發,未開發之前先編寫單元測試用例程式碼,測試程式碼確定需要編寫什麼產品程式碼。這是一種比較先進的開發方法,但是在程式設計的實踐過程中,我認為它過於繁瑣,很多中小企業很難實施,更別提我們個人開發者。我這裡提倡你用單元測試培養你的重構意識,可以說是一種後驅動,用於提高你的重構能力和重構願望,你完全可以把我的這個方法稱為“TDR(Test-Driven Refactoring)——測試驅動重構”。

當然,在開發之前如果你有意識的讓方法可測試,那麼你寫出來的函式將會是比較高質量的程式碼。當你的函式都是一個個可重用性高的函式之時,你將會發現,寫程式碼其實就像堆積木一樣,可以把一個大型的需求分解成無數細小的功能,很快的把需求實現。

以下是一個超大方法中的一段程式碼,如果你懂得怎樣讓這段程式碼程式設計一個可測試的方法,那麼,恭喜你,你入門了。


面向介面程式設計<郭新泉>


引數輸入輸出: 


改進:

 
 openGl 在申請記憶體存放紋理時,是按照2的冪次方申請的,即480*320的圖片,它申請512*512的記憶體。
 所以在設計時圖片時,最好時2的N方。由於不是所有圖片都是2的N方。所以通過“拼圖”的方法解決。即why要用texturePacker的原因


所謂重構       《重構,改善既有程式碼的設計》

如果你有耐心看到這裡,你應該知道,我並非一個標題黨,而這篇文章也許稱為“如何在程式設計中應用重構的思想”更為貼切,但是我不想用這麼嚴肅的標題。

很多程式設計初學者,或者有多年程式設計經驗的人都覺得閱讀別人的程式碼非常困難,重構更是無從談起,他們要麼對這些程式碼望洋興嘆,要麼就是推翻從來。但是,如果我們有重構的意識,以及在程式設計的過程中熟悉一些程式碼調整和優化的小技巧,你自然而然就會培養出重構的能力。

重構,其實很簡單:

  • 把基礎打牢固

  • 多看點優秀的程式碼

  • 避免複製貼上,如果看見重複程式碼時應該有意識要消滅它

  • 減少對程式碼生成器的依賴

  • 在處理現有程式碼時儘量用重構代替重寫,在重寫之前一定要先重構

  • 儘量讓所有的方法都是可測試的

如果你堅持這麼去做了,一段時間之後感覺自然就出來了。

重構的目的,是讓你的程式碼更為精簡、穩定、能夠重用,是最大程度的讓功能和業務分離。在重構的過程中,你的閱讀程式碼的能力、寫出優秀程式碼的能力以及系統架構能力都會穩步提升。你成為一個優秀的程式設計師將指日可待。

等你無法重構的時候再考慮重寫

我帶過很多優秀的程式設計師,也與很多優秀的程式設計師共事過。有一大部分的程式設計師在看到一套系統不是那麼滿意,或者存在某些明顯的問題,就總是忍不住要把整套系統按自己覺得可以優化的方向來重寫,結果,重寫結構往往並不令人滿意。系統中確實存在很多不合理的地方,但是有不少的這種程式碼,恰恰是為了解決一些特定場景下的問題的。也就是說,所有的規範以及程式設計的原則,其實也是有條件限制的,他可能在大部分的時候是正確的,能夠指導你完成你的任務,但是,並不是在所有地方都是適用的。比如資料庫正規化,但實際中我們的設計往往會考慮冗餘,這是違背正規化的,但是為什麼還有那麼多人趨之若鶩呢?因為我們可能需要用空間換時間。

如果我們一開始就考慮重寫,那麼你可能會陷入以下的困境:

  • 需要花更大的精力來完成一些看似簡單的BUG

你要知道,有一部分看似錯誤或者非常不優美的程式碼,其實恰恰是為了解決一些非常刁鑽的問題的。

  • 再也無法相容老的系統了

你急於把原有系統重寫,卻往往忽略了對原有系統的相容,那麼你新的系統的推進則會十分緩慢。而老系統的維護,又會陷入及其尷尬的情況。

  • 過度設計,導致重寫計劃遲遲無法完成

有重寫衝動的程式設計師往往是在架構設計上有一些讀到的見解,他們善於利用所學的各種設計模式和架構技巧來建立系統,但是越是想盡可能的利用設計模式,越是陷入過度設計的困局,導致重寫的計劃遲遲都無法完成。

  • 無法有效利用現有系統已經完成並測試的程式碼

如果你確實有必要進行重寫,我還是建議你把程式碼儘可能的重構。因為重構之後的系統,能夠讓你更輕易的重寫,又最大限度了保留以前可用的業務程式碼。

我舉個例子,說明如何通過重構更好的利用現有程式碼的。

我有一個非常龐大的系統,其中有一塊功能是用於資料採集、儲存、告警管理以及電話、簡訊等告警通知。大致的結構如下:

class="brush:js;toolbar:false">class MainEngine:IEngine{ public MainEngine(ConfigSettings config){ } public void Start(); public void Stop(); }

需要增加新的業務功能時,程式設計師寫的程式碼往往是這樣的:首先時修改配置類

1 2 3 4 5 6 class ConfigSettings{ public bool NewFuncEnable{get;private set;} public ConfigSettings(){ NewFuncEnable=xx;//從配置檔案讀取 } }

接著修改主程式:


在修改的過程中,往往是根據配置檔案來判斷新功能是否啟用。上面程式碼會造成什麼問題呢:

  • 主程式程式碼和擴充套件功能耦合性太強,每增加一個功能都要修改主程式程式碼,這裡非常非常容易出錯。尤其是新的人進度開發組,很容易就忘主程式中增加了一些致命性的程式碼。比如上述的擴充套件功能,可能是在特定的專案中才會有這個擴充套件功能,但是,寫程式碼的人忘記增加是否啟用的配置選項了,導致所有的專案都應用了這個功能,而這個功能需要特定的表,這樣就悲劇了。即使是你增加了配置,也是非常的不美觀,因為在通用的版本中使用了這個配置,往往會讓定製專案以外的人員感到困惑。

  • 增加擴充套件功能的人還需對整個MainEngine程式碼有一定的熟悉,否則,他根本就不知道在Start方法和Stop方法進行newClas的對應方法的呼叫

  • 如果你打算對這段程式碼進行重寫,那麼,你會感到非常的困難,因為你分不清楚newCls這個新例項的作用,要麼你花大精力去把所有程式碼理清楚,要麼直接就把這段新增的業務程式碼去掉了。

那麼我們如何對這段程式碼進行重構呢。首先,我們把新功能註冊的程式碼抽取出來,通過反射來實現新的功能的註冊。

修改MainEngine程式碼

OK,現在我們再來看看怎麼實現原來的新增功能:你只需按規範新建一個類,繼承ITaskHandler介面,並實現介面的方法。最後在XTGL_ServiceBundle表中新增一條記錄即可。我們再來看看這麼做有什麼好處:

  • 新增的類只需按規範寫即可,完全對MainEngine程式碼沒有任何影響。你甚至可以把這個MainEngine程式碼寫在一個新建的Dll中。

  • 新增功能的這個業務類跟原來的程式碼解耦,非常方便進行新功能的業務測試,而無需考慮原有框架的影響

  • 新增功能的業務類與架構完全分離,我們在重寫程式碼中只要保證介面的穩定性,無論我們怎麼把系統架構重寫,我們可以馬上就重用上原有的業務功能程式碼。

重構的目標之一,就是把框架和業務完全分離。

有志於深入瞭解的同學,可以瞭解下反射、Ioc和外掛話程式設計等。


----------------------------------------------------------------------------------------------------------------------------------------------

class ReadStory
{
public:
    virtual void getContent() = 0;   //二者都應該依賴其抽象;
};


// 將類A修改為依賴介面I,類B和類C各自實現介面I
class  Book : public ReadStory    //
{
public:
    void  getContent()
    {
        CCLOG("by“讀書”來講故事");
    }
};


class Newspaper : public ReadStory
{
public:
    void  getContent()
    {
        CCLOG("by讀”報紙“講故事");
    }
};


//class Tv
//{
//    
//};


class Mother
{
public:
    void setObj(  ReadStory *obj)  // getself() {return s_self;}
    {
        readStory = obj;
    }
    
    void toSonReadStory()
    {
        CCLOG("媽媽開始講故事");
        
        readStory->getContent() ;
    }
    
    
private:
    ReadStory *readStory;
};

    Mother *_mother = new Mother();
    _mother->setObj(new Newspaper());
    _mother->toSonReadStory();

定義:高層模組不應該依賴低層模組,二者都應該依賴其抽象;抽象不應該依賴細節;細節應該依賴抽象。

問題由來:類A直接依賴類B,假如要將類A改為依賴類C,則必須通過修改類A的程式碼來達成。這種場景下,類A一般是高層模組,負責複雜的業務邏輯;類B和類C是低層模組,負責基本的原子操作;假如修改類A,會給程式帶來不必要的風險。

解決方案:將類A修改為依賴介面I,類B和類C各自實現介面I,類A通過介面I間接與類B或者類C發生聯絡,則會大大降低修改類A的機率。

         依賴倒置原則基於這樣一個事實:相對於細節的多變性,抽象的東西要穩定的多。以抽象為基礎搭建起來的架構比以細節為基礎搭建起來的架構要穩定的多。在java中,抽象指的是介面或者抽象類,細節就是具體的實現類,使用介面或者抽象類的目的是制定好規範和契約,而不去涉及任何具體的操作,把展現細節的任務交給他們的實現類去完成。

         依賴倒置原則的核心思想是面向介面程式設計,我們依舊用一個例子來說明面向介面程式設計比相對於面向實現程式設計好在什麼地方。場景是這樣的,母親給孩子講故事,只要給她一本書,她就可以照著書給孩子講故事了。程式碼如下:

  1. class Book{  
  2.     public String getContent(){  
  3.         return"很久很久以前有一個阿拉伯的故事……";  
  4.     }  
  5. }  
  6. class Mother{  
  7.     publicvoid narrate(Book book){  
  8.         System.out.println("媽媽開始講故事");  
  9.         System.out.println(book.getContent());  
  10.     }  
  11. }  
  12. publicclass Client{  
  13.     publicstaticvoid main(String[] args){  
  14.         Mother mother = new Mother();  
  15.         mother.narrate(new Book());  
  16.     }  
  17. }  

執行結果:

媽媽開始講故事
很久很久以前有一個阿拉伯的故事……

        執行良好,假如有一天,需求變成這樣:不是給書而是給一份報紙,讓這位母親講一下報紙上的故事,報紙的程式碼如下:

  1. class Newspaper{  
  2.     public String getContent(){  
  3.         return"林書豪38+7領導尼克斯擊敗湖人……";  
  4.     }  
  5. }  

        這位母親卻辦不到,因為她居然不會讀報紙上的故事,這太荒唐了,只是將書換成報紙,居然必須要修改Mother才能讀。假如以後需求換成雜誌呢?換成網頁呢?還要不斷地修改Mother,這顯然不是好的設計。原因就是Mother與Book之間的耦合性太高了,必須降低他們之間的耦合度才行。

我們引入一個抽象的介面IReader。讀物,只要是帶字的都屬於讀物:

  1. interface IReader{  
  2.     public String getContent();  
  3. }  

Mother類與介面IReader發生依賴關係,而Book和Newspaper都屬於讀物的範疇,他們各自都去實現IReader介面,這樣就符合依賴倒置原則了,程式碼修改為:

  1. class Newspaper implements IReader {  
  2.     public String getContent(){  
  3.         return"林書豪17+9助尼克斯擊敗老鷹……";  
  4.     }  
  5. }  
  6. class Book implements IReader{  
  7.     public String getContent(){  
  8.         return"很久很久以前有一個阿拉伯的故事……";  
  9.     }  
  10. }  
  11. class Mother{  
  12.     publicvoid narrate(IReader reader){  
  13.         System.out.println("媽媽開始講故事");  
  14.         System.out.println(reader.getContent());  
  15.     }  
  16. }  
  17. publicclass Client{  
  18.     publicstaticvoid main(String[] args){  
  19.         Mother mother = new Mother();  
  20.         mother.narrate(new Book());  
  21.         mother.narrate(new Newspaper());  
  22.     }  
  23. }  

執行結果:

媽媽開始講故事
很久很久以前有一個阿拉伯的故事……
媽媽開始講故事
林書豪17+9助尼克斯擊敗老鷹……

    這樣修改後,無論以後怎樣擴充套件Client類,都不需要再修改Mother類了。這只是一個簡單的例子,實際情況中,代表高層模組的Mother類將負責完成主要的業務邏輯,一旦需要對它進行修改,引入錯誤的風險極大。所以遵循依賴倒置原則可以降低類之間的耦合性,提高系統的穩定性,降低修改程式造成的風險。

    採用依賴倒置原則給多人並行開發帶來了極大的便利,比如上例中,原本Mother類與Book類直接耦合時,Mother類必須等Book類編碼完成後才可以進行編碼,因為Mother類依賴於Book類。修改後的程式則可以同時開工,互不影響,因為Mother與Book類一點關係也沒有。參與協作開發的人越多、專案越龐大,採用依賴導致原則的意義就越重大。現在很流行的TDD開發模式就是依賴倒置原則最成功的應用。

         傳遞依賴關係有三種方式,以上的例子中使用的方法是介面傳遞,另外還有兩種傳遞方式:構造方法傳遞setter方法傳遞,相信用過Spring框架的,對依賴的傳遞方式一定不會陌生。
在實際程式設計中,我們一般需要做到如下3點:

  • 低層模組儘量都要有抽象類或介面,或者兩者都有。
  • 變數的宣告型別儘量是抽象類或介面。
  • 使用繼承時遵循里氏替換原則。

        依賴倒置原則的核心就是要我們面向介面程式設計,理解了面向介面程式設計,也就理解了依賴倒置。

---------------------

類圖:

類圖知識點:

1.類圖分為三部分,依次是類名、屬性、方法

2.以<<開頭和以>>結尾的為註釋資訊

3.修飾符+代表public,-代表private,#代表protected,什麼都沒有代表包可見。

4.帶下劃線的屬性或方法代表是靜態的。

5.對類圖中物件的關係不熟悉的朋友可以參考文章:

定義:一個軟體實體如類、模組和函式應該對擴充套件開放,對修改關閉。

問題由來:在軟體的生命週期內,因為變化、升級和維護等原因需要對軟體原有程式碼進行修改時,可能會給舊程式碼中引入錯誤,也可能會使我們不得不對整個功能進行重構,並且需要原有程式碼經過重新測試。

解決方案:當軟體需要變化時,儘量通過擴充套件軟體實體的行為來實現變化,而不是通過修改已有的程式碼來實現變化。

         開閉原則是面向物件設計中最基礎的設計原則,它指導我們如何建立穩定靈活的系統。開閉原則可能是設計模式六項原則中定義最模糊的一個了,它只告訴我們對擴充套件開放,對修改關閉,可是到底如何才能做到對擴充套件開放,對修改關閉,並沒有明確的告訴我們。以前,如果有人告訴我“你進行設計的時候一定要遵守開閉原則”,我會覺的他什麼都沒說,但貌似又什麼都說了。因為開閉原則真的太虛了。

         在仔細思考以及仔細閱讀很多設計模式的文章後,終於對開閉原則有了一點認識。其實,我們遵循設計模式前面5大原則,以及使用23種設計模式的目的就是遵循開閉原則。也就是說,只要我們對前面5項原則遵守的好了,設計出的軟體自然是符合開閉原則的,這個開閉原則更像是前面五項原則遵守程度的“平均得分”,前面5項原則遵守的好,平均分自然就高,說明軟體設計開閉原則遵守的好;如果前面5項原則遵守的不好,則說明開閉原則遵守的不好。

         其實筆者認為,開閉原則無非就是想表達這樣一層意思:用抽象構建框架,用實現擴充套件細節。因為抽象靈活性好,適應性廣,只要抽象的合理,可以基本保持軟體架構的穩定。而軟體中易變的細節,我們用從抽象派生的實現類來進行擴充套件,當軟體需要發生變化時,我們只需要根據需求重新派生一個實現類來擴充套件就可以了。當然前提是我們的抽象要合理,要對需求的變更有前瞻性和預見性才行。

         說到這裡,再回想一下前面說的5項原則,恰恰是告訴我們用抽象構建框架,用實現擴充套件細節的注意事項而已:單一職責原則告訴我們實現類要職責單一;里氏替換原則告訴我們不要破壞繼承體系;依賴倒置原則告訴我們要面向介面程式設計;介面隔離原則告訴我們在設計介面的時候要精簡單一;迪米特法則告訴我們要降低耦合。而開閉原則是總綱,他告訴我們要對擴充套件開放,對修改關閉。

         最後說明一下如何去遵守這六個原則。對這六個原則的遵守並不是是和否的問題,而是多和少的問題,也就是說,我們一般不會說有沒有遵守,而是說遵守程度的多少。任何事都是過猶不及,設計模式的六個設計原則也是一樣,制定這六個原則的目的並不是要我們刻板的遵守他們,而需要根據實際情況靈活運用。對他們的遵守程度只要在一個合理的範圍內,就算是良好的設計。我們用一幅圖來說明一下。

        圖中的每一條維度各代表一項原則,我們依據對這項原則的遵守程度在維度上畫一個點,則如果對這項原則遵守的合理的話,這個點應該落在紅色的同心圓內部;如果遵守的差,點將會在小圓內部;如果過度遵守,點將會落在大圓外部。一個良好的設計體現在圖中,應該是六個頂點都在同心圓中的六邊形。

        在上圖中,設計1、設計2屬於良好的設計,他們對六項原則的遵守程度都在合理的範圍內;設計3、設計4設計雖然有些不足,但也基本可以接受;設計5則嚴重不足,對各項原則都沒有很好的遵守;而設計6則遵守過渡了,設計5和設計6都是迫切需要重構的設計。

         到這裡,設計模式的六大原則就寫完了。主要參考書籍有《設計模式》《設計模式之禪》《大話設計模式》以及網上一些零散的文章,但主要內容主要還是我本人對這六個原則的感悟。寫出來的目的一方面是對這六項原則系統地整理一下,一方面也與廣大的網友分享,因為設計模式對程式設計人員來說,的確非常重要。正如有句話叫做一千個讀者眼中有一千個哈姆雷特,如果大家對這六項原則的理解跟我有所不同,歡迎留言,大家共同探討。

 下面是前面5項設計原則的連結

同時為了方便想收藏的朋友,下面給出word版本的下載。