Unity3d 一个优秀的程序必备的几种设计模式

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());
        }
    }
}

主要的变化如下图所示:

修改后,为分公司增加了打印人员ID的方法,总公司直接调用来打印,从而避免了与分公司的员工发生耦合。

迪米特法则的初衷是降低类之间的耦合,由于每个类都减少了不必要的依赖,因此的确可以降低耦合关系。

但是凡事都有度,虽然可以避免与非直接的类通信,但是要通信,必然会通过一个“中介”来发生联系,例如本例中,

总公司就是通过分公司这个“中介”来与分公司的员工发生联系的。

过分的使用迪米特原则,会产生大量这样的中介和传递类,导致系统复杂度变大。

所以在采用迪米特法则时要反复权衡,既做到结构清晰,又要高内聚低耦合。

 

原则6:开闭原则

定义:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。

在软件的生命周期内,因为变化、升级和维护等原因需要对软件原有代码进行修改时,

可能会给旧代码中引入错误,也可能会使我们不得不对整个功能进行重构,并且需要原有代码经过重新测试。

因此,当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。

闭原则是面向对象设计中最基础的设计原则,它指导我们如何建立稳定灵活的系统。开闭原则可能是设计模式六项原则中定义最模糊的一个了,

它只告诉我们对扩展开放,对修改关闭,可是到底如何才能做到对扩展开放,对修改关闭,并没有明确的告诉我们。

以前,如果有人告诉我“你进行设计的时候一定要遵守开闭原则”,我会觉的他什么都没说,但貌似又什么都说了。因为开闭原则真的太虚了。

在仔细思考以及仔细阅读很多设计模式的文章后,终于对开闭原则有了一点认识。

其实,我们遵循设计模式前面5大原则,以及使用23种设计模式的目的就是遵循开闭原则。

也就是说,只要我们对前面5项原则遵守的好了,设计出的软件自然是符合开闭原则的,这个开闭原则更像是前面五项原则遵守程度的“平均得分”,

前面5项原则遵守的好,平均分自然就高,说明软件设计开闭原则遵守的好;

如果前面5项原则遵守的不好,则说明开闭原则遵守的不好。

其实,开闭原则无非就是想表达这样一层意思:用抽象构建框架,用实现扩展细节。

因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保持软件架构的稳定。

而软件中易变的细节,我们用从抽象派生的实现类来进行扩展,当软件需要发生变化时,我们只需要根据需求重新派生一个实现类来扩展就可以了。

当然前提是我们的抽象要合理,要对需求的变更有前瞻性和预见性才行。

 

 

最后说明一下如何去遵守这六个原则。

对这六个原则的遵守并不是是和否的问题,而是多和少的问题,也就是说,我们一般不会说有没有遵守,而是说遵守程度的多少。

任何事都是过犹不及,设计模式的六个设计原则也是一样,制定这六个原则的目的并不是要我们刻板的遵守他们,而需要根据实际情况灵活运用。

对他们的遵守程度只要在一个合理的范围内,就算是良好的设计。

如果大家对这六项原则的理解跟我有所不同,欢迎指正

本文转载自:[ζ?随风去旅行] Unity3d 一个优秀的程序必备的几种设计模式

时间: 2024-10-16 08:12:51

Unity3d 一个优秀的程序必备的几种设计模式的相关文章

如何成为一个优秀的程序员(大部分写的都非常不错)——要勤快,不停的学习;争论是无意义的,不如把两种都学了,会更有收获。

在看这份FAQ之前,你必须先读完ESR的<如何成为一名黑客>(本文末尾附有中译版),该文所讲过的道理我不再重复.该文用很大的篇幅讲什么是黑客文化,虽然态度和信仰的确是成功的最关键因素,但是你肯定会想,做不做黑客跟我没关系,我只是想学个一技之长,找个好工作而已,对吧?那么肯定更希望获得一些具体的可操作的指导.     1.各种软件技术之间是怎样的关系? 我把软件技术分为三个层次:    问题域:计算机图形学.音视频编码.信息安全.模式识别.信息检索.自然语言分析.人工智能.科学计算等:     

学习Axure的心得&amp;关于如何做一个优秀的程序员

这些天学习了Axure的一些基础知识,感觉自己就只是刚刚入门,还有好多东西不懂.本想在网上好好地学一些更高的操作,却找不找比较好的学习视频.网上的资料太多,而且质量参差不齐.(视频画面根本就看不清)到后来就找了一些文本资料来看,但还是未能坚持看下去.不过,我看了一些关于程序员的资料. 下面就来说说我的看法. 一 努力刻苦地编程.好的程序员是后天炼成的,是靠一行行代码和一个个算法给喂出来的.因此,只要努力就一定能成为一个优秀的程序员. 二 拥有过硬的技术和本事.要明白,在IT行业,靠的不是关系,地

一个优秀的程序员所具备的14个品质【转载】

慎于说Yes 在没有搞清楚开发需求.任务工作量.团队期望值之前,有前途的程序员不会轻易答应.特别是对于新人来说,比较急于表现自己,对于同事或者老板的工作安排来者不拒,精神可嘉,方法不可取.承诺太多,会出现自己无法按时完成的情况,即使按时完成,也可能没有达到别人的期望值,长期以往别人对你的失望会越来越多. 敢于说No 看起来和第一条雷同,其实细究起来不一样.第二条执行起来比第一条难得多.当遇到明显不合理的要求时,可以考虑拒绝,前提是准备好了全面充分的理由.不要经常说No,但是一旦说了,就要切中要害

【知乎】怎么成为一个优秀的程序员,而不是一个优秀的码农?

怎么成为一个优秀的程序员,而不是一个优秀的码农? 9 条评论 分享 默认排序按时间排序 98 个回答 3844赞同反对,不会显示你的姓名 萧井陌 微信公众号:炼瓜研究所 技术社区 - 3844 人赞同 优秀的程序员会告诉你打根基的重要性,会劝你在厚积薄发前要隐忍. 优秀的码农会告诉你学啥底层.啥啥啥一拖就好了,学了python还要啥自行车啊,数据结构排序函数二分搜索这不都内置了吗?工作中永远用不到,学算法有啥用啊?成为高手有很多种方法汇编是个屁啊? +++基础的分割线+++ 列举几个我认为比较重

互联网创业:如何雇佣一个优秀的程序员?

经常有人问我:“你在哪里可以找到或者聘请到优秀的程序员?”因为个人就是一名互联网项目经理,经常做项目管理,和程序员打交道的时间非常多,所以了解很多程序员的心态和习性,当然这里面也有很多的小技巧,下面就简单介绍一下我是怎么做的.下文提到的他,泛指程序员和程序媛. 问一些有过项目开发的朋友: 发现人才的最佳方式是通过别人的参考.曾经做过网站建设和软件开发以及APP制作的朋友,他们一定和优秀的程序员打过交道,他肯定会推荐一个好的程序员给你.建议有朋友如果在外包公司的话,那最好不过了.首先,你不必担心筛

谈谈一个优秀的程序员是如何炼成的

一个优秀的程序员是如何炼成的?他们关注什么?问一下自己这个问题会很有意思.这能让你好好思考一下软件开发的过程.拿它来问一下同事也不错,这能引起一些关于如何一起协作的很有意思的讨论.下面是我认为成为一名优秀的程序员所必需的5个技能. 问题分解 编程讲的是如何解决问题.但在你开始写代码之前,你需要清楚如何解决问题.一个好的程序员应该得拥有这样的技能,他能将问题分解成子问题,直到每个子问题都可以很轻松地进行解决.不过要找到一个解决问题的方式可不是件简单的事.好的程序员能够很好的对问题进行建模,这样写出

万众创新:你是一个优秀的程序员吗?

程序员经常为自己的工作和职业而感到自豪,一些软件开发公司甚至觉得优雅的编写代码是一种艺术形式,因为,很多人自称自己为“软件工匠”.但是我相信,写代码只是一个正常的职业,但在很多软件开发的情况下,程序员会抄袭别人的代码,如果每个人都可以看到你写的代码,你觉得还有些代码的必要吗? 所以,在万众创新的大环境下必须要反思:你是一个优秀的程序员吗?你是高于平均水平的软件开发人员?也许你是在排名前20%,甚至最高的10%? 我几乎可以肯定,很多程序员的答案是,高于平均水平.大多数程序员这样的感觉.但是,当然

如何才能造就一个优秀的程序员

一切就像刚刚发生过一样.那是在2000年,Pargas博士正在给我们上数据结构课,当他谈及某些数据结构的时候,他在墙上投影了一个SSH会话.在回答完某位同学提出的问题后,他随后总结了一句,“如果大家想成为一个真正的程序员,那么你需要从现在就开始学习使用vi文本编辑器”.我仍然记得他说这话的时候,脸上带着真挚的笑容,但是事后却证明他的说话是不甚正确的.不过,那个时候我还年轻,很容易受外界影响.所以,下课后我一回到宿舍,就迫不及待地开始学习如何使用vi编辑器了.这样一段时间之后,我突然发现我对vi真

转:如何成为一个优秀的程序员

如何成为一个优秀的程序员 导读:本文是由作者Jason Rudolph发表的一篇博文<Programming Achievements: How to Level Up as a Developer>翻译而来. 文章内容如下: 一个优秀的程序员如何能练就成一个卓越的程序员? 先等等,暂时不谈卓越,先谈谈如何成为一个优秀的程序员. 从第一步到第N步并没有明确的路线图.事实上,第N步到底是什么还不确定.就像逻辑型思维的开发者一样,缺乏明确的目标会让从入门到精通的道路变得异常崎岖. 过去的几年中,我