[設(shè)計模式如此簡單]一、策略模式

本文章著作權(quán)由 沈慶陽 所有,轉(zhuǎn)載請與作者取得聯(lián)系!

幾乎所有的新手程序員都會遇到這樣的一些問題:在編寫代碼的時候,盡管知道自己的目標(biāo)是什么。但當(dāng)代碼編寫到一定程度,代碼量慢慢變大的時候,整個程序的方向不再是自己可控的。編碼工作與目標(biāo)相差愈遠(yuǎn),以至于最終卡在某個環(huán)節(jié)而推翻重來。這是個人遇到的問題,如果這個問題放到團(tuán)隊中呢?缺少適當(dāng)?shù)慕涌?,代碼閱讀性、維護(hù)性差,對于項目帶來的后果是不可想象的。但是問題總歸有解決的方法,在一次次的代碼編寫過程中,前人總結(jié)出來的經(jīng)驗便歸結(jié)成為了設(shè)計模式。

設(shè)計模式就是一套被反復(fù)使用、經(jīng)過分類的代碼設(shè)計經(jīng)驗的總結(jié)。通過使用設(shè)計模式我們可以增加代碼的復(fù)用性,使代碼更容易被他人理解從而提高了代碼的可靠性。設(shè)計模式可以使得代碼編寫真正地實現(xiàn)工程化。

那么在Unity 3D的代碼編寫過程中,經(jīng)過總結(jié)我發(fā)現(xiàn)有四種設(shè)計模式的使用最為頻繁和方便。這四種設(shè)計模式分別是:策略模式、單例模式、工廠模式觀察者模式

策略模式

“策略模式定義了一系列的算法,并將每一個算法封裝起來,而且使它們還可以相互替換。策略模式讓算法獨立于使用它的客戶而獨立變化?!?/code>

哦,看不懂是么,上面的解釋是直接從百科中復(fù)制出來的,所以不必?fù)?dān)心讀不懂的問題,因為它只是一些基本的概念,我們甚至不需要知道這些概念。因為設(shè)計模式不是概念,而是編程的經(jīng)驗,我們只要學(xué)會這些經(jīng)驗就行了。

讓我們以一個簡單的例子來理解策略模式。

UBug游戲公司要做一款模擬狗狗的游戲——狗狗模擬器,Jack是UBug公司的資深程序員,于是這個小游戲編寫的擔(dān)子就到了Jack的肩上。公司的游戲架構(gòu)師給了Jack一份開發(fā)文檔,使用標(biāo)準(zhǔn)的面向?qū)ο螅∣O)技術(shù)進(jìn)行了該系統(tǒng)的內(nèi)部設(shè)計,該設(shè)計設(shè)計了一個狗的父類,并讓所有各種狗繼承該父類。

1.gif

Jack根據(jù)游戲架構(gòu)師給的設(shè)計編寫了這樣的一個程序,他很滿意,因為這樣就可以實現(xiàn)所有種類的狗狗了,簡直完美。UBug公司的產(chǎn)品經(jīng)理在看完這樣一個Demo之后覺得總少些什么,現(xiàn)在的模擬狗狗游戲這么多,我們是不是該有一些創(chuàng)新的地方?既然這樣寫的狗狗不會跳舞,那么我們寫出了會跳舞的狗狗是不是獨樹一幟呢!于是產(chǎn)品經(jīng)理拍了拍Jack的肩膀說,讓狗狗跳起來吧?

Jack回到自己的工位上,他自信滿滿,有了架構(gòu)師給的設(shè)計,我直接在Dog的父類添加一個跳舞的方法不就完美了么?對于一個面向?qū)ο蟮某绦?,簡直So Easy!

2.gif

Jack改完代碼之后,自信滿滿,測試都沒有測試便直接把程序打個包,發(fā)給了他的主管。

于是,在一周之后的股東大會上,UBug的主管給股東們展示了這樣一個“革新”的模擬游戲:哈士奇會跳舞,泰迪會跳舞,就連石頭做的狗狗雕像都在跳舞?

Jack被主管叫到了辦公室,你這樣是不行的?石頭做的狗狗是沒有生命的,怎么可能會跳舞呢?Jack劈頭蓋臉挨了一頓臭罵,回到自己的工位上陷入了沉思。

Jack忽略了一個事情,并不是所有的狗狗都會跳舞。而Jack在父類寫上跳舞的方法之后,所有繼承該父類的子類都擁有跳舞這個方法了。對代碼所做的局部的修改,影響的層面并不只是局部(修改了父類的代碼會影響繼承其的子類)**!于是Jack開始修改狗狗模擬器的代碼。

3.gif

哦,Jack終于完成了對代碼的修改。他把看門狗的bark(),walk(),dance()等方法覆蓋掉了,也就是什么也不做??墒侨绻院笥忠尤敫嗟拇罄硎墓饭贰⑺芰献龅墓饭吠婢呖刹皇且獙懜嗟拇a了么?代碼的復(fù)用性變得好差。

Jack開始總結(jié),利用繼承來實現(xiàn)物體的行為會造成牽一發(fā)而動全身的后果,我是不是可以考慮采用其他的方法?如果采用接口呢?Jack可以將bark()、walk()、dance()寫成接口(Barkable()、Walkable()、Danceable()),從而實現(xiàn)不同的狗狗的行為??墒沁@樣就意味著對每個新增的狗狗都要寫出其bark()等接口的實現(xiàn),代碼的復(fù)用性極低。如果狗狗模擬器要寫幾百只品種的狗狗,那豈不是無窮盡的噩夢!

在代碼的編寫過程中,我們應(yīng)遵循良好的面向?qū)ο笤O(shè)計原則,使用一種對既有代碼影響較小
方式來修改軟件,從而投入更多的精力添加新的功能。在軟件開發(fā)的過程和軟件維護(hù)的過程中,沒有哪個軟件是一成不變的??傂枰粩嗟貙浖M(jìn)行修改**,這是亙古不變的真理。一旦軟件停止了更新,那么這個軟件的生命周期離死亡也就不遠(yuǎn)了。

Jack意識到了繼承并不是解決問題最好的方法,因為狗的行為在子類中總是不斷改變的,讓所有的子類都具有父類中同樣的行為似乎不是很恰當(dāng)。使用接口來解決這個問題起初看似很好,但C#中接口不實現(xiàn)任何代碼,所以通過繼承接口無法實現(xiàn)代碼的復(fù)用。于是Jack找到UBug的游戲架構(gòu)師尋求幫助。游戲架構(gòu)師給了他一個經(jīng)驗:找到應(yīng)用中可能需要變化的地方,把它們獨立出來,不要和那些不需要變化的代碼混在一起。那么怎么確定代碼中可能發(fā)生變化的地方呢?比如每次需求的更改或者增加了新的需求,都會造成某些地方的代碼發(fā)生改變,那么我們就可以確定這一部分的代碼需要被獨立出來,和其余穩(wěn)定的代碼部分有所區(qū)分。也就是說,通過上述的設(shè)計原則,我們把會變化的部分提取并封裝起來,以便后續(xù)可以輕易地改動此部分的代碼,從而不影響不需要變化的部分。這幾乎是所有的設(shè)計模式的精髓,所有的設(shè)計模式都在解決系統(tǒng)中某部分的改變不會影響其他部分。至此,是時候把狗狗的行為從父類中提出出來了!

經(jīng)過一系列的分析過后,Jack發(fā)現(xiàn)除了bark()、dance()等問題之外,狗類的行為還算正常,因此為了分開“變化和不變化”的部分,Jack準(zhǔn)備建立兩組類。一個是和bark()相關(guān)的,一個是和dance()相關(guān)的,每一組類都實現(xiàn)各自的行為。比如可以有一個類實現(xiàn)安靜不叫的狗狗,一個類實現(xiàn)怒吼的狗狗一個類實現(xiàn)哀鳴的狗狗。Jack明白了他要做的是針對接口編程,而不是針對實現(xiàn)編程**。

我們利用接口代表每個行為,比如BarkBehaviour和DanceBehaviour,每個實現(xiàn)都將實現(xiàn)其中的一個接口。我們編寫了一個專門實現(xiàn)BarkBehaviour與DanceBehaviour的“行為”類。由行為類而不是狗狗類來實現(xiàn)行為的接口。以往的做法,行為由父類來實現(xiàn),或是繼承某個接口并由子類實現(xiàn)。這兩種方法都是針對實現(xiàn)編程,代碼被實現(xiàn)綁死,很難去更改具體行為。在新的設(shè)計中,狗的子類將使用接口(BarkBehaviour等)來表示行為,所以實現(xiàn)的“實現(xiàn)”不會與狗的子類產(chǎn)生綁定,也就是具體行為編寫在實現(xiàn)了BarkBehaviour等的類中。

4.gif

如何理解針對接口編程

“針對接口編程”的真正含義就是“針對超類型編程”。

在軟件術(shù)語中,被繼承的類一般稱為“超類”,也有叫做父類。通常情況下,每個子類的對象“是”它的超類的對象。一個超類可以有很多個子類,所以超類的集合通常比它的任何一個子類集合都大。

假設(shè)有一個抽象類Programmer,有兩個具體實現(xiàn)來繼承Programmer,分別是CppProgrammer和JavaProgrammer。

由針對實現(xiàn)編程的做法如下:

CppProgrammer cppProgrammer=new CppProgrammer();

cppProgrammer.code();

上述聲明了一個cppProgrammer,以CppProgrammer為類型。cppProgrammer是Programmer的具體實現(xiàn),我們必須對具體實現(xiàn)編碼。如果使用針對接口編程做法是否會更方便呢?

Programmer 
programmer=new CppProgrammer();

programmer.code();

當(dāng)我們知道對象是C++程序員的時候可以利用Programmer進(jìn)行多態(tài)調(diào)用。

那么理解了如何針對接口編程之后,我們就需要開始整合狗狗的行為了。

首先,需要在Dog類中加入兩個接口的實例變量,BarkBehaviour和DanceBehaviour,其聲明為接口類型(不是具體類實現(xiàn)類型),每個狗狗對象都會動態(tài)地設(shè)置這些變量以便在運(yùn)行時引用正確的行為類型(如AngryBark、Quiet等)。

public class Dog{

   BarkBehaviour  barkBehaviour;

         …

         public  void performBark(){

                  barkBehaviour.bark();

      }

}

在上述代碼中,狗狗對象并非自己親自處理Bark行為,而是委托給BarkBehaviour來引用的對象。

那么如何具體設(shè)定barkBehaviour的實例變量呢?

public class Husky:Dog{
      public  Husky(){
      }
}
5.gif

在繼續(xù)往下看的時候,不妨先根據(jù)上面的類圖自己編寫C#代碼,并在Unity中跑一下。

你運(yùn)行自己編寫的代碼了么?好吧,那么就看一下我們寫的代碼吧。

public abstract class Dog{

   
public BarkBehaviour barkBehaviour;

   
public DanceBehaviour danceBehaviour;

 

   
public Dog() { }

 

   
public abstract void display();

 

   
public void performBark()

    {

       
barkBehaviour.Bark();

    }

 

   
public void performDance()

    {

       
danceBehaviour.Dance();

    }

 

   
public void walk()

    {

       
Debug.Log("I am walking...");

       
System.Console.WriteLine("I am walking...");

    }

}

 

public interface BarkBehaviour

{

   
void Bark();

}

 

public class AngryBark : BarkBehaviour

{

   
void BarkBehaviour.Bark()

    {

       
Debug.Log("Woof!Woof!Woooooooof!!!!!(Angry Face)!");

       
System.Console.WriteLine("Woof!Woof!Woooooooof!!!!!(Angry
Face)!");

    }

}

 

public class QuietBark : BarkBehaviour

{

   
void BarkBehaviour.Bark()

    {

       
Debug.Log("...");

       
System.Console.WriteLine("...");

    }

}

 

public class SadBark : BarkBehaviour

{

   
void BarkBehaviour.Bark()

    {

       
Debug.Log("Woo~~~Woooo...(Sad Face)...");

       
System.Console.WriteLine("Woo~~~Woooo...(Sad Face)...");

    }

}

 

public interface DanceBehaviour

{

   
void Dance();

}

 

public class JazzDance : DanceBehaviour

{

   
void DanceBehaviour.Dance()

    {

       
Debug.Log("Jaaaaz...Jazz.Jazz....");

       
System.Console.WriteLine("Jaaaaz...Jazz.Jazz....");

    }

}

 

public class WaggleDance : DanceBehaviour

{

   
void DanceBehaviour.Dance()

    {

       
Debug.Log("Waggle Waggle Waggle...");

       
System.Console.WriteLine("Waggle Waggle Waggle...");

    }

}
public class Husky : Dog{

  
public Husky()

    {

       
barkBehaviour = new AngryBark();

       
danceBehaviour = new WaggleDance();

    }

 

   
public override void display()

    {

        Console.WriteLine("Wow Look At Me??I
am a Husky?!");

       
Debug.Log("Wow Look At Me??I am a Husky?!");

    }

}

編寫測試腳本

public class UBugSimDogs : MonoBehaviour {

   
Dog husky = new Husky();

         //
Use this for initialization

         void
Start () {

       
husky.performBark();

       
husky.performDance();

         }

}

那么在Unity的控制臺中運(yùn)行,我們將看到如下效果

6.png

哈,正確了。我們的哈士奇可以憤怒地吼叫和跳舞了!完美!

但是至此,Jack開始了思考,我是否可以讓這個程序更加完美呢?每次都在構(gòu)造函數(shù)中指定相應(yīng)的行為是否太麻煩?能不能有更妙的方法?有的!我們可以使用“設(shè)定方法”(Setter Method)來設(shè)定狗狗的行為,而不是在構(gòu)造器中實例化。在Dog類中加入兩個新的設(shè)定方法。

public void setBarkBehaviour(BarkBehaviour bb)

  {

     
barkBehaviour = bb;

  }



 
public void setDanceBehaviour(DanceBehaviour db)

  {

     
danceBehaviour = db;

}

使用上述兩個設(shè)定方法編寫我們的泰迪吧!

public class Teddy : Dog

{

   
public Teddy()

    {

       
barkBehaviour = new SadBark();

       
danceBehaviour = new WaggleDance();

    }

   
public override void display()

    {

       
Debug.Log("Woo,I am Teddy?I f everywhere!");

    }

}

Teddy的測試腳本

public class UBugSimDogs : MonoBehaviour {

   
Dog teddy = new Teddy();

         //
Use this for initialization

         void
Start () {

       
teddy.performBark();

       
teddy.setBarkBehaviour(new QuietBark());

       
teddy.performBark();

 

       
teddy.performDance();

       
teddy.setDanceBehaviour(new JazzDance());

       
teddy.performDance();

         }

}

在上述腳本中,我們動態(tài)地更改了Teddy的Bark行為和Dance行為,看看控制臺中我們的Teddy是怎樣表演的!

![Upload 7.png failed. Please try again.]

成功啦!我們動態(tài)地更改了Teddy的行為!那么在運(yùn)行中如果想要更改狗狗的行為只用調(diào)用setter方法就可以了。

總結(jié)

讓我們來回顧一下最終的代碼,將其繪制為UML圖。

8.gif

在上圖中,我們不再把狗狗說成一組行為,而是看成一組算法。就像文章開頭說的一樣,策略模式定義了一系列的算法,并將每一個算法封裝起來。在上圖中,有繼承、直接關(guān)聯(lián)和接口實現(xiàn)三種箭頭,其關(guān)系表示了“是一個”、“有一個”和“實現(xiàn)”的關(guān)系。

在面向?qū)ο蟮某绦蛟O(shè)計中,關(guān)聯(lián)有時候比繼承更好。也就是面向?qū)ο蟮脑O(shè)計原則中有,多用組合,少用繼承。使用組合建立的系統(tǒng)具有很大的彈性,其可以將算法封裝在一起,也可以在運(yùn)行時動態(tài)地改變行為。當(dāng)然,組合的行為對象應(yīng)該符合接口的標(biāo)準(zhǔn)。

參考文獻(xiàn):Freeman, Elisabeth, Freeman, et al. Head First Design Patterns[C]// O' Reilly & Associates, Inc. 2004.

本文章著作權(quán)由 沈慶陽 所有,轉(zhuǎn)載請與作者取得聯(lián)系!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,942評論 25 709
  • 設(shè)計模式匯總 一、基礎(chǔ)知識 1. 設(shè)計模式概述 定義:設(shè)計模式(Design Pattern)是一套被反復(fù)使用、多...
    MinoyJet閱讀 4,081評論 1 15
  • 分享一則之前賣創(chuàng)意T的收獲。 創(chuàng)意T這種東西,100%是創(chuàng)意驅(qū)動的,什么意思呢?沒有人愿意撞衫啊!尤其是這玩樣兒買...
    Stove3閱讀 1,122評論 0 2
  • 作為史上唯恐天下不亂,生來就為搗蛋的骨灰級選手,像心愛的百合姐出軌這樣的大事,我怎么能不來當(dāng)當(dāng)圍觀吃瓜群眾,為新聞...
    前世911閱讀 495評論 10 2
  • 有些事情需要儀式感,2016年2月已過去,寫一篇月度總結(jié),和二月說再見。第一次寫月度總結(jié),不追求多細(xì)致,堅持下來才...
    愛吃饅頭的姑娘閱讀 900評論 0 0

友情鏈接更多精彩內(nèi)容