[简单的设计模式]策略模式

标签: 设计模式  程序员  工作  编码

本帖最后由 sqy941013 于 2017-6-22 10:26 编辑

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

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

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

那么在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是怎样表演的!
这里写图片描述
成功啦!我们动态地更改了Teddy的行为!那么在运行中如果想要更改狗狗的行为只用调用setter方法就可以了。
总结

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

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

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

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

版权声明:本文为qq_20849387原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_20849387/article/details/73820464

智能推荐

HTML 表单元素的基本样式

HTML 表单元素的基本样式 原创 ixygj197875 发布于2018-02-22 17:48:53 阅读数 2296 收藏 更新于2018-05-20 15:35:58 分类专栏: 揭秘 CSS 揭秘 CSS 收起 表单元素主要包括 label、input、textarea、select、datalist、******、progress、meter、output等,以及对表单元素进行分组的 ...

php输出语句

php输出语句 常见的输出语句 echo(): 可以一次输出多个值,多个值之间用逗号分隔。echo是语言结构(language construct),而并不是真正的函数,因此不能作为表达式的一部分使用。 print(): 函数print()打印一个值(它的参数),如果字符串成功显示则返回true,否则返回false。 print_r(): 可以把字符串和数字简单地打印出来,而数组则以括起来的键和值...

工厂模式

简介 常见的实例化对象模式。 用工厂方法替代new操作的一种模式。 当我们使用new操作实例化对象时,调用构造函数完成初始化。若初始化仅是进行赋值等简单的操作,写入构造函数即可。但如果初始化时需要执行一长串复杂的代码,将多个工作装入一个方法,是不妥的。 创建实例与使用实例分离。将创建实例所需的大量初始化工作从基类的构造函数中分离出去。 简单工厂模式、工厂方法模式针对的是一个产品等级结构;而抽象工厂...

B1105 Spiral Matrix (画图)

B1105 Spiral Matrix (25分) //第一次只拿了21分 矩阵的长和宽,求最大因子,从sqrt(num)开始枚举. 每次循环一次,s++,t--,d--,r++ 测试点四运行超时,是因为输入一个数字的时候,需要直接输出这个数字。//1分 测试点二运行超时,最后一个数字不必再while循环一次,直接输出即可。//3分 最后一个测试点卡了好久/(ㄒoㄒ)/~~ 螺旋矩阵...

Java基础=>String,StringBuffer与StringBuilder的区别

字符串常量池 什么是字符串常量池? JVM为了减少字符串对象的重复创建,其维护了一块特殊的内存,这段内存被称为字符串常量池(存储在方法区中)。 具体实现 当代码中出现字符串时,JVM首先会对其进行检查。 如果字符串常量池中存在相同内容的字符串对象,如果有,则不再创建,直接返回这个对象的地址返回。 如果字符串常量池中不存在相同内容的字符串对象,则创建一个新的字符串对象并放入常量池,并返回新创建的字符...

猜你喜欢

java调用其他java项目的Https接口

项目中是这样的: 用户拿出二维码展示,让机器识别二维码, 机器调用开门的后台系统接口, 然后开门的后台系统接口需要调用管理系统的接口, 管理系统需要判断能不能开门.这两个系统是互相独立的.当时使用http调用是没有问题的.当时后来要求必须用https.废话不说,直接代码: 我的项目中调用的是 HttpsUtils.Get(utlStr) 这个接口 开门系统接口如下图:   管理系统的接口...

Hadoop1.2.1全分布式模式配置

一 集群规划 主机名            IP                               安装的软件 &nbs...

Go语言gin框架的安装

尝试安装了一下gin,把遇到的一些小问题来记录一下 安装步骤 首先来看看官方文档,链接点这里 可以看到安装步骤很简单,就一句话 在命令行中输入这句话运行等待就好。 问题来了,因为墙的问题,go get会很慢,所以命令行里面半天什么反应也没有,不要急,慢慢等着就会看到gin-gonic/gin这个目录出现 这个时候命令行还是没有结束,表示还在下一些东西。有的时候可能心急的人就停了(比如我),然后写个...

uni-app表单组件二

input(输入框) 属性名 类型 说明 平台差异 value String 输入框的初始内容 type String input 的类型 password Boolean(默认false) 是否是密码类型 placeholder String 输入框为空时占位符 placeholder-style String 指定 placeholder 的样式 placeholder-class Strin...

深入理解 JavaScript 代码执行机制

深入理解 JavaScript 代码执行机制 前言 本文仅为个人见解,如有错误的地方欢迎留言区探讨和指正。 1、餐前甜品 如下是一段 JavaScript 代码,如果你毫不犹豫的说出代码执行顺序。那么请直接滚动到底部,留下你的足迹,接受膜拜。如果还不是很确定,那么请往下继续查看。 2、磨刀不误砍柴工(了解浏览器原理) (1) 进程和线程 进程是cpu资源分配的最小单位(是能拥有资源和独立运行的最小...