征服Unity3dUnity技术分享unity3D技术分享

unity框架设计原则

2017-12-20  本文已影响235人  半闲书屋半闲人

Unity脚本编程

众所周知,unity的编程属于脚本化,脚本没有一个具体的概念跟架构, 导致在项目过程中,经常出现哪里需要实现什么功能,就随便添加脚本,
结果,就造成了一片混乱,不好管理。
更有甚者,自己的写的代码闲置一段时间后,再去想找某个功能的实现,都要在视图中翻来覆去找半天。
哎!请容许我在此感叹一声,这还是你写的东西么?
因此,一个好的设计模式是多么的重要啊。

如何写脚本架构

那么,我们在使用unity3d开发东西的时候,脚本架构到底应该如何来写?
呵呵...
其实,我也给不了你们具体答案,因为每个人的开发习惯,每个团队的开发模式也各有千秋,
so,在此我只做几种设计模式的总结,

参考书籍

主要参考书籍有《设计模式》《设计模式之禅》《大话设计模式》以及网上一些零散的文章,
但主要内容还是我本人的一些经验以及感悟。
写出来的目的一方面是系统地整理一下,一方面也与广大的网友分享,
至于你们到底如何使用,
望君斟酌啊!

设计模式

设计模式对编程人员来说,的确非常重要。
当然,如果大家的理解跟我有所不同,欢迎留言,大家共同探讨。
•原则1:单一职责
•原则2:里氏替换原则(子类扩展但不改变父类功能)
•原则3:依赖倒置原则
•原则4:接口隔离原则
•原则5:迪米特法则(最少知道原则)
•原则6:开闭原则

说到单一职责原则,很多人都会不屑一顾。
因为它太简单了,稍有经验的程序员即使从来没有读过设计模式、从来没有听说过单一职责原则,在设计软件时也会自觉的遵守这一重要原则,因为这是常识。
在软件编程中,谁也不希望因为修改了一个功能导致其他的功能发生故障。
而避免出现这一问题的方法便是遵循单一职责原则。
虽然单一职责原则如此简单,并且被认为是常识,但是即便是经验丰富的程序员写出的程序,也会有违背这一原则的代码存在。
为什么会出现这种现象呢?因为有职责扩散。所谓职责扩散,就是因为某种原因,职责被分化成了更细的职责。

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

可以看到,这种修改方式没有改动原来的方法,而是在类中新加了一个方法,这样虽然也违背了单一职责原则,
但在方法级别上却是符合单一职责原则的,因为它并没有动原来方法的代码。这三种方式各有优缺点,
那么在实际编程中,采用哪一中呢?
其实这真的比较难说,需要根据实际情况来确定。
我的原则是:只有逻辑足够简单,才可以在代码级别上违反单一职责原则;只有类中方法数量足够少,才可以在方法级别上违反单一职责原则。

•可以降低类的复杂度,一个类只负责一项职责,其逻辑肯定要比负责多项职责简单的多;
•提高类的可读性,提高系统的可维护性;
•变更引起的风险降低,变更是必然的,如果单一职责原则遵守的好,当修改一个功能时,可以显著降低对其他功能的影响。
需要说明的一点是单一职责原则不只是面向对象编程思想所特有的,只要是模块化的程序设计,都适用单一职责原则

肯定有不少人跟我刚看到这项原则的时候一样,对这个原则的名字充满疑惑。
其实原因就是这项原则最早是在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));
    }
}

100-50=150
100-80=180
100+20+100=220

我们发现原本运行正常的相减功能发生了错误。
原因就是类B在给方法起名时无意中重写了父类的方法,造成所有运行相减功能的代码全部调用了类B重写后的方法,造成原本运行正常的功能出现了错误。
在本例中,引用基类A完成的功能,换成子类B之后,发生了异常。
在实际编程中,我们常常会通过重写父类的方法来完成新的功能,这样写起来虽然简单,
但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的几率非常大。
如果非要重写父类的方法,比较通用的做法是:原来的父类和子类都继承一个更通俗的基类,原有的继承关系去掉,采用依赖、聚合,组合等关系代替。

子类可以扩展父类的功能,但不能改变父类原有的功能。它包含以下4层含义:
1.子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
2.子类中可以增加自己特有的方法。
3.当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
4.当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。

看上去很不可思议,因为我们会发现在自己编程中常常会违反里氏替换原则,程序照样跑的好好的。
所以大家都会产生这样的疑问,假如我非要不遵循里氏替换原则会有什么后果?
后果就是:你写的代码出问题的几率将会大大增加。

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领导尼克斯击败湖人……";
    }
}
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());
    }
}
未遵守接口分离的设计示意图.jpg
//接口
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拆分为三个接口,拆分后的设计如图2所示:


遵循接口分离的设计示意图.jpg
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");
    }
}
//总公司员工
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());
    }
}
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());
        }
    }
}
主要变化示意图.png
上一篇 下一篇

猜你喜欢

热点阅读