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
相关文章
相关标签/搜索