[Unity设计模式与游戏开发]工厂模式

前言

工厂模式(Factory Design Pattern)也是游戏开发中比较常用的创建型模式,一般情况下,工厂模式分为三种更加细分的类型:简单工厂、工厂方法和抽象工厂。在GoF设计模式一书中,它将简单工厂模式看做是工厂方法模式的一种特例,所以工厂模式只被分成了工厂方法和抽象工厂两类。举个例子在游戏开发中哪儿用到了工厂模式,我随意看到了FairyGUI中创建UI组件相关的源码就用到了简单工厂模式,至于怎么用的我们下面在分析,在比如资源加载Resources.Load也是用到了工厂方法模式,根据我们需要创建的资源类型来创建对应的实例,在比如我们自己开发的游戏功能相关的,举例:比如创建武器对象,就拿实际的枪而言,枪分很多种类,例如pistol、步枪、机枪等等,其中每种类型又具体细分,就拿pistol举例,相比我们看过《亮剑》电视剧,其中有这么一段剧情,秀琴从xiao gui zi手中缴获一只pistol,李云龙满是惊喜,介绍说这是王八盒子,pistol还有其他类型,我也是百度的,鸡腿橹子、加拿大橹子、美国柯尔特等等,当我们创建不同的具体的武器的时候就需要用到工厂模式,其实对于设计模式而言,网上有太多太多的类似的文章,但跟游戏开发相关的却很少,都是拿一些鸟、菜系等简单的例子举例,如果对于编程内功不强的初学者而言只是学到了表面,不会深刻理解的,因为这就是我的心路历程,所以我决定这次系列的设计模式教程我拿我这些年实战项目经验来举例讲解,这也能够应对面试过程中面试官的一个问题"谈一谈你实际工作中用到的设计模式",当面试官这么问的时候或许你会突然蒙逼,脑海中还是停留在网上那些简单的萝卜青菜的案例之中,无法很好的结合自己实战项目来谈,这样就不能体现你跟初级开发者之间的差异。

简单工厂模式

基本介绍

  • 简单工厂模式属于创建型模式,是工厂模式的一种。简单工厂模式是由一个工厂对象决定创建出哪一种产品类的实例。简单工厂模式是工厂模式家族中最简单实用的模式。
  • 简单工厂模式:定义了一个创建对象的类,由这个类来封装实例化对象的行为
  • 在软件开发中,当我们会大量的创建某种、某类或者某批对象时,就会用到工厂模式。

Unity中哪些地方使用到了简单工厂

我们最常用的Resource.Load接口就是典型的应用了简单工厂方法模式,我们需要加载各种不同的资源的时候,只需要通过这个接口传递泛型参数,告知接口我们需要加载的对象的类型,然后接口内部就根据我们传入的类型和路径去创建一个这样的对象给我们。

例如,我们加载一个GameObject,是这样加载的Resources.Load(“XXX”);其实它的接口内部是这样的

public static T Load<T>(string path) where T : Object
{
	return (T)((object)Resources.Load(path, typeof(T)));
}

反射出泛型T的实际类型然后加载一个object类型的对象强转成我们的泛型T,就获得到我们想要的资源,那么会有同学好奇下面的方法是又是怎么调用创建的呢,我们在Unity中或者说反编译UnityEngine.dll也是无法看到里面具体的实现的,因为Unity引擎是C+ +写的,上层的dll也是应用了C+ +的库,想要看具体的实现还需要看C+ +的源码才行,如果感兴趣可以看一下文章末尾参考引用的链接深入了解。C++ 里面资源的创建关系图如下,在Unity引擎中所有的对象都是基于UnityEngine.Object类的,其中我们Unity用到的Asset和Prefab只是两个抽象的概念。
在这里插入图片描述
至于C+ +里面是如何创建各种实例对象的,这里就不分析了,因为Unity不是开源的,可以用过网传的源码版本去自己探索,不过我这里列举一个开源的FairyGUI中用到的简单工厂模式的例子来讲解,这样我们就能类比想象到Unity底层是如何创建对象的。

问题:FairyGUI中是如何使用简单工厂模式创建UI对象的?
我们来分析FairyGUI中创建UI对象的源码,看下图:
在这里插入图片描述
我们看到FairyGUI中有一个UIObjectFactory工厂类,专门负责创建GObject对象,然后有一个GComponent组件是所有UI对象的父类
在这里插入图片描述
下面我们就来看一下这个GComponent中是如何实例化具体对象的,看下图:
在这里插入图片描述
我们看到GComponent中实例化对象就是通过工厂类的NewObject接口,根据类型来创建对象,或许会有人问如果我们不用简单工厂模式是怎样创建呢?下面我们会通过一个实战案例来解释这个问题。

实战案例

问题

FPS类游戏中创建各种不同的武器,武器暂时需求

如果不用工厂模式怎么做?

先定义武器类型

public abstract class Weapon
{
    public string Name { get; set; }
}

//pistol
public class Pistol : Weapon
{
    public Pistol(string name)
    {
        this.Name = name;
        Debug.Log(string.Format("这是一个{0}pistol", name));
    }
}

//步枪
public class Rifle : Weapon
{
    public Rifle(string name)
    {
        this.Name = name;
        Debug.Log(string.Format("这是一个{0}步枪", name));
    }
}

实例化

Weapon weapon1 = new Pistol("王八盒子");
Weapon weapon2 = new Rifle("AK47");

效果图
在这里插入图片描述
可以看到程序运行没问题,假设我们程序中只要需要用到pistol或者步枪对象的对方我们都实例化一个,当代码中好多New Pistol(“XXX”)这样的代码的时候,假设程序需求改了,如果游戏送审,工信部说你们游戏中不允许使用王八盒子这种武器,不然不给过审,那策划反馈到程序这边,程序就需要全局搜索,把所有代码中凡是New Pistol的地方全部删掉或者修改,是不是超级麻烦,这也是为什么初级跟高级程序虽然对同一个功能都能达到最终的运行效果,但如果需求变动,甚至改个好几遍,初级程序会更崩溃的原因,因为代码设计本身可扩展性,可维护性就很差,当然初级程序并不是不想写高级一点的代码,而是自身的水平有限,这也就是为什么公司宁可重金招聘高级程序也不愿意招聘初级的原因。再或者如果我们要实例化100个pistol或者步枪,是不是要写100个New,这是一个巨大的工作量,如果我们用简单工厂模式,就会降低客户端跟具体武器类的强耦合关系,下面我们来用简单工厂模式来修改这个设计。

通过简单工厂模式来优化设计
public enum WeaponType
{
    None = 0,
    EPistol,
    ERifle
}

public class WeaponFactory
{
    public Weapon Create(WeaponType type, string name)
    {
        Weapon weapon = null;
        switch (type)
        {
            case WeaponType.EPistol:
                weapon = new Pistol(name);
                break;
            case WeaponType.ERifle:
                weapon = new Rifle(name);
                break;
        }
        return weapon;
    }
}

public class SimpleFactoryPatternExample : MonoBehaviour
{
    void Start()
    {
        WeaponFactory weaponFactory = new WeaponFactory();
        Weapon weapon1 = weaponFactory.Create(WeaponType.EPistol, "王八盒子");
        Weapon weapon2 = weaponFactory.Create(WeaponType.ERifle, "AK47");
    }
}

运行的结果跟上图是一样的,这样做有什么好处呢?我们把new Pistol()的操作改成了weaponFactory.Create(),客户端跟Weapon的耦合就变成了客户端<–>WeaponFactory<–>Weapon的耦合,有效的降低了客户端跟Weapon的耦合度,将创建实例的方法封装到了工厂里面,客户端需要什么对象就由交给工厂去创建就可以了,就不需要客户端去创建了,客户端去管理各种工厂,工厂去做自己擅长的事情,这就好比类似公司管理,老板适当放权,只需要管理核心骨干即可,每个部门由每个部门的领导去管理,这样老板有更多的精力去处理更重要的事情。

UML类图:
在这里插入图片描述

但这样的修改也带来一个问题,就是如果我再添加一个冲锋枪怎么办呢?也就违背了我们设计原则之一的开闭原则,这个问题就可以引出工厂方法模式来解决。

简单工厂模式优缺点分析

优点
  • 比较好理解,简易操作
  • 简单工厂可以有效降低客户端和具体对象的耦合,将new具体任务交给了一个简单的工厂类
  • 可以有效的进行代码复用,如客户端A和B都需要一个具体对象,客户端A和客户端B可以通过同一个简单工厂来获取具体类型的实例
缺点
  • 一定程度违背了开闭原则(OCP),即对扩展开放,对修改关闭。在新增产品时需要修改简单工厂类,我们应该做到在添加功能的时候,尽量不要修改代码或者尽可能的减少修改代码
小结

简单工厂模式虽然有说违背了OCP原则,但并不是说不能用它,相反其实简单工厂在我们日常开发中还是比较常用的,上面已经列举了Unity引擎中用到了简单工厂的一些案例,总而言之就是一般应对简单需求,对象之间的关系不是那么复杂的时候最适合用它。

工厂方法模式

上文我们提到引进简单工厂模型来优化程序设计,但也有弊端,就是不满足OCP原则,那在此基础上我们如何优化设计来避免不满足OCP原则呢,就可以引进工厂方法模式。当然在需求简单的情况下,还是可以使用简单工厂模式的,当需求复杂化,我们可以考虑使用工厂方法模式,对于设计模式的使用,恰到好处就好,千万避免过度设计。针对上面的需求,我们发生修改:当武器工厂产能有限,因为不同的武器生产流水线不同,为了提高效率,我们就专门的pistol武器工厂专门生产pistol,步枪武器工厂专门生产步枪,这样专门的工厂专门生成专门的特定的武器,这样效率就会高很多,就不需要来回的切换,再举个生活中的例子,通用汽车公司是好几个汽车品牌的母公司,它旗下有凯迪拉克、别克、雪佛兰三个汽车品牌, 然后各种不同的品牌都有自己专门的汽车生产工厂,不存在一个通用汽车工厂生成这三种不同的汽车。那我们针对这个修改的需求来用工厂方法模式修改一下。

UML设计图

在这里插入图片描述

代码

/// <summary>
/// 武器工厂抽象基类
/// </summary>
public abstract class WeaponFactoryBase
{
    public abstract Weapon CreateWeapon(string name);
}

/// <summary>
/// pistol生产工厂
/// </summary>
public class PistolFactory : WeaponFactoryBase
{
    public override Weapon CreateWeapon(string name)
    {
        return new Pistol(name);
    }
}

/// <summary>
/// 步枪生产工厂
/// </summary>
public class RifleFactory : WeaponFactoryBase
{
    public override Weapon CreateWeapon(string name)
    {
        return new Rifle(name);
    }
}

运行结果还是跟上面是一样的。

优缺点分析

优点

工厂模式有效的解决了添加新产品必须要修改工厂类代码的问题,新增一种武器只需要新增一个武器类型的工厂即可,符合开闭原则。

缺点

工厂模式的本质是将具体的创建工作放在了具体子类的工厂中进行。

抽象工厂模式

基本介绍

  • 抽象工厂模式:定义一个interface用于创建相关或有依赖关系的对象族,而无需指明具体的类。
  • 抽象工厂模式可以将简单工厂模式和工厂方法模式进行整合
  • 从设计层面看,抽象工厂模式就是简单工厂模式的改进(或者称进一步抽象)
  • 将工厂抽象成两层,AbsFactory(抽象工厂)和具体实现的工厂子类。程序员可以根据创建对象类型使用对应的工厂子类。这样单个的简单工厂就变成了工厂族,更利于代码的维护和扩展。

我的理解就是抽象抽象再抽象。

需求变更

针对上面武器工厂我们抽象到具体的某个武器工厂,如果再抽象一层,上升到不同国家各个武器工厂生产的规格不同,那我们如何抽象呢?可能这个例子有点不太恰当,按道理一个型号的武器规则应该是一样的,到底是否一样我也不知道,假设游戏里面有各个不同的国家,国家生产某个类型的武器参数可以自定义,如果还理解不了,我再举个生活中的例子,上面讲到通用汽车公司旗下三款品牌的汽车,想必在不同国家虽然同一款型号的汽车肯定都是不一样的,要根据当地国家人民的喜好来进行定制,所谓同一款型号就有国产和进口一说,这样类比可能你就理解刚刚那个游戏中不同国家生产同一款武器型号不一样的例子,我们可以理解为改装。这里我们就拿中国武器工厂和美国武器工厂来举例。

UML设计图

在这里插入图片描述

代码

/// <summary>
/// 国家武器工厂基类
/// </summary>
public abstract class CountryWeaponFactoryBase
{
    string name;
    public WeaponFactoryBase RifleFactory;
    public WeaponFactoryBase PistolFactory;
    public virtual void InitFactory(string factoryName)
    {
        name = factoryName;
        RifleFactory = new RifleFactory();
        PistolFactory = new PistolFactory();
    }
    public abstract void CreateRifle(string name);
    public abstract void CreatePistol(string name);
}

/// <summary>
/// 中国武器工厂
/// </summary>
public class ChineseWeaponFactory : CountryWeaponFactoryBase
{
    public override void InitFactory(string factoryName)
    {
        base.InitFactory(factoryName);
        Debug.Log(string.Format("初始化{0},并做一些个性化设置",factoryName));
    }

    public override void CreatePistol(string name)
    {
        base.PistolFactory.CreateWeapon(name);
    }

    public override void CreateRifle(string name)
    {
        base.RifleFactory.CreateWeapon(name);
    }
}

/// <summary>
/// 美国武器工厂
/// </summary>
public class AmericanWeaponFactory : CountryWeaponFactoryBase
{
    public override void InitFactory(string factoryName)
    {
        base.InitFactory(factoryName);
        Debug.Log(string.Format("初始化{0},并做一些个性化设置", factoryName));
    }

    public override void CreatePistol(string name)
    {
        base.PistolFactory.CreateWeapon(name);
    }

    public override void CreateRifle(string name)
    {
        base.RifleFactory.CreateWeapon(name);
    }
}

调用

CountryWeaponFactoryBase chineseWeaponFactory = new ChineseWeaponFactory();
chineseWeaponFactory.InitFactory("中国军工厂");
chineseWeaponFactory.CreatePistol("92式pistol");
chineseWeaponFactory.CreateRifle("95式自动步枪");

CountryWeaponFactoryBase americanWeaponFactory = new AmericanWeaponFactory();
americanWeaponFactory.InitFactory("美国武器工厂");
americanWeaponFactory.CreatePistol("MK23pistol");
americanWeaponFactory.CreateRifle("M-16步枪");

效果图

在这里插入图片描述

小结

将具体生产过程下沉到各个国家自己的工厂之中,我的理解就是对工厂又做了一层抽象。

工厂模式小结

  1. 工厂模式的意义
    将实例化对象的代码提取出来,放到一个类中统一管理和维护,达到和主项目的依赖关系的解耦。从而提高项目的扩展和维护性。
  2. 三种工厂模式(简单工厂模式、工厂方法模式、抽象工厂模式)
  3. 设计模式的依赖抽象原则
  • 创建对象实例时,不要直接new类,而是把这个new类的动作放在一个工厂的方法中,并返回。有的书上说,变量不要直接持有具体类的引用。
  • 不要让类继承具体类,而是继承抽象类或者实现interface(接口)
  • 不要覆盖基类中已经实现的方法

设计模式系列教程汇总

http://dingxiaowei.cn/tags/设计模式/

更多相关学习资料

教程代码下载

https://github.com/dingxiaowei/UnityDesignPatterns

©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页