http://www.tracefact.net/Design-Pattern/Strategy.aspx
引言
看过一些设计模式方面的书籍和文章,虽然很正式,很权威,(也觉得有那么一点刻板),总是觉得让人不那么好靠近。于是,我思考着像写故事一样来写下自己对设计模式的理解。我们将以一款奇幻角色扮演游戏(D&D)为蓝本,通过游戏中的模块创建或者功能实现来展示GOF的设计模式。当然,这不是一款真正意义上的游戏,只是为了了解设计模式,所以,我会尽可能的使游戏简单。废话不多说了,我们Start off吧。
继承及其问题
在开始我们的游戏之旅之前,我们需要定义玩家可以选择的角色。我们首先想到了四个角色职业:野蛮人(Barbarian)、佣兵(Soldier)、圣骑士(Paladin)、法师(Wizard)。
按照OO的思想,我们需要先定义一个抽象类作为基类,然后供这四个职业继承,以实现代码的重用。在此之前,我来分析一下角色拥有的能力(方法):
- DisplayInfo():显示角色的基本信息。(比如圣骑士:追求至善的热情、维护法律的意志、击退邪恶的力量 -- 这就是圣骑士的三件武器 ... )
- Walk():让角色行走。
- Stay():让角色站立。
一般来说,设计时会遵循这样的原则:
- 对于所有继承类都有,但是每个继承类的实现各不相同的方法,我们在基类中只给出定义,不给出实现,而在继承类中予以实现。换言之,就是在基类中定义一个抽象方法。
- 对于所有继承类都有,并且每个继承类的实现完全相同的方法,我们直接在基类中实现它,而由子类去继承,以实现代码重用。
很显然,每个角色都拥有DisplayInfo()、Walk() 和 Stay()的能力。其中,DisplayInfo()对于每个角色都不同;而Walk()和Stay()对每个角色都相同。于是,我们构建基类Charactor,实现了这样的设计:
在基类中实现的问题
到目前为止,我们的程序仅实现了四个角色样子各不相同,并且都能行走和站立。为了让角色更丰富一些,现在我们让角色可以装配武器,所以,我们需要新添一个方法,我们给它命名为 UseWeapon(),它的实现效果是角色手中拿起一把剑。我们首先想到的是可以将UseWeapon()放到基类中,这样可以实现代码的重用。
于是,我们的设计变成下图:
这样看上去很不错,我们利用了面向对象四大思想(抽象、封装、继承、多态)中的继承。可是好景不长,没过几天,我们觉得这样的角色设置有些单调,个性不鲜明,我们想要对游戏规则做如下修改:
- 野蛮人用斧。
- 法师不用武器。
这样,便引出了在本例中使用继承的问题:我们发现法师、野蛮人都可以用剑,而这不符合我们定义的游戏规则。
我们将上面的问题抽象化,得到的结论是:给基类添加实体方法,使得不应该拥有此方法的子类也拥有了此方法,也使得所有子类方法拥有了完全一样的实现。
覆盖基类方法的问题
这个问题似乎很好解决,既然野蛮人和法师不同,那我们只需要让野蛮人和法师覆盖基类的 UseWeapon() 方法就可以了,与此同时,我们将基类方法声明为 virtual 虚拟方法,并给出实现。这个实现,可以视为角色的默认实现(默认角色用剑)。
这一次,我们的设计变成了下面这样:
这样看上去似乎很好的解决了这个问题,直到有一天,我们又有了新的需求:
- 需要新添角色 战士(Warrior),他也使用斧。
- 需要新添角色 牧师(Cleric),他也不使用武器。
同时也带来了新的问题:
- 我们没有实现代码重用,对于 野蛮人 和战士,他们对UseWeapon()的实现是相同的(都用斧),但我们不得不将完全一样的代码在每个子类中都写一遍。
- 如果这个方法的实现需要经常改动,我们需要反复修改所有相关子类中的方法。
- 牧师 和 法师都不使用武器,但是他们都继承了UseWeapon()方法,即便是用一个什么都不做的(空的)UseWeapon()方法覆盖基类方法,他们仍会暴露出 UseWeapon() 的能力(可以从他们的实例中访问此方法)。
接口及其问题
我们发现继承并不是那么好用,尤其是使用继承 问题3 似乎难以解决,于是我们改变思路,这一次,我们使用接口来实现。
我们定义一个 IWeaponable 接口,然后从基类中去除UseWeapon()方法,然后对于可以使用武器的子类,实现这个接口;对于不可以使用武器的子类(牧师、法师),不去实现这个接口。
现在的设计变成了这样:
使用接口所产生的新问题远比它解决的问题多,我们首先看下它解决了什么问题:
- 牧师、法师 不再具有使用武器的能力,它们的实例也不会暴露出UseWeapon()方法。
再看它产生了哪些问题:
- 因为接口只是一个契约,而不包含实现,于是将有大量的子类需要实现此接口。
- 代码没有重用,所有用剑、用斧的角色,其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()方法不同:
- 它不是用来给子类去覆盖的。
- 我们通过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模式完成了整个设计,并给出了它的定义。
希望这篇文章能对你有所帮助!
相关推荐
在本文中,我们将通过一个奇幻角色扮演游戏(RPG)的例子来解释这个模式,以便更好地理解其工作原理。 在RPG游戏中,我们有四种角色:野蛮人、佣兵、圣骑士和法师。这些角色各自有不同的特性和能力。在面向对象编程...
内容概要:本文详细介绍了如何在Abaqus中批量建立非线性弹簧,特别是轨道弹簧的施加方法。主要内容涵盖非线性弹簧的基本概念、重要性及其在轨道交通仿真中的应用。文中具体讲解了创建材料属性、截面属性、装配体与部件、参考点与约束,以及施加非线性弹簧的具体步骤。此外,还提供了用于批量创建非线性弹簧的Python脚本代码片段,展示了如何通过编程方式高效地完成任务。最后,强调了这些方法在轨道交通车轨耦合模型中的重要性和应用前景。 适合人群:对Abaqus有基本了解,从事轨道交通仿真研究的技术人员和工程师。 使用场景及目标:①掌握Abaqus中非线性弹簧的创建流程;②提高轨道交通车轨耦合模型仿真的准确性;③利用Python脚本提升建模效率。 其他说明:本文提供的代码片段仅为示范,实际应用时需根据具体情况进行调整。
内容概要:本文详细介绍了PSO-LSSVM-Adaboost回归预测模型的构建方法及其性能评估标准。首先阐述了最小二乘支持向量机(LSSVM)的基本概念以及核函数的选择,接着重点讲解了利用粒子群优化(Particle Swarm Optimization, PSO)进行超参数调整的具体步骤,包括粒子初始化、适应度计算、速度和位置更新规则等。随后探讨了Adaboost集成学习算法的应用,特别是针对LSSVM作为基分类器时的权重更新策略。最后给出了模型的整体架构图,并采用多种评价指标如均方根误差(Root Mean Square Error, RMSE)、平均绝对误差(Mean Absolute Error, MAE)、决定系数(R-squared, R²)等来衡量模型的表现。 适合人群:对机器学习有一定了解并希望深入研究组合模型的科研工作者和技术爱好者。 使用场景及目标:适用于多特征输入单输出的预测任务,旨在提高预测精度的同时减少过拟合风险。通过本篇文章的学习,读者能够掌握如何将三种不同的机器学习技术有机结合,从而构建更加鲁棒的预测系统。 其他说明:文中提供了完整的Python代码片段用于辅助理解和实操练习,同时分享了一些实用的经验技巧,例如设置合理的迭代终止条件以节约计算资源。此外还提到了一些常见的注意事项,像处理特殊的数据情况(如y_true为0的问题)以及选择合适的评估指标。
2025年C++大作业芙芙与膨膨兽游戏.zip
内容概要:本文基于PFC3D(Particle Flow Code 3D)软件,详细探讨了岩石注浆过程中的破坏现象及其背后的机理。首先介绍了注浆破坏的复杂性,指出这是由材料特性、地质构造和计算机模拟技术共同决定的。接着重点讲解了注浆速度和流量的调整方法,强调适当的速度和流量对于确保注浆效率和避免过度破坏的重要性。最后讨论了在不考虑渗流场的情况下,如何根据岩石结构特征选择最佳的注浆孔位置,以提高注浆效果并保护周围岩石结构。 适合人群:从事地质工程领域的研究人员和技术人员,尤其是那些希望深入了解岩石注浆过程的人。 使用场景及目标:适用于需要利用PFC3D进行岩石注浆模拟的研究项目,旨在帮助用户掌握注浆速度、流量调节技巧以及合理的注浆孔位选择方法。 其他说明:文中提供了简单的PFC3D模拟代码框架,便于读者快速上手实践。同时提醒读者注意实际操作时应结合实验室理论模型和现场具体情况来进行参数优化。
毕业设计-家政服务小程序 V2.7.1-整站商业源码.zip
在拆分工作的时候,很多内容都会通过多分支延伸逻辑的表达和呈现。 今天给大家分享商务风所涉及工作汇报逻辑关系图PPT,希望能够帮助到大家。[666]
实训商业源码-万能表单weui 8.1.16-论文模板.zip
内容概要:本文详细介绍了如何利用滑动平均算法(MA)处理电力系统中的功率波动,确保其符合国家并网标准。首先解释了功率波动的概念及其对电力系统的影响,接着阐述了滑动平均算法的基本原理和实现步骤,包括设置不同时间窗口(1分钟和10分钟)来平滑功率数据。随后讨论了如何计算滑动后的最大功率波动以及如何调整滑动窗口参数以达到最佳效果。最后,提出了合理的功率分配策略,以确保最终输出既稳定又高效地满足国家标准。 适合人群:从事电力系统研究和技术实施的专业人士,尤其是关注功率波动处理和并网标准的技术人员。 使用场景及目标:适用于需要解决电力系统中功率波动问题的实际工程环境,旨在帮助技术人员理解和应用滑动平均算法,从而提升电力系统的稳定性和效率。 其他说明:文中提供了详细的理论背景和技术细节,有助于深入理解滑动平均算法的应用,并指导具体的工程实践。
实训商业源码-门店微商城小程序V1.2.2 开源版-论文模板.zip
毕业设计-分享红包暴力营销 11.9.44-整站商业源码.zip
实训商业源码-多商家营销活动平台V2.1.0 小程序前端+后端-论文模板.zip
内容概要:本文探讨了广义回归神经网络(GRNN)在数据预测中的应用及其优化方法。GRNN作为一种基于径向基函数网络的前馈型神经网络,在预测任务中有广泛应用。但其性能高度依赖于平滑因子的选择。文中介绍了嵌套最新的DBO(蜣螂)优化算法,通过自动优化平滑因子,使GRNN获得全局最优预测网络权重。具体来说,DBO算法模拟蜣螂行为,通过多次迭代随机生成不同平滑因子并评估其预测效果,最终选择最优平滑因子。此外,还提供了Python代码示例,展示了如何实现这一优化过程。 适合人群:对机器学习尤其是神经网络有一定了解的数据科学家、研究人员以及相关从业者。 使用场景及目标:适用于需要提高数据预测精度的任务,如金融建模、天气预报等领域。目标是通过优化平滑因子提升GRNN模型的预测性能。 其他说明:该方法不仅提高了预测准确性,还为数据预测任务提供了一个智能化、高效化的解决方案。建议读者在实践中结合自身业务特点灵活应用。
Avif批量转换Jpg工具是一款专注于图片格式转换的软件,旨在帮助用户便捷地将Avif格式图片批量转换为Jpg格式。在当今图片处理需求多样化的时代,不同格式的图片应用场景各异,这款软件正是为解决Avif与Jpg格式转换难题而诞生。 该软件具备强大的批量转换功能,能够一次性处理多张Avif图片,大大提高了转换效率,节省用户时间。用户无需逐一转换图片,只需简单操作,即可实现批量格式转换,让繁琐的图片格式转换工作变得轻松简单。 它适用于有图片格式转换需求的各类人群,如设计师、摄影师、自媒体从业者等,这些人群在日常工作中常常需要处理不同格式的图片。使用时,用户只需将需要转换的Avif图片导入软件,设置好输出路径等参数,点击转换按钮即可完成操作。 软件的核心使命是为用户提供高效、便捷的图片格式转换服务,帮助用户快速处理图片。在技术上,它采用先进算法,确保转换过程高效稳定。虽然目前暂未提及安全认证相关信息,但从其功能表现来看,专注于格式转换的单一功能,减少了潜在风险。在性能方面,批量转换功能的高效性体现了其良好的性能指标。
毕业论文-无后台版-整站商业源码.zip
内容概要:本文介绍了如何利用遗传算法(GA)、粒子群优化算法(PSO)以及差分进化算法(DE)对K均值聚类方法进行优化,旨在最小化类内距离并支持自定义k值。文中详细阐述了三种优化算法的工作原理及其在K均值聚类中的具体应用方式,同时提供了完整的MATLAB代码实现,包括数据加载、预处理、聚类及优化步骤。实验结果显示,经过优化后的K均值聚类能够显著提升聚类效果,降低对初始条件的敏感性。 适合人群:从事数据分析、机器学习领域的研究人员和技术人员,尤其是那些希望深入了解聚类算法优化机制的人。 使用场景及目标:适用于需要对大规模数据集进行高效分类的应用场景,如市场细分、图像识别、文本分类等领域。通过学习本文提供的优化方法,读者可以获得更好的聚类结果,为后续的数据分析提供坚实的基础。 其他说明:本文不仅理论讲解详尽,还附带了实用的MATLAB代码,方便读者动手实践,快速掌握相关技能。
实训商业源码-社群团购接龙V9.4.0公众号+小程序双端源码-论文模板.zip
内容概要:《师说》由唐代文学家韩愈所作,强调了师道的重要性。文章指出古代学者必定有老师,因为老师能够传授道理、教授学业、解答疑惑。文中认为人的认知并非与生俱来,每个人都会遇到疑惑,而解决疑惑的最佳途径就是向老师求教。无论年龄、地位,只要懂得道理,就可以成为他人之师。作者还批评了当时社会上一些人轻视师道的现象,认为这导致了智慧的退化。最后,韩愈以李蟠为例,赞扬了他能不受世俗限制,向自己求学的行为,并以此文赠予李蟠,倡导尊师重道的精神。; 适合人群:对古代文化、教育思想感兴趣的读者,以及希望深入了解中国传统文化中师生关系的人士。; 使用场景及目标:①适用于古代文化研究及教学场景;②有助于理解古代儒家对于教育、师生关系的看法;③可以作为现代教育理念的参考,思考如何传承和发扬尊师重道的传统美德。; 其他说明:此文不仅是对师道的论述,更是对当时社会风气的一种批判,体现了韩愈对教育和人才发展的深切关怀。
内容概要:本文介绍了使用COMSOL软件构建单轴压缩裂纹发展的二维模型,旨在通过弹性模量变化相图确定裂纹开裂位置。首先,文中详细描述了如何在COMSOL中创建二维模型并设定材料属性,如弹性模量和泊松比。接着,通过设定边界条件进行单轴压缩加载模拟,具体操作包括对底部边界的固定约束以及顶部边界的均匀压力载荷施加。最后,利用COMSOL的后处理功能提取弹性模量数据并绘制变化相图,从而识别出裂纹可能产生的位置。这种方法有助于深入理解材料在受力时的内部变化机制。 适合人群:从事材料科学、工程力学领域的研究人员和技术人员,尤其是那些希望借助数值模拟手段探究材料裂纹行为的人群。 使用场景及目标:适用于需要评估材料在单轴压缩条件下裂纹发展路径的研究项目。主要目标是通过建立精确的数学模型,预测裂纹形成的具体位置,进而指导材料的设计与改进。 其他说明:文中提供的MATLAB代码片段展示了具体的建模步骤,便于读者理解和复现实验过程。此外,还提到了未来可以扩展的方向,如引入材料非均质性等因素,使模型更加贴近现实情况。
毕业设计-接力棒2.6.4 开源版-整站商业源码.zip