奇幻RPG(角色技能 与 Strategy模式)

看过一些设计模式方面的书籍和文章,虽然很正式,很权威,(也觉得有那么一点刻板),总是觉得让人不那么好靠近。于是,我思考着像写故事一样来写下自己对设计模式的理解。我们将以一款奇幻角色扮演游戏(D&D)为蓝本,通过游戏中的模块创建或者功能实现来展示GOF的设计模式。当然,这不是一款真正意义上的游戏,只是为了了解设计模式,所以,我会尽可能的使游戏简单。废话不多说了,我们Start off吧。

继承及其问题

在开始我们的游戏之旅之前,我们需要定义玩家可以选择的角色。我们首先想到了四个角色职业:野蛮人(Barbarian)、佣兵(Soldier)、圣骑士(Paladin)、法师(Wizard)。

按照OO的思想,我们需要先定义一个抽象类作为基类,然后供这四个职业继承,以实现代码的重用。在此之前,我来分析一下角色拥有的能力(方法):

  • DisplayInfo():显示角色的基本信息。(比如圣骑士:追求至善的热情、维护法律的意志、击退邪恶的力量 -- 这就是圣骑士的三件武器 ... )
  • Walk():让角色行走。
  • Stay():让角色站立。

一般来说,设计时会遵循这样的原则:

  1. 对于所有继承类都有,但是每个继承类的实现各不相同的方法,我们在基类中只给出定义,不给出实现,而在继承类中予以实现。换言之,就是在基类中定义一个抽象方法。
  2. 对于所有继承类都有,并且每个继承类的实现完全相同的方法,我们直接在基类中实现它,而由子类去继承,以实现代码重用。

很显然,每个角色都拥有DisplayInfo()、Walk() 和 Stay()的能力。其中,DisplayInfo()对于每个角色都不同;而Walk()和Stay()对每个角色都相同。于是,我们构建基类Charactor,实现了这样的设计:

在基类中实现的问题

到目前为止,我们的程序仅实现了四个角色样子各不相同,并且都能行走和站立。为了让角色更丰富一些,现在我们让角色可以装配武器,所以,我们需要新添一个方法,我们给它命名为 UseWeapon(),它的实现效果是角色手中拿起一把剑。我们首先想到的是可以将UseWeapon()放到基类中,这样可以实现代码的重用。

于是,我们的设计变成下图:

这样看上去很不错,我们利用了面向对象四大思想(抽象、封装、继承、多态)中的继承。可是好景不长,没过几天,我们觉得这样的角色设置有些单调,个性不鲜明,我们想要对游戏规则做如下修改:

  1. 野蛮人用斧。
  2. 法师不用武器。

这样,便引出了在本例中使用继承的问题:我们发现法师、野蛮人都可以用剑,而这不符合我们定义的游戏规则。

我们将上面的问题抽象化,得到的结论是:给基类添加实体方法,使得不应该拥有此方法的子类也拥有了此方法,也使得所有子类方法拥有了完全一样的实现。

覆盖基类方法的问题

这个问题似乎很好解决,既然野蛮人和法师不同,那我们只需要让野蛮人和法师覆盖基类的 UseWeapon() 方法就可以了,与此同时,我们将基类方法声明为 virtual 虚拟方法,并给出实现。这个实现,可以视为角色的默认实现(默认角色用剑)。

这一次,我们的设计变成了下面这样:

这样看上去似乎很好的解决了这个问题,直到有一天,我们又有了新的需求:

  1. 需要新添角色 战士(Warrior),他也使用斧。
  2. 需要新添角色 牧师(Cleric),他也不使用武器。

同时也带来了新的问题:

  1. 我们没有实现代码重用,对于 野蛮人 和战士,他们对UseWeapon()的实现是相同的(都用斧),但我们不得不将完全一样的代码在每个子类中都写一遍。
  2. 如果这个方法的实现需要经常改动,我们需要反复修改所有相关子类中的方法。
  3. 牧师 和 法师都不使用武器,但是他们都继承了UseWeapon()方法,即便是用一个什么都不做的(空的)UseWeapon()方法覆盖基类方法,他们仍会暴露出 UseWeapon() 的能力(可以从他们的实例中访问此方法)。

接口及其问题

我们发现继承并不是那么好用,尤其是使用继承 问题3 似乎难以解决,于是我们改变思路,这一次,我们使用接口来实现。

我们定义一个 IWeaponable 接口,然后从基类中去除UseWeapon()方法,然后对于可以使用武器的子类,实现这个接口;对于不可以使用武器的子类(牧师、法师),不去实现这个接口。

现在的设计变成了这样:

使用接口所产生的新问题远比它解决的问题多,我们首先看下它解决了什么问题:

  • 牧师、法师 不再具有使用武器的能力,它们的实例也不会暴露出UseWeapon()方法。

再看它产生了哪些问题:

  1. 因为接口只是一个契约,而不包含实现,于是将有大量的子类需要实现此接口。
  2. 代码没有重用,所有用剑、用斧的角色,其UseWeapon()的实现方式都相同。如果将来需要修改,所有的相关子类都需要改动。

封装行为

到目前位置,我们在进行这个角色设计的时候,不管是使用继承还是使用接口,UseWeapon()方法要么是在基类中实现,要么是在子类中实现,我们实际上都是在面向实现编程。OO的一个原则是面向接口编程。面向实现编程的一个主要问题就是不够灵活,以本例而言,所有的角色,要么用剑,要么用斧,要么什么都不用。如果我想让同一个角色先用斧,再用剑,也就是动态地给他分配武器,是无法实现的。

OO有一个原则称作“Encapsulate what varies”,也就是俗称的“封装变化”。在我们当前的情况中,UseWeapon()这个行为是不断变化的,那么我们就应该想办法将它封装起来。这一次,我们依然要使用接口,但是实现此接口的类不再是我们定义的角色的基类或者子类,而是专用于UseWeapon()这个行为的类。如下图所示:

可以看出:我们将对接口的实现分放到了它自己的继承体系中,而不是放到我们的角色类中。每一个实现此接口的类完成一个特定的对UseWeapon()方法的实现。比如说,UseSword类的UseWeapon()实现是 拿起一把剑。UseAx类的UseWeapon实现是 拿起一把斧。而 UseNothing的实现是什么都不做,仅仅由角色发一句抱怨:I can‘t use any weapon。

现在我们要做的,就是将这个方法体系 与 我们的角色体系结合起来,具体如何做呢?就是在我们角色的基类中声明一个 IWeaponable 类型的变量,把它复合进去。如下图所示:

注意到 Character 类中仍有一个UseWeapon()方法,但是这个方法与之前的UseWeapon()方法不同:

  1. 它不是用来给子类去覆盖的。
  2. 我们通过Character的UseWeapon()方法实际去调用 IWeaponable 接口的UseWeapon()方法(实际上调用了其实体类的UseWeapon()方法)。

所以,它的代码大致是这样的:

public void UseWeapon(){
    WeaponBehavior.UseWeapon();
}

而WeaponBehavior在使用之前是应该被赋值的,我们在子类的构造函数中去做这件事:

public Barbarian(){
    WeaponBehavior = new UseAxe();
}

同时,因为各种UseWeapon的方法都被封装在了WeaponBehavior中,我们可以动态地改变它,我们给Character基类再添加一个更换武器的方法:

public void ChangeWeapon(IWeaponable newWeapon){
    WeaponBehavior = newWeapon;  
}

Strategy 模式

实际上,我们上面所做的一切,就完成了Strategy模式,现在让我们看看Stragegy模式的官方定义:

Strategy模式定义了一系列的算法,将它们每一个进行封装,并使它们可以相互交换。Strategy模式使得算法不依赖于使用它的客户端。

代码实现与测试

我们已经完成了设计,现在通过代码来实现整个过程,简单起见,我们只创建两个角色和三种武器技能(包含一个不能使用武器的实现),必要的说明会放在注释中。

using System;
using System.Collections.Generic;
using System.Text;

namespace Strategy {

#region 封装 UseWeapon() 行为

// 定义使用 武器接口
    public interface IWeaponable {
       void UseWeapon();
    }
    // 使用 剑 的类
    public class UseSword : IWeaponable {
       public void UseWeapon() {
           Console.WriteLine("Action: Sword Armed. There is a sharp sword in my hands now.");
       }
    }

// 使用 斧 的类
    public class UseAxe : IWeaponable {
       public void UseWeapon() {
           Console.WriteLine("Action: Axe Armed. There is a heavy axe in my hands now.");
       }
    }

// 不能使用武器的类
    public class UseNothing : IWeaponable {
       public void UseWeapon() {
           Console.WriteLine("Speak: I can‘t use any weapon.");
       }
    }

#endregion
    
    
    #region 定义角色类

// 角色基类
    public abstract class Character {

protected IWeaponable WeaponBehavior;      // 通过此接口调用实际的 UseWeapon方法。
       
       // 使用武器,通过接口来调用方法
       public void UseWeapon() {
           WeaponBehavior.UseWeapon();
       }

// 动态地给角色更换武器
       public void ChangeWeapon(IWeaponable newWeapon){
           Console.WriteLine("Haha, I‘ve got a new equip.");
           WeaponBehavior = newWeapon;  
       }
       
       public void Walk() {
           Console.WriteLine("I‘m start to walk ...");
       }

public void Stop() {
           Console.WriteLine("I‘m stopped.");
       }
              
       public abstract void DisplayInfo(); // 显示角色信息
    }

// 定义野蛮人
    public class Barbarian : Character {

public Barbarian() {
           // 初始化继承自基类的WeaponBehavior变量
           WeaponBehavior = new UseAxe();  // 野蛮人用斧
       }

public override void DisplayInfo() {
           Console.WriteLine("Display: I‘m a Barbarian from northeast.");
       }
    }

// 定义圣骑士
    public class Paladin : Character {

public Paladin() {
           WeaponBehavior = new UseSword();
       }

public override void DisplayInfo() {
           Console.WriteLine("Display: I‘m a paladin ready to sacrifice.");
       }
    }

// 定义法师
    public class Wizard : Character {

public Wizard() {
           WeaponBehavior = new UseNothing();
       }

public override void DisplayInfo() {
           Console.WriteLine("Display: I‘m a Wizard using powerful magic.");
       }
    }
    #endregion

// 测试程序
    class Program {
       static void Main(string[] args) {
           Character barbarian = new Barbarian(); // 默认情况下野蛮人用斧
           barbarian.DisplayInfo();
           barbarian.Walk();
           barbarian.Stop();
           barbarian.UseWeapon();

barbarian.ChangeWeapon(new UseSword()) ;   // 现在也可以使用剑
           barbarian.UseWeapon();

barbarian.ChangeWeapon(new UseNothing());// 也可以让他什么都用不了,当然一不会这样:)
           barbarian.UseWeapon();          
       }
    }
}

值得注意的地方是:Strategy模式没有解决我们之前提到的问题3。法师不应该暴露出UseWeapon()的能力(NOTE:如果在VS2005下使用C#语言,当你在法师后面按下点号的时候,智能提示上应该找不到UseWeapon()方法)而不是提供一个什么都不做的UseWeapon()方法。

总结

在本文中,我们通过一个实现奇幻角色扮演游戏(RPG)的技能设计演示了设计模式中的Strategy模式。

我们首先以继承的方式来实现,然后分析了继承可能引起的问题;随后又使用接口实现了一遍,分析了使用接口会带来的问题。

最后,我们通过封装行为的Strategy模式完成了整个设计,并给出了它的定义。

希望这篇文章能对你有所帮助!

时间: 2024-07-29 07:54:24

奇幻RPG(角色技能 与 Strategy模式)的相关文章

奇幻RPG(人物构造 与 Abstract Factory模式)

在前一节,我们介绍了Strategy模式,并使用此模式实现了一个根据角色的职业来分配技能的范例(实际也就是动态地为类分配方法).作为一款奇幻RPG,有了职业,我们还应当可以为角色选择种族,比如说:人类(Human).精灵(Elf).矮人(Dwarf).兽人(Orc)等等.而这四个种族又有着截然不同的外形,精灵皮肤灰白.有着长长的耳朵.没有体毛和胡须:矮人的皮肤与人类近似,但是身材矮小.通常留着浓密的胡子:兽人则有着绿色的皮肤和高大的身躯,并且面目丑陋.本文将讨论如何使用GOF的Abstract

C++设计模式---Strategy模式

一.前言 学习的第一个设计模式!不知道理解的对不对,期望大家一起多交流~ Strategy模式:策略模式,定义了算法族,分别封装起来,此模式可以让算法的变化独立于使用算法的客户.Strategy模式将逻辑算法封装到一个类中,通过组合的方式将具体的算法实现在组合对象中,再通过委托的方式将抽象的接口的实现委托给组合对象实现.其模型结构图如下: 二.Strategy策略实例 最近在写遥感影像融合相关算法,PCA.Brovey和SFIM算法,正好可以用于这次学习Strategy策略. 关于这三个融合算法

设计模式之策略(Strategy)模式

Strategy模式是一种行为型设计模式,它将算法一个个封装起来,在某一时刻能够互换地使用其中的一个算法.从概念上看,所有这些算法完成的都是相同的工作,只是实现不同而已. 动机 在开发中,我们常常会遇到概念上相同,处理方法不同的任务,例如,对一件商品使用不同的税额计算方法来计算其价格.一般来说,有以下的方法来处理: 复制和粘贴(一份代码具有两个版本,维护成本大) 使用switch或者if语句,用一个变量指定各种情况(分支会变得越来越长) 函数指针或者委托(无法维持对象的状态) 继承(需求变化时,

设计模式-strategy模式

策略模式 <设计模式>一书中对策略模式的意图是这样叙述的: 定义一系列的算法,把他们一个个封装起来,并且使他们可以相互替换,Strategy模式使算法可以独立于使用他的客户而变化. 仔细分析 在不同的环境下,每个类处理事情使用的算法是不一样的,所以针对不同环境,我们可以灵活使用这些算法.这样可以使得算法独立于客户,可以根据所处上下文,使用不同的业务规则或算法.

Java策略模式(Strategy模式)

Strategy是属于设计模式中 对象行为型模式,主要是定义一系列的算法,把这些算法一个个封装成单独的类. Stratrgy应用比较广泛,比如,公司经营业务变化图,可能有两种实现方式,一个是线条曲线,一个是框图(bar),这是两种算法,可以使用Strategy实现. 这里以字符串替代为例,有一个文件,我们需要读取后,希望替代其中相应的变量,然后输出.关于替代其中变量的方法可能有多种方法,这取决于用户的要求,所以我们要准备几套变量字符替代方案. 首先,我们建立一个抽象类RepTempRule 定义

C++设计模式实现--策略(Strategy)模式

一. 举例说明 以前做了一个程序,程序的功能是评价几种加密算法时间,程序的使用操作不怎么变,变的是选用各种算法. 结构如下: Algorithm:抽象类,提供算法的公共接口. RSA_Algorithm:具体的RSA算法. DES_Algorithm:具体的DES算法. BASE64_Algorithm:具体的Base64算法. 在使用过程中,我只需要对外公布Algorithm_Context这个类及接口即可. 代码实现: [cpp] view plaincopy //策略类 class Alg

设计模式(1)---Strategy模式

Strategy模式(行为模式) 1.概述 在软件构建过程中,某些对象使用的算法可能多种多样,经常改变,如果将这些算法都编码到对象中,将会使对象变得异常复杂:而且有时候支持不使用的算法也是一个性能负担. 2.问题 如何让算法和对象分开来,降低他们之间的耦合度,使得算法可以独立于使用它的客户而变化? 3.解决方案 策略模式:它定义了一系列算法,把每一个算法封装起来,让它们之间可以相互替换,本模式使得算法可独立于使用它的客户而变化. 4.结构 5.例子 商场收银软件:营业员根据顾客所购买商品的单价和

Strategy模式详解--设计模式(13)

Strategy模式来源:        在软件开发中也常常遇到类似的情况,实现某一个功能有多种算法或者策略,我们可以根据环境或者条件的不同选择不同的算法或者策略来完成该功能.如查找.排序等,一种常用的方法是硬编码(Hard Coding)在一个类中,如需要提供多种查找算法,可以将这些算法写到一个类中,在该类中提供多个方法,每一个方法对应一个具体的查找算法:当然也可以将这些查找算法封装在一个统一的方法中,通过if-else-或者case等条件判断语句来进行选择.这两种实现方法我们都可以称之为硬编

Behavioral模式之Strategy模式

1.意图 定义一系列的算法,把他们一个个封装起来,并使它们可相互替换.本模式使得算法可以独立与使用它的客户而变化. 2.别名 政策(Policy) 3.动机 有许多算法可对一个正文流进行分析.将这些算法硬编进使用它们的类中是不可取的.其原因如下: 需要换行功能的客户程序如果直接包含换行算法代码的话将会变得复杂,这使得客户程序庞大并且难以维护,尤其当其需要支持多种换行算法时问题更加严重. 不同的时候需要不同的算法,我们不想支持我们并不使用的换行算法. 当换行功能是客户程序的一个难以分割的成分时,增