[设计模式如此简单]一、策略模式

本文章著作权由 沈庆阳 所有,转载请与作者取得联系!

几乎所有的新手程序员都会遇到这样的一些问题:在编写代码的时候,尽管知道自己的目标是什么。但当代码编写到一定程度,代码量慢慢变大的时候,整个程序的方向不再是自己可控的。编码工作与目标相差愈远,以至于最终卡在某个环节而推翻重来。这是个人遇到的问题,如果这个问题放到团队中呢?缺少适当的接口,代码阅读性、维护性差,对于项目带来的后果是不可想象的。但是问题总归有解决的方法,在一次次的代码编写过程中,前人总结出来的经验便归结成为了设计模式。

设计模式就是一套被反复使用、经过分类的代码设计经验的总结。通过使用设计模式我们可以增加代码的复用性,使代码更容易被他人理解从而提高了代码的可靠性。设计模式可以使得代码编写真正地实现工程化。

那么在Unity 3D的代码编写过程中,经过总结我发现有四种设计模式的使用最为频繁和方便。这四种设计模式分别是:策略模式单例模式工厂模式观察者模式

策略模式

“策略模式定义了一系列的算法,并将每一个算法封装起来,而且使它们还可以相互替换。策略模式让算法独立于使用它的客户而独立变化。”

哦,看不懂是么,上面的解释是直接从百科中复制出来的,所以不必担心读不懂的问题,因为它只是一些基本的概念,我们甚至不需要知道这些概念。因为设计模式不是概念,而是编程的经验,我们只要学会这些经验就行了。

让我们以一个简单的例子来理解策略模式。

UBug游戏公司要做一款模拟狗狗的游戏——狗狗模拟器,Jack是UBug公司的资深程序员,于是这个小游戏编写的担子就到了Jack的肩上。公司的游戏架构师给了Jack一份开发文档,使用标准的面向对象(OO)技术进行了该系统的内部设计,该设计设计了一个狗的父类,并让所有各种狗继承该父类。

1.gif

Jack根据游戏架构师给的设计编写了这样的一个程序,他很满意,因为这样就可以实现所有种类的狗狗了,简直完美。UBug公司的产品经理在看完这样一个Demo之后觉得总少些什么,现在的模拟狗狗游戏这么多,我们是不是该有一些创新的地方?既然这样写的狗狗不会跳舞,那么我们写出了会跳舞的狗狗是不是独树一帜呢!于是产品经理拍了拍Jack的肩膀说,让狗狗跳起来吧?

Jack回到自己的工位上,他自信满满,有了架构师给的设计,我直接在Dog的父类添加一个跳舞的方法不就完美了么?对于一个面向对象的程序,简直So Easy!

2.gif

Jack改完代码之后,自信满满,测试都没有测试便直接把程序打个包,发给了他的主管。

于是,在一周之后的股东大会上,UBug的主管给股东们展示了这样一个“革新”的模拟游戏:哈士奇会跳舞,泰迪会跳舞,就连石头做的狗狗雕像都在跳舞?

Jack被主管叫到了办公室,你这样是不行的?石头做的狗狗是没有生命的,怎么可能会跳舞呢?Jack劈头盖脸挨了一顿臭骂,回到自己的工位上陷入了沉思。

Jack忽略了一个事情,并不是所有的狗狗都会跳舞。而Jack在父类写上跳舞的方法之后,所有继承该父类的子类都拥有跳舞这个方法了。对代码所做的局部的修改,影响的层面并不只是局部(修改了父类的代码会影响继承其的子类)**!于是Jack开始修改狗狗模拟器的代码。

3.gif

哦,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等的类中。

4.gif

如何理解针对接口编程

“针对接口编程”的真正含义就是“针对超类型编程”。

在软件术语中,被继承的类一般称为“超类”,也有叫做父类。通常情况下,每个子类的对象“是”它的超类的对象。一个超类可以有很多个子类,所以超类的集合通常比它的任何一个子类集合都大。

假设有一个抽象类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(){
      }
}
5.gif

在继续往下看的时候,不妨先根据上面的类图自己编写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的控制台中运行,我们将看到如下效果

6.png

哈,正确了。我们的哈士奇可以愤怒地吼叫和跳舞了!完美!

但是至此,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图。

8.gif

在上图中,我们不再把狗狗说成一组行为,而是看成一组算法。就像文章开头说的一样,策略模式定义了一系列的算法,并将每一个算法封装起来。在上图中,有继承、直接关联和接口实现三种箭头,其关系表示了“是一个”、“有一个”和“实现”的关系。

在面向对象的程序设计中,关联有时候比继承更好。也就是面向对象的设计原则中有,多用组合,少用继承。使用组合建立的系统具有很大的弹性,其可以将算法封装在一起,也可以在运行时动态地改变行为。当然,组合的行为对象应该符合接口的标准。

参考文献:Freeman, Elisabeth, Freeman, et al. Head First Design Patterns[C]// O' Reilly & Associates, Inc. 2004.

本文章著作权由 沈庆阳 所有,转载请与作者取得联系!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,413评论 25 707
  • 设计模式汇总 一、基础知识 1. 设计模式概述 定义:设计模式(Design Pattern)是一套被反复使用、多...
    MinoyJet阅读 3,895评论 1 15
  • 分享一则之前卖创意T的收获。 创意T这种东西,100%是创意驱动的,什么意思呢?没有人愿意撞衫啊!尤其是这玩样儿买...
    Stove3阅读 932评论 0 2
  • 作为史上唯恐天下不乱,生来就为捣蛋的骨灰级选手,像心爱的百合姐出轨这样的大事,我怎么能不来当当围观吃瓜群众,为新闻...
    前世911阅读 446评论 10 2
  • 有些事情需要仪式感,2016年2月已过去,写一篇月度总结,和二月说再见。第一次写月度总结,不追求多细致,坚持下来才...
    爱吃馒头的姑娘阅读 706评论 0 0