思考并回答以下问题:
- 建造者模式的官方定义是什么?类图是什么样的?四个角色是什么样子的?
- new对象出来之后不代表实例化过程结束了,还需要给对象的属性赋值,调用对象的函数,才算是完成了复杂对象的构建。怎么理解?
- 建造者模式是创建型模式,是为了获取到Product的实例。但是这个实例的方法有不同的表现。怎么理解?
- 建造者模式在其他应用上可能很少用到,但是在游戏开发中,创建角色就会用到。像观察者模式在游戏程序开发中非常重要,但是在PHP中很少使用。所以开发游戏来学习设计模式是非常好的,因为游戏可以模拟现实中的一切,而PHP网站开发遇到的情况没有那么广泛。是这样吗?
- 回答这个关键问题,没有使用建造者模式之前代码存在什么问题?必须用工厂模式和单例模式的原因都知道,建造者模式必须用吗?如果是理由是什么?
- 建造者模式的优点是,能将复杂对象的“产生流程”与“功能实现”拆分后,让系统调整和维护变得更容易。此外,在不需更新实现者的情况下,调整产生流程的顺序就能完成装备线的更改,这也是建造者模式的另一优点。怎么理解?
- 建造者模式的适用条件是将复杂的构建流程以一个类封装,并让不同功能的组装和设置在各自不同的类中实现。怎么理解?
本章涵盖:
- 角色功能的组装
- 建造者模式
- 建造者模式的定义
- 建造者模式的说明
- 建造者模式的实现范例
- 使用建造者模式组装角色的各项功能
- 角色功能的组装
- 实现说明
- 使用建造者模式的优点
- 角色建造者的执行流程
- 建造者模式面对变化时
- 结论
角色功能的组装
在游戏角色的产生-工厂方法模式的应用中,《P级阵地》将双方角色的产生及功能组装等工作全部移到工厂类中,类结构图如图1所示。
图1 产生游戏角色的工厂类的类结构图
Listing1 产生游戏角色的工厂(CharacterFactory.cs)
1 | public class CharacterFactory : ICharacterFactory |
两个工厂方法按照传入参数的指示,将对应的角色对象产生出来,除此之外,还要将每一个角色在游戏执行时所需要的功能对象,如角色属性(ICharacterAttr)、武器(IWeapon)、角色AI(ICharacterAI)等,也按序设置给新产生出来的角色对象。
但如同那一章提到的缺点,对于这些功能的组装,在实现上两个阵营角色似乎没什么差异,只是“重复着一定的顺序和程序代码”。所以,如果按照之前学习到的:当发现两个功能有着类似的算法流程时,就可以运用模板方法模式(Template Method)来优化,但若如此真正实现后,还会发生其他问题。
模板方法模式的实现方式
运用模板方法模式后的角色工厂可能如下:
Listing2 产生游戏角色的工厂
1 | public abstract class CharacterFactory : ICharacterFactory |
上述程序代码新增了一个加入角色功能的方法:AddCharacterFuncs,方法内调用了一组样板方法(Template Method)。又因为两个阵营对于各自角色装备的功能有些差异,所以将这些差异点,交给两个新的工厂类:SoldierFactory和EnemyFactory去实现,AddCharacterFuncs则保留了角色装备各个功能时调用的顺序。
但是,现在的角色工厂类从原来的一个变成了两个角色工厂,PBDFactory在获取角色工厂时,还必须指定要使用哪一个工厂,才能产生正确的角色单位。而且还要加上防止产生错误阵营的“防呆”程序代码,这样一来,也使得原本的角色工厂接口(ICharacterFactory)的扩充受到了限制,每当增加一个新的ICharacterFactory子类时,都要用继承得到两个孙类去实现两个阵营的差异点(所以出现了继承绑定)。
所以,接下来的修正方向应该是:
- 将重复的算法放到一个类中;
- 将两个新的工厂类从工厂继承体系中搬移出去,让组装角色功能的流程独立出来。
上述说明的修改方向就是运用建造者模式的适用条件,将复杂的构建流程以一个类封装,并让不同功能的组装和设置在各自不同的类中实现。
建造者模式
工厂类是将生产对象的地点全部集中到一个地点来管理,但是如何在生产对象的过程中,能够更有效率并且更具弹性,则需要搭配其他的设计模式。建造者模式就是常用来搭配使用的模式之一。
建造者模式的定义
在GoF中对建造者模式(Builder)的定义是:1
将一个复杂对象的构建流程与它的对象表现分离出来,让相同的构建流程可以产生不同的对象行为表现。
简单举一个例子来说明:虽然是同品牌的汽车,但在组装时,一般都可以选择不同的规格、内装和外观。现有几辆车的配装如下:
- A款车配有1.6cc引擎、一般座椅、白色烤漆;
- B款车配有2.0cc引擎、真皮座椅、红色烤漆;
- C款车配有2.4cc引擎、小牛皮座椅、黑色烤漆。
对于装配厂而言,无论车子的规格还是外观是否有所不同,在装配一辆车子时,都会按照一定的步骤来组装:
准备车架 → 外观烤漆 → 将引擎放入车架 → 装入内装(椅)
像上面这样将汽车装配的流程定义出来,即“将汽车(复杂对象)的装配流程与它的车辆规格(对象表现)分离出来”。在定义好装配流程之后,就可以将其应用在不同款的汽车组装上,如图2所示。
图2 3款车在3个流程中的示意图
每一站的装配员(Director)可以按照不同的需求,安装对应的设备到汽车中,即“让相同的汽车装配流程(构建流程)可以装配(建立)在不同的汽车款式(对象表现)上”。
所以,建造者模式可以分成两个步骤来实施:
- (1)将复杂的构建流程独立出来,并将整个流程分成几个步骤,其中的每一个步骤可以是一个功能组件的设置,也可以是参数的指定,并且在一个构建方法中,将这些步骤串接起来。
- (2)定义一些专门实现这些步骤(提供这些功能)的实现者(Builder),这些实现者知道每一部分该如何完成,并且能接受参数来决定要产出的功能,但不知道整个组装流程是什么。
基本上,实现时只要把握这两个原则:“流程分析安排”(Director)和“功能分开实现”(Builder),就能将建造者模式应用于复杂的对象构建流程上。
建造者模式的说明
将“流程分析安排”和“功能分开实现”以不同的类来实现的话,类结构图如图3所示。
图3 以建造者模式来实现类的类结构图
参与者的说明如下:
- Director(建造指示者)
- 负责对象构建时的“流程分析安排”。
- 在Construct方法中,会明确定义对象组装的流程,即调用Builder接口方法的顺序。
- Builder(功能实现者接口)
- 定义不同的操作方法将“功能分开来实现”。
- 其中的每一个方法都是用来提供给某复杂对象的一部分功能,或是提供设置规则。
- ConcreteBuilder(功能实现者)
- Builder的具体实现,实现产出功能的类。
- 不同的ConcreteBuilder(功能实现者)可以产出不同的功能,用来实现不同对象的行为表现和功能。
- Product(产品)
- 代表最终完成的复杂对象,必须提供方法让Builder类可以将各部位功能设置给它。
建造者模式的实现范例
在实现上,Director(建造指示者)与Builder(功能实现者接口)是同时进行的。当在Director的构造函数中,一边将流程分开调用的同时,也将被调用的步骤加入到Builder的接口方法中。
以下是Director范例:
Listing3 利用Builder接口来构建对象(Builder.cs))
1 | public class Director |
在Director类的构建方法(Construct)中,将Builder对象以参数的方式传入,此时的Builder对象代表某一特定功能的实现者(例如B款车的装配产线)。然后按流程规划,分别调用Builder对象中提供各功能的方法来组装产品。而实现上,对于Product(产品类)的对象,是要由Director还是Builder来保存,则可按照实际项目的需求来决定,并不一定要由某一个类负责维护。
Builder(功能实现者接口)定义了能够产生对象所需功能的方法:
Listing4 接口用来生成Product的各个零件(Builder.cs)
1 | public abstract class Builder |
两个子类:ConcreteBuilderA和ConcreteBuilderB分别实现接口所需的方法,不同的子类可以产生不同属性的功能。装备时,将产出的功能直接设置给传入的Product对象中,Product类则是最后被产出的对象:
Listing5 欲产生的复杂对象(Builder.cs)
1 | public class Product |
Product中的每一项功能都是由Builder的实现来提供的,本身并不参与功能的产出。在测试程序中,分别传入不同的Builder子类给Director对象:
Listing6 测试建造者模式(BuilderTest.cs)
1 | void UnitTest() |
在Director的指挥下,将不同属性的功能指定给Product对象,最后获取Product,并显示该Product当前获得的功能和状态。通过信息的输出,可以看到Product对象在使用不同的Builder时,会有不同的功能表现:
执行结果
1 | ShowProduct Functions: |
使用建造者模式组装角色的各项功能
角色的组装算是游戏实现上最复杂的功能之一。每款游戏遇到这个部分时,都要针对程序代码不断地重构、调整、修正、防呆(意即在失误发生前即加以防止)…,原因是“角色”是游戏的卖点之一。游戏中的角色要有多种职业、好看的装备、炫丽的武器,才能博得玩家的喜好。如果是商城制的游戏(即可以在游戏内购买游戏道具),甚至可以让角色可以长出金光闪闪的翅膀走在游戏内的街上“招摇”和“拉风”。
新一代的游戏引擎由于有Shader技术的支持,所以复杂一点的Avatar(纸娃娃)系统也一并提供给玩家使用,让他们能定制自己最喜欢的角色。也因为这些复杂的游戏设置和定制化的参数,让游戏系统要产生一个角色对象时,需要更多方面的考虑,因此也需要包含更多的系统设计,否则就容易造成难以收拾的后果。
角色功能的组装
接下来,我们继续尚未完成的优化工作。按照建造者模式的两个原则:“流程分析安排”和“功能分开实现”来分析现有的程序代码,就会发现,原本规划在角色工厂CharacterFactory的增加角色功能AddCharacterFuncs方法,就是建造者模式所需要的“流程分析安排”(Director)。
Listing7 增加角色功能
1 | public void AddCharacterFuncs( ICharacter pRole, ENUM_Weapon emWeapon, int Lv) |
而Soldier角色工厂(SoldierFactory)和Enemy角色工厂(EnemyFactory)两个子类扮演的则是“功能分开实现”。虽然已经找到建造者模式的要素,也就是实际上已经完成建造者模式的实现了,但因为实现在CharacterFactory中还是会延伸出一些缺点,所以必须将这一部分从角色工厂CharacterFactory中分离,单独实现成为一个新的系统,其类结构图如图4所示。
图4 CharacterFactory类及其子类的类结构图
参与者的说明如下:
- CharacterBuilderSystem:角色建造者系统负责《P级阵地》中双方角色构建时的装配流程。它是一个“IGameSystem游戏系统”,因为角色构建完成后,还需要通知其他游戏系统,所以将其加入已经具有中介者模式(Mediator)的PBaseDefenseGame类中,方便与其他游戏功能沟通。
- ICharacterBuilder:定义游戏角色功能的组装方法,包含3D模型、武器、属性、AI等功能。
- SoldierBuilder:负责玩家阵营角色功能的产生并设置给玩家角色。
- EnemyBuilder:负责敌方阵营角色功能的产生并设置给敌方角色。
实现说明
角色建造者系统(CharacterBuilderSystem)继承自游戏系统接口(IGameSystem),并定义了与角色产生和功能设置有关的流程:
Listing8 角色建造者系统,利用Builder接口来构建对象(CharacterBuilderSystem.cs)
1 | public class CharacterBuilderSystem : IGameSystem |
在Construct方法中,将一个角色所需的功能及设置顺序明确定义下来,并通过调用ICharacterBuilder提供的方法来完成组装:
Listing9 角色建造函数(ICharacterBuilder.cs)
1 | // 建造角色时所需的参数 |
由于游戏角色复杂,需要配合的功能(武器、属性)有所差异,因此会使得建造一个角色所需的参数变多。而实现上比较好的方式是,将这些参数以一个类加以封装,让这些多达7或8个的角色设置参数,不会占满与角色产生流程有关的方法中,这样做会比较方便后续的开发和维护。当需要新增或删除角色的设置参数时,只需要修改封装结构的内容,而不必改变整个流程中的方法,并且封装后的参数类也可以顺应不同角色建造的需要,以继承的方式增加在子类中。这也就是角色建造参数类(ICharacterBuildParam)要声明为抽象类的原因,因为两个阵营角色在建造时,需要的参数各有不同。
SoldierBuilder类提供了玩家阵营角色建造时所需要的功能及设置:
Listing10 Solider角色建造函数(SoldierBuilder.cs)
1 | // 建立Soldier时所需的参数 |
每一个角色功能在产生时,都可以搭配其他游戏系统或对象工厂来获取所需的部分。例如在加载角色模型LoadAsset方法中,搭配资源加载工厂IAssetFactory来获取角色的3D模型资源;在加入武器AddWeapon方法中,搭配武器工厂IWeaponFactory来获取角色使用的武器,而各个方法在设置和产出功能对象时,都会引用角色设置参数SoldierBuildParam中的设置来产出对应的功能。所以,通过SoldierBuildParam参数类在角色的构建流程中穿梭,让每个构建步骤产生的功能对象可以有不同的表现行为和功能。
EnemyBuilder类用来组装敌方角色时所需要的功能,也以相同的方式来实现:
Listing11 Enemy角色建造者(EnemyBuilder.cs))
1 | // 建立Enemy时所需的参数 |
比较不一样的是,因为敌方阵营的角色无法被玩家用鼠标选中,所以在它的构建流程中,加入的鼠标单击选取功能AddOnClickScript方法是没有具体实现的。这也是建造者模式在实现上的另一个灵活点,对于某项功能,实现类可以选择是否加入,不加入时就可以不实现该方法。如果要更明确地指定子类的哪些方法是要实现的或哪些方法是可以选择的话,则可利用程序设计语言的语句限制来规定。以C#来说,强制子类一定要实现某方法的话,则将其定义为抽象函数(abstract function),不一定需要实现的功能,则将其定义为虚函数(virtual function),通过接口的声明,就可明白类实现的规则。
从角色工厂CharacterFactory将角色功能的组装流程搬移出去后,角色在通过工厂方法(FactoryMethod)产生时,就可以调用角色建造者系统(CharacterBuilderSystem)来组装角色功能:
Listing12 产生游戏角色的工厂(CharacterFactory.cs)
1 | public class CharacterFactory : ICharacterFactory |
重构后的两个工厂方法,会先产生一个“角色参数类(SoldierBuildParam、EnemyBuildParam)”对象,并将产生出来的角色对象放在其中,让角色能在整个功能装备的流程中都能被存取到。除此之外,角色建造参数也记录了组装角色时要使用的设置值。最后产生的建造者Builder对象,将角色建造参数设置完成后,就交由建造指导者类(CharacterBuilderSystem)去完成最后的角色组装功能。
使用建造者模式的优点
在重构后的角色工厂(CharacterFactory)中,只简单负责角色的“产生”,而复杂的功能组装工作则交由新增加的角色建造者系统(CharacterBuilderSystem)来完成。运用建造者模式的角色建造者系统,将角色功能的“组装流程”给独立出来,并以明确的方法调用来实现,这有助于程序代码的阅读和维护。而各个角色的功能装备任务,也交由不同的类来实现,并使用接口方法操作,将系统之间的耦合度(即依赖度)降低。所以当实现系统有任何变化时,也可以使用替换实现类的方式来应对。
角色建造者的执行流程
如图5所示的是角色建造者系统的执行流程图,它会指挥Soldier角色建造者(SoldierBuilder)来完成角色功能的组装,最后将装配好的Soldier对象加入角色管理系统(CharacterSystem)中来管理。
图5 角色建造者的执行流程
建造者模式面对变化时
将角色的“功能产生顺序”和“哪些组件会被加入角色”等功能,集中在一个函数方法中实现,对于项目后期的维护和开发是非常有帮助的。
某天……,
企划:“我玩《P级阵地》有一段时间了,总觉得,如果角色头上能够有个血条来显示当前的生命力该有多好…,因为可以方便我调试…”
小程:“加显示血条吗?不过,你说是调试用? ”
企划:“嗯……测试完看好不好用,再决定要不要开放给玩家,这样可以吗?”
小程:“可以。”
企划:“那可不可以再额外加个功能,我想要角色出现时,能利用特效来提示玩家,特效就出现角色位置上。”
小程:“嗯 那我一起添加就好了。”
小程之所以答应得那么干脆,主要是因为上面的两项需求,可以很简单地在角色建造者系统(CharacterBuilderSystem)的Construct方法中进行调整:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20// 构建
public void Construct(ICharacterBuilder theBuilder)
{
// 利用Builder产生各个部分加入Product中
theBuilder.LoadAsset( ++m_GameObjectID);
theBuilder.AddOnClickScript();
theBuilder.AddWeapon();
theBuilder.SetCharacterAttr();
theBuilder.AddAI();
// 是否显示头上血条,可用开关控制
if(m_bEnableHUD)
theBuilder.AddHud();
// 角色出生特效
theBuilder.AddBornEffect();
// 加入管理器内
theBuilder.AddCharacterSystem(m_PBDGame);
}
将新增的功能加入组装流程中,并让两阵营的建造者(SoldierBuilder、EnemyBuilder)实现新增的两个功能:AddHud和AddBornEffect。而想要删除时,也可以暂时从构建流程取消。
结论
建造者模式的优点是,能将复杂对象的“产生流程”与“功能实现”拆分后,让系统调整和维护变得更容易。此外,在不需更新实现者的情况下,调整产生流程的顺序就能完成装备线的更改,这也是建造者模式的另一优点。
与其他模式的合作
建造者模式在实现过程中,大多利用《P级阵地》的工厂类(Factory Class)获取所需的功能组件,而这两种生成模式(Creational Pattern)的相互配合,也是本章范例的重点之一。
其他应用方式
- 在奇幻类型的角色扮演游戏中,设计者为了增加法术系统的声光效果,在施展法术时,大多会分成不同的段落来呈现法术特效。例如发射前的法术吟唱特效、发射时的特效、法术在行进时的特效、击中对手时的特效、对手被打中时的特效、最后消失时的特效。有时为了执行性能的考虑,会在施展法术时,就将所有特效全部准备完成。这个时候就可以利用建造者模式(Builder)将所有特效组装完成。
- 游戏的用户界面(UI)就如同一般的网页或App,有时也会有复杂的版面配置和信息显示。利用建造者模式可以将界面的呈现,分成不同的区域或内容来实现,让界面也可以有“功能装组”的应用方式。