一個優秀的Unity3d開發者必備的幾種設計模式


Unity腳本編程

眾所周知,unity的編程屬於腳本化,腳本沒有一個具體的概念跟架構, 導致在項目過程中,經常出現哪里需要實現什么功能,就隨便添加腳本,
結果,就造成了一片混亂,不好管理。


更有甚者,自己的寫的代碼閑置一段時間后,再去想找某個功能的實現,都要在視圖中翻來覆去找半天。
哎!請容許我在此感嘆一聲,這還是你寫的東西么?
因此,一個好的設計模式是多么的重要啊,

如何寫腳本架構

那么,我們在使用unity3d開發東西的時候,腳本架構到底應該如何來寫?
呵呵...


其實,我也給不了你們具體答案,因為每個人的開發習慣,每個團隊的開發模式也各有千秋,
so,在此我只做幾種設計模式的總結,

參考書籍

主要參考書籍有《設計模式》《設計模式之禪》《大話設計模式》以及網上一些零散的文章,
但主要內容還是我本人的一些經驗以及感悟。
寫出來的目的一方面是系統地整理一下,一方面也與廣大的網友分享,
至於你們到底如何使用,
望君斟酌啊!

設計模式

設計模式對編程人員來說,的確非常重要。
當然,如果大家的理解跟我有所不同,歡迎留言,大家共同探討。

  • 原則1:單一職責
  • 原則2:里氏替換原則(子類擴展但不改變父類功能)
  • 原則3:依賴倒置原則
  • 原則4:接口隔離原則
  • 原則5:迪米特法則(最少知道原則)
  • 原則6:開閉原則

    原則1:單一職責原則

    說到單一職責原則,很多人都會不屑一顧。
    因為它太簡單了,稍有經驗的程序員即使從來沒有讀過設計模式、從來沒有聽說過單一職責原則,在設計軟件時也會自覺的遵守這一重要原則,因為這是常識。
    在軟件編程中,誰也不希望因為修改了一個功能導致其他的功能發生故障。
    而避免出現這一問題的方法便是遵循單一職責原則。
    雖然單一職責原則如此簡單,並且被認為是常識,但是即便是經驗豐富的程序員寫出的程序,也會有違背這一原則的代碼存在。
    為什么會出現這種現象呢?因為有職責擴散。所謂職責擴散,就是因為某種原因,職責被分化成了更細的職責。

    用一個類描述動物呼吸這個場景

    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,代碼如下:

    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();
            terrestrial.breathe("");
            terrestrial.breathe("");
            terrestrial.breathe("");
    
            Aquatic aquatic = new Aquatic();
            aquatic.breathe("");
        }
    }
    
    //運行結果:
        //牛呼吸空氣
        //羊呼吸空氣
        //豬呼吸空氣
        //魚呼吸水

    改動量小的方法

    我們會發現如果這樣修改花銷是很大的,除了將原來的類分解之外,還需要修改客戶端。
    而直接修改類Animal來達成目的雖然違背了單一職責原則,但花銷卻小的多,代碼如下:

    class Animal
    {
        public void breathe(String animal)
        {
            if ("" == animal)
            {
                Debug.Log((animal + "呼吸水"));
            }
            else
            {
                Debug.Log((animal + "呼吸空氣"));
            }
        }
    }
    
    public class Client
    {
        public static void main(String[] args)
        {
            Animal animal = new Animal();
            animal.breathe("");
            animal.breathe("");
            animal.breathe("");
            animal.breathe("");
        }
    }

    隱患

    可以看到,這種修改方式要簡單的多。
    但是卻存在着隱患:有一天需要將魚分為呼吸淡水的魚和呼吸海水的魚,
    則又需要修改Animal類的breathe方法,而對原有代碼的修改會對調用“豬”“牛”“羊”等相關功能帶來風險,
    也許某一天你會發現程序運行的結果變為“牛呼吸水”了。
    這種修改方式直接在代碼級別上違背了單一職責原則,雖然修改起來最簡單,但隱患卻是最大的。

    另一種修改方式

    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();
            animal.breathe("");
            animal.breathe("");
            animal.breathe("");
            animal.breathe2("");
        }
    }

    可以看到,這種修改方式沒有改動原來的方法,而是在類中新加了一個方法,這樣雖然也違背了單一職責原則,
    但在方法級別上卻是符合單一職責原則的,因為它並沒有動原來方法的代碼。這三種方式各有優缺點,
    那么在實際編程中,采用哪一中呢?
    其實這真的比較難說,需要根據實際情況來確定。
    我的原則是:只有邏輯足夠簡單,才可以在代碼級別上違反單一職責原則;只有類中方法數量足夠少,才可以在方法級別上違反單一職責原則。

    遵循單一職責原的優點有

  • 可以降低類的復雜度,一個類只負責一項職責,其邏輯肯定要比負責多項職責簡單的多;
  • 提高類的可讀性,提高系統的可維護性;
  • 變更引起的風險降低,變更是必然的,如果單一職責原則遵守的好,當修改一個功能時,可以顯著降低對其他功能的影響。

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

    原則2:里氏替換原則

    名字的由來

    肯定有不少人跟我剛看到這項原則的時候一樣,對這個原則的名字充滿疑惑。
    其實原因就是這項原則最早是在1988年,由麻省理工學院的一位姓里的女士(Barbara Liskov)提出來的。
    簡單來說的話,就是當我們使用繼承時,遵循里氏替換原則。

    定義

    注:類B繼承類A時,除添加新的方法完成新增功外,盡量不要重寫父類A的方法,也盡量不要重載父類A的方法。
    繼承包含這樣一層含義:父類中凡是已經實現好的方法(相對於抽象方法而言),實際上是在設定一系列的規范和契約,
    雖然它不強制要求所有的子類必須遵從這些契約,但是如果子類對這些非抽象方法任意修改,
    就會對整個繼承體系造成破壞。而里氏替換原則就是表達了這一層含義。
    繼承作為面向對象三大特性之一,在給程序設計帶來巨大便利的同時,也帶來了弊端。
    比如使用繼承會給程序帶來侵入性,程序的可移植性降低,增加了對象間的耦合性,如果一個類被其他的類所繼承,
    則當這個類需要修改時,必須考慮到所有的子類,並且父類修改后,
    所有涉及到子類的功能都有可能會產生故障。

    繼承的風險

    那就讓我們一起看看繼承的風險,如下:

    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后,只需要再完成第二個功能就可以了,代碼如下

    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
    {
        private 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:依賴倒置原則

    定義

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

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

    依賴倒置原則核心思想

    依賴倒置原則的核心思想是面向接口編程,我們依舊用一個例子來說明面向接口編程比相對於面向實現編程好在什么地方。

    情景舉例

    場景是這樣的,母親給孩子講故事,只要給她一本書,她就可以照着書給孩子講故事了。代碼如下:

    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();
            mother.narrate(new Book());
        }
    }

    運行結果:

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

    需求變動

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

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

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

    抽象的接口

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

    interface IReader
    {
        String getContent();
    }

    Mother類與接口IReader發生依賴關系,而Book和Newspaper都屬於讀物的范疇,
    他們各自都去實現IReader接口,這樣就符合依賴倒置原則了,代碼修改為:

    interface IReader
    {
        String getContent();
    }
    
    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();
            mother.narrate(new Book());
            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中的方法1、方法2、方法3,類B是對類A依賴的實現。
    類C依賴接口I中的方法1、方法4、方法5,類D是對類C依賴的實現。
    對於類B和類D來說,雖然他們都存在着用不到的方法(也就是圖中紅色字體標記的方法),但由於實現了接口I,所以也必須要實現這些用不到的方法。

    示例代碼

    對類圖不熟悉的可以參照程序代碼來理解,代碼如下:

    //接口
    interface I
    {
        void method1();
        void method2();
        void method3();
        void method4();
        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();
            a.depend1(new B());
           a.depend2(new B());
           a.depend3(new B());
    
            C c = new C();
          c.depend1(new D()));
            c.depend2(new D());
            c.depend3(new D());
        }
    }

    可以看到,如果接口過於臃腫,只要接口中出現的方法,不管對依賴於它的類有沒有用處,實現類中都必須去實現這些方法,這顯然不是好的設計。
    如果將這個設計修改為符合接口隔離原則,就必須對接口I進行拆分。

    遵循接口隔離原則的設計

    在這里我們將原有的接口I拆分為三個接口,拆分后的設計如圖2所示:

    類圖2

    示例代碼

    照例貼出程序的代碼,供不熟悉類圖的朋友參考:

    interface I1
    {
         void method1();
    }
    
    interface I2
    {
         void method2();
         void method3();
    }
    
    interface I3
    {
         void method4();
         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。
    先來看一下違反迪米特法則的設計。

    //總公司員工
    class Employee
    {
        private String id;
        public void setId(String id)
        {
            this.id = id;
        }
        public String getId()
        {
            return id;
        }
    }
    
    //分公司員工
    class SubEmployee
    {
        private String id;
        public void setId(String id)
        {
            this.id = id;
        }
        public String getId()
        {
            return id;
        }
    }
    
    class SubCompanyManager
    {
        public List<SubEmployee> getAllEmployee()
        {
            List<SubEmployee> list = new List<SubEmployee>();
            for (int i = 0; i < 100; i++)
            {
                SubEmployee emp = new SubEmployee();
                //為分公司人員按順序分配一個ID
                emp.setId("分公司" + i);
                list.Add(emp);
            }
            return list;
        }
    }
    
    class CompanyManager
    {
    
        public List<Employee> getAllEmployee()
        {
            List<Employee> list = new List<Employee>();
            for (int i = 0; i < 30; i++)
            {
                Employee emp = new Employee();
                //為總公司人員按順序分配一個ID
                emp.setId("總公司" + i);
                list.Add(emp);
            }
            return list;
        }
    
        public void printAllEmployee(SubCompanyManager sub)
        {
            List<SubEmployee> list1 = sub.getAllEmployee();
            foreach (SubEmployee e in list1)
            {
                Debug.Log(e.getId());
            }
    
    
            List<Employee> list2 = this.getAllEmployee();
            foreach (Employee e in list2)
            {
                Debug.Log(e.getId());
            }
        }
    }
    
    public class Client
    {
        void Start()
        {
            CompanyManager e = new CompanyManager();
            e.printAllEmployee(new SubCompanyManager());
        }
    }

    現在這個設計的主要問題出在CompanyManager中,根據迪米特法則,只與直接的朋友發生通信,
    而SubEmployee類並不是CompanyManager類的直接朋友(以局部變量出現的耦合不屬於直接朋友),從邏輯上講總公司只與他的分公司耦合就行了,
    與分公司的員工並沒有任何聯系,這樣設計顯然是增加了不必要的耦合。

    修改后的代碼

    按照迪米特法則,應該避免類中出現這樣非直接朋友關系的耦合。修改后的代碼如下:

    class SubCompanyManager
    {
        public List<SubEmployee> getAllEmployee()
        {
            List<SubEmployee> list = new List<SubEmployee>();
            for (int i = 0; i < 100; i++)
            {
                SubEmployee emp = new SubEmployee();
                //為分公司人員按順序分配一個ID
                emp.setId("分公司" + i);
                list.Add(emp);
            }
            return list;
        }
        public void printEmployee()
        {
            List<SubEmployee> list = this.getAllEmployee();
            foreach (SubEmployee e in list)
            {
                Debug.Log(e.getId());
            }
        }
    }
    
    class CompanyManager
    {
        public List<Employee> getAllEmployee()
        {
            List<Employee> list = new List<Employee>();
            for (int i = 0; i < 30; i++)
            {
                Employee emp = new Employee();
                //為總公司人員按順序分配一個ID
                emp.setId("總公司" + i);
                list.Add(emp);
            }
            return list;
        }
    
        public void printAllEmployee(SubCompanyManager sub)
        {
            sub.printEmployee();
            List<Employee> list2 = this.getAllEmployee();
            foreach (Employee e in list2)
            {
                Debug.Log(e.getId());
            }
        }
    }

    主要的變化

    image

    修改后,為分公司增加了打印人員ID的方法,總公司直接調用來打印,從而避免了與分公司的員工發生耦合。

    總結

    迪米特法則的初衷是降低類之間的耦合,由於每個類都減少了不必要的依賴,因此的確可以降低耦合關系。
    但是凡事都有度,雖然可以避免與非直接的類通信,但是要通信,必然會通過一個“中介”來發生聯系,例如本例中,
    總公司就是通過分公司這個“中介”來與分公司的員工發生聯系的。
    過分的使用迪米特原則,會產生大量這樣的中介和傳遞類,導致系統復雜度變大。
    所以在采用迪米特法則時要反復權衡,既做到結構清晰,又要高內聚低耦合。

     

    原則6:開閉原則

    定義

    一個軟件實體如類、模塊和函數應該對擴展開放,對修改關閉

    在軟件的生命周期內,因為變化、升級和維護等原因需要對軟件原有代碼進行修改時,
    可能會給舊代碼中引入錯誤,也可能會使我們不得不對整個功能進行重構,並且需要原有代碼經過重新測試。
    因此,當軟件需要變化時,盡量通過擴展軟件實體的行為來實現變化,而不是通過修改已有的代碼來實現變化。
    閉原則是面向對象設計中最基礎的設計原則,它指導我們如何建立穩定靈活的系統。開閉原則可能是設計模式六項原則中定義最模糊的一個了,

    它只告訴我們對擴展開放,對修改關閉,可是到底如何才能做到對擴展開放,對修改關閉,並沒有明確的告訴我們。
    以前,如果有人告訴我“你進行設計的時候一定要遵守開閉原則”,我會覺的他什么都沒說,但貌似又什么都說了。因為開閉原則真的太虛了。
    在仔細思考以及仔細閱讀很多設計模式的文章后,終於對開閉原則有了一點認識。
    其實,我們遵循設計模式前面5大原則,以及使用23種設計模式的目的就是遵循開閉原則。

    如何遵守

    也就是說,只要我們對前面5項原則遵守的好了,設計出的軟件自然是符合開閉原則的,這個開閉原則更像是前面五項原則遵守程度的“平均得分”,
    前面5項原則遵守的好,平均分自然就高,說明軟件設計開閉原則遵守的好;
    如果前面5項原則遵守的不好,則說明開閉原則遵守的不好。
    其實,開閉原則無非就是想表達這樣一層意思:用抽象構建框架,用實現擴展細節。
    因為抽象靈活性好,適應性廣,只要抽象的合理,可以基本保持軟件架構的穩定。
    而軟件中易變的細節,我們用從抽象派生的實現類來進行擴展,當軟件需要發生變化時,我們只需要根據需求重新派生一個實現類來擴展就可以了。
    當然前提是我們的抽象要合理,要對需求的變更有前瞻性和預見性才行。

     

    如何去遵守這六個原則

    對這六個原則的遵守並不是 是和否的問題,而是多和少的問題,也就是說,我們一般不會說有沒有遵守,而是說遵守程度的多少。
    任何事都是過猶不及,設計模式的六個設計原則也是一樣,制定這六個原則的目的並不是要我們刻板的遵守他們,而需要根據實際情況靈活運用。
    對他們的遵守程度只要在一個合理的范圍內,就算是良好的設計。
    如果大家對這六項原則的理解跟我有所不同,歡迎指正

    說明

    本文轉載自並對其整理目錄:[ζއ隨風去旅行] Unity3d 一個優秀的程序必備的幾種設計模式


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM