本文章著作权由 沈庆阳 所有,转载请与作者取得联系!
几乎所有的新手程序员都会遇到这样的一些问题:在编写代码的时候,尽管知道自己的目标是什么。但当代码编写到一定程度,代码量慢慢变大的时候,整个程序的方向不再是自己可控的。编码工作与目标相差愈远,以至于最终卡在某个环节而推翻重来。这是个人遇到的问题,如果这个问题放到团队中呢?缺少适当的接口,代码阅读性、维护性差,对于项目带来的后果是不可想象的。但是问题总归有解决的方法,在一次次的代码编写过程中,前人总结出来的经验便归结成为了设计模式。
设计模式就是一套被反复使用、经过分类的代码设计经验的总结。通过使用设计模式我们可以增加代码的复用性,使代码更容易被他人理解从而提高了代码的可靠性。设计模式可以使得代码编写真正地实现工程化。
那么在Unity 3D的代码编写过程中,经过总结我发现有四种设计模式的使用最为频繁和方便。这四种设计模式分别是:策略模式、单例模式、工厂模式和观察者模式。
策略模式
“策略模式定义了一系列的算法,并将每一个算法封装起来,而且使它们还可以相互替换。策略模式让算法独立于使用它的客户而独立变化。”
哦,看不懂是么,上面的解释是直接从百科中复制出来的,所以不必担心读不懂的问题,因为它只是一些基本的概念,我们甚至不需要知道这些概念。因为设计模式不是概念,而是编程的经验,我们只要学会这些经验就行了。
让我们以一个简单的例子来理解策略模式。
UBug游戏公司要做一款模拟狗狗的游戏——狗狗模拟器,Jack是UBug公司的资深程序员,于是这个小游戏编写的担子就到了Jack的肩上。公司的游戏架构师给了Jack一份开发文档,使用标准的面向对象(OO)技术进行了该系统的内部设计,该设计设计了一个狗的父类,并让所有各种狗继承该父类。
Jack根据游戏架构师给的设计编写了这样的一个程序,他很满意,因为这样就可以实现所有种类的狗狗了,简直完美。UBug公司的产品经理在看完这样一个Demo之后觉得总少些什么,现在的模拟狗狗游戏这么多,我们是不是该有一些创新的地方?既然这样写的狗狗不会跳舞,那么我们写出了会跳舞的狗狗是不是独树一帜呢!于是产品经理拍了拍Jack的肩膀说,让狗狗跳起来吧?
Jack回到自己的工位上,他自信满满,有了架构师给的设计,我直接在Dog的父类添加一个跳舞的方法不就完美了么?对于一个面向对象的程序,简直So Easy!
Jack改完代码之后,自信满满,测试都没有测试便直接把程序打个包,发给了他的主管。
于是,在一周之后的股东大会上,UBug的主管给股东们展示了这样一个“革新”的模拟游戏:哈士奇会跳舞,泰迪会跳舞,就连石头做的狗狗雕像都在跳舞?
Jack被主管叫到了办公室,你这样是不行的?石头做的狗狗是没有生命的,怎么可能会跳舞呢?Jack劈头盖脸挨了一顿臭骂,回到自己的工位上陷入了沉思。
Jack忽略了一个事情,并不是所有的狗狗都会跳舞。而Jack在父类写上跳舞的方法之后,所有继承该父类的子类都拥有跳舞这个方法了。对代码所做的局部的修改,影响的层面并不只是局部(修改了父类的代码会影响继承其的子类)**!于是Jack开始修改狗狗模拟器的代码。
哦,Jack终于完成了对代码的修改。他把看门狗的bark(),walk(),dance()等方法覆盖掉了,也就是什么也不做。可是如果以后又要加入更多的大理石的狗狗、塑料做的狗狗玩具可不是要写更多的代码了么?代码的复用性变得好差。
Jack开始总结,利用继承来实现物体的行为会造成牵一发而动全身的后果,我是不是可以考虑采用其他的方法?如果采用接口呢?Jack可以将bark()、walk()、dance()写成接口(Barkable()、Walkable()、Danceable()),从而实现不同的狗狗的行为。可是这样就意味着对每个新增的狗狗都要写出其bark()等接口的实现,代码的复用性极低。如果狗狗模拟器要写几百只品种的狗狗,那岂不是无穷尽的噩梦!
在代码的编写过程中,我们应遵循良好的面向对象设计原则,使用一种对既有代码影响较小
方式来修改软件,从而投入更多的精力添加新的功能。在软件开发的过程和软件维护的过程中,没有哪个软件是一成不变的。总需要不断地对软件进行修改**,这是亘古不变的真理。一旦软件停止了更新,那么这个软件的生命周期离死亡也就不远了。
Jack意识到了继承并不是解决问题最好的方法,因为狗的行为在子类中总是不断改变的,让所有的子类都具有父类中同样的行为似乎不是很恰当。使用接口来解决这个问题起初看似很好,但C#中接口不实现任何代码,所以通过继承接口无法实现代码的复用。于是Jack找到UBug的游戏架构师寻求帮助。游戏架构师给了他一个经验:找到应用中可能需要变化的地方,把它们独立出来,不要和那些不需要变化的代码混在一起。那么怎么确定代码中可能发生变化的地方呢?比如每次需求的更改或者增加了新的需求,都会造成某些地方的代码发生改变,那么我们就可以确定这一部分的代码需要被独立出来,和其余稳定的代码部分有所区分。也就是说,通过上述的设计原则,我们把会变化的部分提取并封装起来,以便后续可以轻易地改动此部分的代码,从而不影响不需要变化的部分。这几乎是所有的设计模式的精髓,所有的设计模式都在解决系统中某部分的改变不会影响其他部分。至此,是时候把狗狗的行为从父类中提出出来了!
经过一系列的分析过后,Jack发现除了bark()、dance()等问题之外,狗类的行为还算正常,因此为了分开“变化和不变化”的部分,Jack准备建立两组类。一个是和bark()相关的,一个是和dance()相关的,每一组类都实现各自的行为。比如可以有一个类实现安静不叫的狗狗,一个类实现怒吼的狗狗一个类实现哀鸣的狗狗。Jack明白了他要做的是针对接口编程,而不是针对实现编程**。
我们利用接口代表每个行为,比如BarkBehaviour和DanceBehaviour,每个实现都将实现其中的一个接口。我们编写了一个专门实现BarkBehaviour与DanceBehaviour的“行为”类。由行为类而不是狗狗类来实现行为的接口。以往的做法,行为由父类来实现,或是继承某个接口并由子类实现。这两种方法都是针对实现编程,代码被实现绑死,很难去更改具体行为。在新的设计中,狗的子类将使用接口(BarkBehaviour等)来表示行为,所以实现的“实现”不会与狗的子类产生绑定,也就是具体行为编写在实现了BarkBehaviour等的类中。
如何理解针对接口编程
“针对接口编程”的真正含义就是“针对超类型编程”。
在软件术语中,被继承的类一般称为“超类”,也有叫做父类。通常情况下,每个子类的对象“是”它的超类的对象。一个超类可以有很多个子类,所以超类的集合通常比它的任何一个子类集合都大。
假设有一个抽象类Programmer,有两个具体实现来继承Programmer,分别是CppProgrammer和JavaProgrammer。
由针对实现编程的做法如下:
CppProgrammer cppProgrammer=new CppProgrammer();
cppProgrammer.code();
上述声明了一个cppProgrammer,以CppProgrammer为类型。cppProgrammer是Programmer的具体实现,我们必须对具体实现编码。如果使用针对接口编程做法是否会更方便呢?
Programmer
programmer=new CppProgrammer();
programmer.code();
当我们知道对象是C++程序员的时候可以利用Programmer进行多态调用。
那么理解了如何针对接口编程之后,我们就需要开始整合狗狗的行为了。
首先,需要在Dog类中加入两个接口的实例变量,BarkBehaviour和DanceBehaviour,其声明为接口类型(不是具体类实现类型),每个狗狗对象都会动态地设置这些变量以便在运行时引用正确的行为类型(如AngryBark、Quiet等)。
public class Dog{
BarkBehaviour barkBehaviour;
…
public void performBark(){
barkBehaviour.bark();
}
}
在上述代码中,狗狗对象并非自己亲自处理Bark行为,而是委托给BarkBehaviour来引用的对象。
那么如何具体设定barkBehaviour的实例变量呢?
public class Husky:Dog{
public Husky(){
}
}
在继续往下看的时候,不妨先根据上面的类图自己编写C#代码,并在Unity中跑一下。
你运行自己编写的代码了么?好吧,那么就看一下我们写的代码吧。
public abstract class Dog{
public BarkBehaviour barkBehaviour;
public DanceBehaviour danceBehaviour;
public Dog() { }
public abstract void display();
public void performBark()
{
barkBehaviour.Bark();
}
public void performDance()
{
danceBehaviour.Dance();
}
public void walk()
{
Debug.Log("I am walking...");
System.Console.WriteLine("I am walking...");
}
}
public interface BarkBehaviour
{
void Bark();
}
public class AngryBark : BarkBehaviour
{
void BarkBehaviour.Bark()
{
Debug.Log("Woof!Woof!Woooooooof!!!!!(Angry Face)!");
System.Console.WriteLine("Woof!Woof!Woooooooof!!!!!(Angry
Face)!");
}
}
public class QuietBark : BarkBehaviour
{
void BarkBehaviour.Bark()
{
Debug.Log("...");
System.Console.WriteLine("...");
}
}
public class SadBark : BarkBehaviour
{
void BarkBehaviour.Bark()
{
Debug.Log("Woo~~~Woooo...(Sad Face)...");
System.Console.WriteLine("Woo~~~Woooo...(Sad Face)...");
}
}
public interface DanceBehaviour
{
void Dance();
}
public class JazzDance : DanceBehaviour
{
void DanceBehaviour.Dance()
{
Debug.Log("Jaaaaz...Jazz.Jazz....");
System.Console.WriteLine("Jaaaaz...Jazz.Jazz....");
}
}
public class WaggleDance : DanceBehaviour
{
void DanceBehaviour.Dance()
{
Debug.Log("Waggle Waggle Waggle...");
System.Console.WriteLine("Waggle Waggle Waggle...");
}
}
public class Husky : Dog{
public Husky()
{
barkBehaviour = new AngryBark();
danceBehaviour = new WaggleDance();
}
public override void display()
{
Console.WriteLine("Wow Look At Me??I
am a Husky?!");
Debug.Log("Wow Look At Me??I am a Husky?!");
}
}
编写测试脚本
public class UBugSimDogs : MonoBehaviour {
Dog husky = new Husky();
//
Use this for initialization
void
Start () {
husky.performBark();
husky.performDance();
}
}
那么在Unity的控制台中运行,我们将看到如下效果
哈,正确了。我们的哈士奇可以愤怒地吼叫和跳舞了!完美!
但是至此,Jack开始了思考,我是否可以让这个程序更加完美呢?每次都在构造函数中指定相应的行为是否太麻烦?能不能有更妙的方法?有的!我们可以使用“设定方法”(Setter Method)来设定狗狗的行为,而不是在构造器中实例化。在Dog类中加入两个新的设定方法。
public void setBarkBehaviour(BarkBehaviour bb)
{
barkBehaviour = bb;
}
public void setDanceBehaviour(DanceBehaviour db)
{
danceBehaviour = db;
}
使用上述两个设定方法编写我们的泰迪吧!
public class Teddy : Dog
{
public Teddy()
{
barkBehaviour = new SadBark();
danceBehaviour = new WaggleDance();
}
public override void display()
{
Debug.Log("Woo,I am Teddy?I f everywhere!");
}
}
Teddy的测试脚本
public class UBugSimDogs : MonoBehaviour {
Dog teddy = new Teddy();
//
Use this for initialization
void
Start () {
teddy.performBark();
teddy.setBarkBehaviour(new QuietBark());
teddy.performBark();
teddy.performDance();
teddy.setDanceBehaviour(new JazzDance());
teddy.performDance();
}
}
在上述脚本中,我们动态地更改了Teddy的Bark行为和Dance行为,看看控制台中我们的Teddy是怎样表演的!
![Upload 7.png failed. Please try again.]
成功啦!我们动态地更改了Teddy的行为!那么在运行中如果想要更改狗狗的行为只用调用setter方法就可以了。
总结
让我们来回顾一下最终的代码,将其绘制为UML图。
在上图中,我们不再把狗狗说成一组行为,而是看成一组算法。就像文章开头说的一样,策略模式定义了一系列的算法,并将每一个算法封装起来。在上图中,有继承、直接关联和接口实现三种箭头,其关系表示了“是一个”、“有一个”和“实现”的关系。
在面向对象的程序设计中,关联有时候比继承更好。也就是面向对象的设计原则中有,多用组合,少用继承。使用组合建立的系统具有很大的弹性,其可以将算法封装在一起,也可以在运行时动态地改变行为。当然,组合的行为对象应该符合接口的标准。
参考文献:Freeman, Elisabeth, Freeman, et al. Head First Design Patterns[C]// O' Reilly & Associates, Inc. 2004.
本文章著作权由 沈庆阳 所有,转载请与作者取得联系!