Unity3d 一個優秀的程序必備的幾種設計模式

【徹底參考】http://www.unitymanual.com/thread-22531-1-2.htmlhtml

編程衆所周知,它是屬於腳本化,腳本沒有一個具體的概念跟架構,
致使在項目過程當中,常常出現哪裏須要實現什麼功能,就隨便添加腳本,
結果,就形成了一片混亂,很差管理。
更有甚者,本身的寫的代碼閒置一段時間後,再去想找某個功能的實現,都要在視圖中翻來覆去找半天。
哎!請允許我在此感嘆一聲,這仍是你寫的東西麼?


所以,一個好的設計模式是多麼的重要啊,
那麼,咱們在使用unity3d開發東西的時候,腳本架構到底應該如何來寫?
呵呵...
其實,我也給不了大傢俱體答案,由於每一個人的開發習慣,每一個團隊的開發模式也各有千秋,
so,在此我只作幾種設計模式的總結,
主要參考書籍有《設計模式》《設計模式之禪》《大話設計模式》以及網上一些零散的文章,
但主要內容仍是我本人的一些經驗以及感悟。
寫出來的目的一方面是系統地整理一下,一方面也與廣大的網友分享,
至於大家到底如何使用,

望君斟酌啊!
由於設計模式對編程人員來講,的確很是重要。
固然,若是你們的理解跟我有所不一樣,歡迎留言,你們共同探討。


設計模式六大原則(1):單一職責原則
說到單一職責原則,不少人都會不屑一顧。
由於它太簡單了,稍有經驗的程序員即便歷來沒有讀過設計模式、歷來沒有據說過單一職責原則,在設計軟件時也會自覺的遵照這一重要原則,由於這是常識。
在軟件編程中,誰也不但願由於修改了一個功能致使其餘的功能發生故障。
而避免出現這一問題的方法即是遵循單一職責原則。
雖然單一職責原則如此簡單,而且被認爲是常識,可是即使是經驗豐富的程序員寫出的程序,也會有違背這一原則的代碼存在。
爲何會出現這種現象呢?由於有職責擴散。所謂職責擴散,就是由於某種緣由,職責被分化成了更細的職責。
如:用一個類描述動物呼吸這個場景
程序員

[C#]  純文本查看  複製代碼
?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
class Animal
{
     public void breathe( string animal)
     {
                 Debug.Log(animal+ "呼吸空氣" );
         }
}
public class Client
{
     Animal animal = new Animal();
     void Start()
     {
       animal.breathe( "牛" );
       animal.breathe( "羊" );
       animal.breathe( "豬" );
     }
}
運行結果:
牛呼吸空氣
羊呼吸空氣
豬呼吸空氣
程序上線後,發現問題了,並非全部的動物都呼吸空氣的,好比魚就是呼吸水的。
修改時若是遵循單一職責原則,須要將Animal類細分爲陸生動物類Terrestrial,水生動物Aquatic,代碼以下:
[C#]  純文本查看  複製代碼
?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Terrestrial
     {
         public void breathe(String animal){
             Debug.Log(animal + "呼吸空氣" );
         }
     }
     class Aquatic
     {
         public void breathe(String animal){
             Debug.Log(animal + "呼吸水" );
         }
     }
 
     public class Client
     {
         public static void main(String[] args)
         {
             Terrestrial terrestrial = new Terrestrial();
             Debug.Log(terrestrial.breathe( "牛" ));
            Debug.Log(terrestrial.breathe( "羊" ));
            Debug.Log(terrestrial.breathe( "豬" ));
 
             Aquatic aquatic = new Aquatic();
            Debug.Log( aquatic.breathe( "魚" ));
         }
     }


運行結果:
牛呼吸空氣
羊呼吸空氣
豬呼吸空氣
魚呼吸水
咱們會發現若是這樣修改花銷是很大的,除了將原來的類分解以外,還須要修改客戶端。
而直接修改類Animal來達成目的雖然違背了單一職責原則,但花銷卻小的多,代碼以下:

[C#]  純文本查看  複製代碼
?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Animal
    {
        public void breathe(String animal)
        {
                    if ( "魚" .equals(animal))
            {
                           Debug.Log((animal+ "呼吸水" ));
                    }
            else
            {
                            Debug.Log((animal+ "呼吸空氣" ));
                    }
            }
    }
 
    public class Client
    {
        public static void main(String[] args)
        {
            Animal animal = new Animal();
            Debug.Log(animal.breathe( "牛" ));
            Debug.Log(animal.breathe( "羊" ));
            Debug.Log(animal.breathe( "豬" ));
            Debug.Log(animal.breathe( "魚" ));
        }
    }


能夠看到,這種修改方式要簡單的多。
可是卻存在着隱患:有一天須要將魚分爲呼吸淡水的魚和呼吸海水的魚,
則又須要修改Animal類的breathe方法,而對原有代碼的修改會對調用「豬」「牛」「羊」等相關功能帶來風險,
也許某一天你會發現程序運行的結果變爲「牛呼吸水」了。
這種修改方式直接在代碼級別上違背了單一職責原則,雖然修改起來最簡單,但隱患倒是最大的。
還有一種修改方式:
[C#]  純文本查看  複製代碼
?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
class Animal
     {
         public void breathe(String animal){
                 Debug.Log(animal+ "呼吸空氣" );
         }
 
         public void breathe2(String animal){
                 Debug.Log(animal+ "呼吸水" );
         }
     }
 
     public class Client
     {
         public static void main(String[] args)
         {
             Animal animal = new Animal();
             Debug.Log(animal.breathe( "牛" ));
            Debug.Log(animal.breathe( "羊" ));
            Debug.Log(animal.breathe( "豬" ));
            Debug.Log(animal.breathe2( "魚" ));
         }
     }

能夠看到,這種修改方式沒有改動原來的方法,而是在類中新加了一個方法,這樣雖然也違背了單一職責原則,
但在方法級別上倒是符合單一職責原則的,由於它並無動原來方法的代碼。這三種方式各有優缺點,
那麼在實際編程中,採用哪一中呢?
其實這真的比較難說,須要根據實際狀況來肯定。
個人原則是:只有邏輯足夠簡單,才能夠在代碼級別上違反單一職責原則;只有類中方法數量足夠少,才能夠在方法級別上違反單一職責原則。

遵循單一職責原的優勢有:
  • 能夠下降類的複雜度,一個類只負責一項職責,其邏輯確定要比負責多項職責簡單的多;
  • 提升類的可讀性,提升系統的可維護性;
  • 變動引發的風險下降,變動是必然的,若是單一職責原則遵照的好,當修改一個功能時,能夠顯著下降對其餘功能的影響。

須要說明的一點是單一職責原則不僅是面向對象編程思想所特有的,只要是模塊化的程序設計,都適用單一職責原則。編程



設計模式六大原則(2):里氏替換原則

確定有很多人跟我剛看到這項原則的時候同樣,對這個原則的名字充滿疑惑。
其實緣由就是這項原則最先是在1988年,由麻省理工學院的一位姓裏的女士(Barbara Liskov)提出來的。
簡單來講的話,就是 當咱們使用繼承時,遵循里氏替換原則。
注:類B繼承類A時,除添加新的方法完成新增功外,儘可能不要重寫父類A的方法,也儘可能不要重載父類A的方法。


繼承包含這樣一層含義:父類中凡是已經實現好的方法(相對於抽象方法而言),其實是在設定一系列的規範和契約,
雖然它不強制要求全部的子類必須聽從這些契約,可是若是子類對這些非抽象方法任意修改,
就會對整個繼承體系形成破壞。而里氏替換原則就是表達了這一層含義。


繼承做爲面向對象三大特性之一,在給程序設計帶來巨大便利的同時,也帶來了弊端。
好比使用繼承會給程序帶來侵入性,程序的可移植性下降,增長了對象間的耦合性,若是一個類被其餘的類所繼承,
則當這個類須要修改時,必須考慮到全部的子類,而且父類修改後,
全部涉及到子類的功能都有可能會產生故障。
那就讓咱們一塊兒看看 繼承的風險,以下:
[C#]  純文本查看  複製代碼
?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
[/size][/font][/color]
[color=#000][font=宋體][size=13px] class A
     {
         public int func1( int a, int b)
         {
             return a - b;
         }
     }
 
     public class Client
     {
        void Start()
        {
                     A a = new A();
                     Debug.Log(( "100-50=" +a.func1(100, 50));
                     Debug.Log(( "100-80=" +a.func1(100, 80)));
             }
     }

運行結果:
100-50=50
100-80=20
後來,咱們須要增長一個新的功能:完成兩數相加,而後再與100求和,由類B來負責。
即類B須要完成兩個功能:
兩數相減。
兩數相加,而後再加100。
因爲類A已經實現了第一個功能,因此類B繼承類A後,只須要再完成第二個功能就能夠了,代碼以下
[C#]  純文本查看  複製代碼
?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
[/size][/font][/color]
[color=#000][font=宋體][size=2] class B:A{
         public int func1( int a, int b){
                 return a+b;
         }
         
         public int func2( int a, int b){
                 return func1(a,b)+100;
         }
}
 
public class Client{
          void Start()
      {
                 B b = new B();
                 Debug.Log( "100-50=" +b.func1(100, 50));
                 Debug.Log( "100-80=" +b.func1(100, 80));
                 Debug.Log( "100+20+100=" +b.func2(100, 20));
         }

類B完成後,運行結果:
100-50=150
100-80=180
100+20+100=220
咱們發現本來運行正常的相減功能發生了錯誤。
緣由就是類B在給方法起名時無心中重寫了父類的方法,形成全部運行相減功能的代碼所有調用了類B重寫後的方法,形成本來運行正常的功能出現了錯誤。
在本例中,引用基類A完成的功能,換成子類B以後,發生了異常。
在實際編程中,咱們經常會經過重寫父類的方法來完成新的功能,這樣寫起來雖然簡單,
可是整個繼承體系的可複用性會比較差,特別是運用多態比較頻繁時,程序運行出錯的概率很是大。
若是非要重寫父類的方法,比較通用的作法是:原來的父類和子類都繼承一個更通俗的基類,原有的繼承關係去掉,採用依賴、聚合,組合等關係代替。


里氏替換原則通俗的來說就是:子類能夠擴展父類的功能,但不能改變父類原有的功能。它包含如下4層含義:
1.子類能夠實現父類的抽象方法,但不能覆蓋父類的非抽象方法。
2.子類中能夠增長本身特有的方法。
3.當子類的方法重載父類的方法時,方法的前置條件(即方法的形參)要比父類方法的輸入參數更寬鬆。
4.當子類的方法實現父類的抽象方法時,方法的後置條件(即方法的返回值)要比父類更嚴格。


看上去很難以想象,由於咱們會發如今本身編程中經常會違反里氏替換原則,程序照樣跑的好好的。
因此你們都會產生這樣的疑問,假如我非要不遵循里氏替換原則會有什麼後果?
後果就是:你寫的代碼出問題的概率將會大大增長。
設計模式六大原則(3):依賴倒置原則
定義:高層模塊不該該依賴低層模塊,兩者都應該依賴其抽象; 抽象不該該依賴細節;細節應該依賴抽象。

本帖隱藏的內容

以抽象爲基礎搭建起來的架構比以細節爲基礎搭建起來的架構要穩定的多。
抽象指的是接口或者抽象類,細節就是具體的實現類,使用接口或者抽象類的目的是制定好規範和契約,而不去涉及任何具體的操做,把展示細節的任務交給他們的實現類去完成。


依賴倒置原則的核心思想是面向接口編程,咱們依舊用一個例子來講明面向接口編程比相對於面向實現編程好在什麼地方。
場景是這樣的,母親給孩子講故事,只要給她一本書,她就能夠照着書給孩子講故事了。代碼以下:
[C#]  純文本查看  複製代碼
?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
class Book{
         public String getContent(){
                 return "好久好久之前有一個阿拉伯的故事……" ;
         }
}
 
class Mother{
         public void narrate(Book book){
                 Debug.Log( "媽媽開始講故事" );
                 Debug.Log(book.getContent());
         }
}
 
public class Client{
         void Start()
     {
                 Mother mother = new Mother();
                 Debug.Log(mother.narrate( new Book()));
         }
}

運行結果:
媽媽開始講故事
好久好久之前有一個阿拉伯的故事……
運行良好,假若有一天,需求變成這樣:不是給書而是給一份報紙,讓這位母親講一下報紙上的故事,報紙的代碼以下:
[C#]  純文本查看  複製代碼
?
1
2
3
4
5
class Newspaper{
         public String getContent(){
                 return "林書豪38+7領導尼克斯擊敗湖人……" ;
         }
}

這位母親卻辦不到,由於她竟然不會讀報紙上的故事,這太荒唐了,只是將書換成報紙,竟然必需要修改Mother才能讀。
假如之後需求換成雜誌呢?換成網頁呢?
還要不斷地修改Mother,這顯然不是好的設計。
緣由就是Mother與Book之間的耦合性過高了,必須下降他們之間的耦合度才行。
咱們引入一個抽象的接口IReader。
讀物,只要是帶字的都屬於讀物:
[C#]  純文本查看  複製代碼
?
1
2
3
interface IReader{
         public String getContent();
}


Mother類與接口IReader發生依賴關係,而Book和Newspaper都屬於讀物的範疇,
他們各自都去實現IReader接口,這樣就符合依賴倒置原則了,代碼修改成:
[C#]  純文本查看  複製代碼
?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Newspaper : IReader {
         public String getContent(){
                 return "林書豪17+9助尼克斯擊敗老鷹……" ;
         }
}
class Book : IReader{
         public String getContent(){
                 return "好久好久之前有一個阿拉伯的故事……" ;
         }
}
 
class Mother{
         public void narrate(IReader reader){
                 Debug.Log( "媽媽開始講故事" );
                 Debug.Log(reader.getContent());
         }
}
 
public class Client{
         public static void main(String[] args){
                 Mother mother = new Mother();
                 Debug.Log(mother.narrate( new Book()));
                 Debug.Log(mother.narrate( new Newspaper()));
         }
}

運行結果:
媽媽開始講故事
好久好久之前有一個阿拉伯的故事……
媽媽開始講故事
林書豪17+9助尼克斯擊敗老鷹……
這樣修改後,不管之後怎樣擴展Client類,都不須要再修改Mother類了。
這只是一個簡單的例子,實際狀況中,表明高層模塊的Mother類將負責完成主要的業務邏輯,一旦須要對它進行修改,引入錯誤的風險極大。
因此遵循依賴倒置原則能夠下降類之間的耦合性,提升系統的穩定性,下降修改程序形成的風險。


採用依賴倒置原則給多人並行開發帶來了極大的便利,
好比上例中,本來Mother類與Book類直接耦合時,Mother類必須等Book類編碼完成後才能夠進行編碼,由於Mother類依賴於Book類。
修改後的程序則能夠同時開工,互不影響,由於Mother與Book類一點關係也沒有。
參與協做開發的人越多、項目越龐大,採用依賴致使原則的意義就越重大。
如今很流行的TDD開發模式就是依賴倒置原則最成功的應用。
在實際編程中,咱們通常須要作到以下3點:
1.低層模塊儘可能都要有抽象類或接口,或者二者都有。
2.變量的聲明類型儘可能是抽象類或接口。使用繼承時遵循里氏替換原則。
3.依賴倒置原則的核心就是要咱們面向接口編程,理解了面向接口編程,也就理解了依賴倒置。


設計模式六大原則(4):接口隔離原則
定義:客戶端不該該依賴它不須要的接口;一個類對另外一個類的依賴應該創建在最小的接口上。 
將臃腫的接口I拆分爲獨立的幾個接口,類A和類C分別與他們須要的接口創建依賴關係。也就是採用接口隔離原則。
舉例來講明接口隔離原則:


(圖1 未遵循接口隔離原則的設計)
這個圖的意思是:類A依賴接口I中的方法一、方法二、方法3,類B是對類A依賴的實現。
類C依賴接口I中的方法一、方法四、方法5,類D是對類C依賴的實現。
對於類B和類D來講,雖然他們都存在着用不到的方法(也就是圖中紅色字體標記的方法),但因爲實現了接口I,因此也必需要實現這些用不到的方法。
對類圖不熟悉的能夠參照程序代碼來理解,代碼以下:
[C#]  純文本查看  複製代碼
?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
interface I {
         public void method1();
         public void method2();
         public void method3();
         public void method4();
         public void method5();
}
 
class A{
         public void depend1(I i){
                 i.method1();
         }
         public void depend2(I i){
                 i.method2();
         }
         public void depend3(I i){
                 i.method3();
         }
}
 
class B : I{
         public void method1() {
                 Debug.Log( "類B實現接口I的方法1" );
         }
         public void method2() {
                 Debug.Log( "類B實現接口I的方法2" );
         }
         public void method3() {
                 Debug.Log( "類B實現接口I的方法3" );
         }
         //對於類B來講,method4和method5不是必需的,可是因爲接口A中有這兩個方法,
         //因此在實現過程當中即便這兩個方法的方法體爲空,也要將這兩個沒有做用的方法進行實現。
         public void method4() {}
         public void method5() {}
}
 
class C{
         public void depend1(I i){
                 i.method1();
         }
         public void depend2(I i){
                 i.method4();
         }
         public void depend3(I i){
                 i.method5();
         }
}
 
class D : I{
         public void method1() {
                 Debug.Log( "類D實現接口I的方法1" );
         }
         //對於類D來講,method2和method3不是必需的,可是因爲接口A中有這兩個方法,
         //因此在實現過程當中即便這兩個方法的方法體爲空,也要將這兩個沒有做用的方法進行實現。
         public void method2() {}
         public void method3() {}
 
         public void method4() {
                 Debug.Log( "類D實現接口I的方法4" );
         }
         public void method5() {
                 Debug.Log( "類D實現接口I的方法5" );
         }
}
 
public class Client{
          void Start(){
                 A a = new A();
                 Debug.Log(a.depend1( new B()));
                 Debug.Log(a.depend2( new B()));
                 Debug.Log(a.depend3( new B()));
                 
                 C c = new C();
                 Debug.Log(c.depend1( new D()));
                 Debug.Log(c.depend2( new D()));
                 Debug.Log(c.depend3( new D()));
         }
}


能夠看到,若是接口過於臃腫,只要接口中出現的方法,無論對依賴於它的類有沒有用處,實現類中都必須去實現這些方法,這顯然不是好的設計。
若是將這個設計修改成符合接口隔離原則,就必須對接口I進行拆分。
在這裏咱們將原有的接口I拆分爲三個接口,拆分後的設計如圖2所示:


(圖2 遵循接口隔離原則的設計)
照例貼出程序的代碼,供不熟悉類圖的朋友參考:
[C#]  純文本查看  複製代碼
?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
interface I1 {
         public void method1();
}
 
interface I2 {
         public void method2();
         public void method3();
}
 
interface I3 {
         public void method4();
         public void method5();
}
 
class A{
         public void depend1(I1 i){
                 i.method1();
         }
         public void depend2(I2 i){
                 i.method2();
         }
         public void depend3(I2 i){
                 i.method3();
         }
}
 
class B : I1, I2{
         public void method1() {
                 Debug.Log( "類B實現接口I1的方法1" );
         }
         public void method2() {
                 Debug.Log( "類B實現接口I2的方法2" );
         }
         public void method3() {
                 Debug.Log( "類B實現接口I2的方法3" );
         }
}
 
class C{
         public void depend1(I1 i){
                 i.method1();
         }
         public void depend2(I3 i){
                 i.method4();
         }
         public void depend3(I3 i){
                 i.method5();
         }
}
 
class D : I1, I3{
         public void method1() {
                 Debug.Log( "類D實現接口I1的方法1" );
         }
         public void method4() {
                 Debug.Log( "類D實現接口I3的方法4" );
         }
         public void method5() {
                 Debug.Log( "類D實現接口I3的方法5" );
         }
}

接口隔離原則的含義是:創建單一接口,不要創建龐大臃腫的接口,儘可能細化接口,接口中的方法儘可能少。
也就是說,咱們要爲各個類創建專用的接口,而不要試圖去創建一個很龐大的接口供全部依賴它的類去調用。
本文例子中,將一個龐大的接口變動爲3個專用的接口所採用的就是接口隔離原則。
在程序設計中,依賴幾個專用的接口要比依賴一個綜合的接口更靈活。
接口是設計時對外部設定的「契約」,經過分散定義多個接口,能夠預防外來變動的擴散,提升系統的靈活性和可維護性。


說到這裏,不少人會覺的接口隔離原則跟以前的單一職責原則很類似,其實否則。
其一,單一職責原則原注重的是職責;而接口隔離原則注重對接口依賴的隔離。
其二,單一職責原則主要是約束類,其次纔是接口和方法,它針對的是程序中的實現和細節;
而接口隔離原則主要約束接口接口,主要針對抽象,針對程序總體框架的構建。
採用接口隔離原則對接口進行約束時,要注意如下幾點:
1.接口儘可能小,可是要有限度。對接口進行細化能夠提升程序設計靈活性是不掙的事實,可是若是太小,則會形成接口數量過多,使設計複雜化。因此必定要適度。
2.爲依賴接口的類定製服務,只暴露給調用的類它須要的方法,它不須要的方法則隱藏起來。只有專一地爲一個模塊提供定製服務,才能創建最小的依賴關係。
3.提升內聚,減小對外交互。使接口用最少的方法去完成最多的事情。


運用接口隔離原則,必定要適度,接口設計的過大或太小都很差。設計接口的時候,只有多花些時間去思考和籌劃,才能準確地實踐這一原則。

設計模式六大原則(5):迪米特法則
定義:一個對象應該對其餘對象保持最少的瞭解。
類與類之間的關係越密切,耦合度越大,當一個類發生改變時,對另外一個類的影響也越大。
所以,儘可能下降類與類之間的耦合。


自從咱們接觸編程開始,就知道了軟件編程的總的原則:低耦合,高內聚。
不管是面向過程編程仍是面向對象編程,只有使各個模塊之間的耦合儘可能的低,才能提升代碼的複用率。
低耦合的優勢不言而喻,可是怎麼樣編程才能作到低耦合呢?那正是迪米特法則要去完成的。


迪米特法則又叫最少知道原則,最先是在1987年由美國Northeastern University的Ian Holland提出。
通俗的來說,就是一個類對本身依賴的類知道的越少越好。也就是說,對於被依賴的類來講,不管邏輯多麼複雜,都儘可能地的將邏輯封裝在類的內部,對外除了提供的public方法,不對外泄漏任何信息。
迪米特法則還有一個更簡單的定義:只與直接的朋友通訊。首先來解釋一下什麼是直接的朋友:
每一個對象都會與其餘對象有耦合關係,只要兩個對象之間有耦合關係,咱們就說這兩個對象之間是朋友關係。
耦合的方式不少,依賴、關聯、組合、聚合等。其中,咱們稱出現成員變量、方法參數、方法返回值中的類爲直接的朋友,
而出如今局部變量中的類則不是直接的朋友。也就是說,陌生的類最好不要做爲局部變量的形式出如今類的內部。
舉一個例子:有一個集團公司,下屬單位有分公司和直屬部門,如今要求打印出全部下屬單位的員工ID。
先來看一下違反迪米特法則的設計。
[C#]  純文本查看  複製代碼
?
01
02
03
04
05
06
07
08
09
10
11
12
相關文章
相關標籤/搜索