行为型模式——策略(Strategy)
问题背景
当需要从一系列算法中动态选择一个时,考虑使用策略。在大部分有伤害系统的游戏中,都会区分物理伤害和魔法伤害。思考如下场景:单位A攻击单位B,发出抛射体P,P与B碰撞后调用MakeDamage方法造成伤害。这个方法的实现方式就是讨论的重点:是直接在方法内部实现伤害计算逻辑,还是将请求转发给一个专门计算伤害的对象?如果直接在内部实现,会让抛射体类变得臃肿,而且抛射体的职责应该是控制移动,而不是计算伤害。再者,如果以后出现了一种新的伤害,比如真实伤害,就要修改抛射体类,违背了开闭原则。所以,我们选择后者。
解决方案
这次我们要分离的对象是一个算法,或者说是一个策略,因此我们提取一个策略接口IStrategy,其中有一个Calculate方法负责计算。实现类PhysicalStrategy和ManaStrategy实现IStrategy接口,分别实现计算物理伤害和魔法伤害的逻辑。抛射体类聚合了一个策略对象,负责计算伤害。使用策略后的程序结构是这样的:
图中Unit表示游戏中的单位,可以攻击、被攻击,Attack方法是物理攻击,会生成一个聚合了PhysicalStrategy的抛射体;UseSkill是魔法攻击,会生成一个聚合了ManaStrategy的抛射体。
这样,每个类就做到了轻量、各司其职,系统的结构更加清晰了。同时,由于接口的使用,新的伤害类型也非常容易扩展。
效果
- 提供了一系列可重用的算法,减少了重复代码。
- 用组合代替继承,获得了动态特性。
- 很好地分离了各个类的职责。
缺陷
由于策略提供的是一种算法,所以可能存在这种情况:从结果来看,两种策略没有任何不同,它们只是适用场景不同,比如插入排序和快速排序。这时用户就必须理解两种策略的实现细节才能正确使用它们,这在一定程度上破坏了类隐藏信息的特点。另外,在一些高性能场景,尤其是策略模式这种针对算法的设计模式,调用策略类的额外开销会产生严重的负面影响,这也是需要考虑的。
相关模式
- 享元:一些策略对象可以设计成享元。
- 单例:无状态的策略对象可以设计成单例。
实现
using System;
namespace Strategy
{
class Client
{
public interface IStrategy
{
int Calculate(Projectile proj);
}
public class Unit
{
public int HP { get; set; }
public int ATK { get; }
public int DEF { get; }
public int MDF { get; }
public Unit(int hp, int atk, int def, int mdf)
{
HP = hp;
ATK = atk;
DEF = def;
MDF = mdf;
}
public void Attack(Unit target)
{
Console.WriteLine("攻击");
new Projectile(this, target, ATK, true).MakeDamage();
}
public void UseSkill(Unit target)
{
Console.WriteLine("使用技能");
new Projectile(this, target, (int) (1.2 * ATK), false).MakeDamage();
}
}
public class Projectile
{
public IStrategy strategy;
public Unit Owner { get; }
public Unit Target { get; }
public int Damage { get; }
public Projectile(Unit owner, Unit target, int damage, bool isPhysical)
{
Owner = owner;
Target = target;
Damage = damage;
strategy = isPhysical ? new PhysicalStrategy() as IStrategy : new ManaStrategy() as IStrategy;
}
public void MakeDamage()
{
var damage = strategy.Calculate(this);
Target.HP = Math.Max(Target.HP - damage, 0);
Console.WriteLine($"目标受到了{damage}点伤害,剩余HP: {Target.HP}");
}
}
public class PhysicalStrategy : IStrategy
{
public int Calculate(Projectile proj)
{
return Math.Max(proj.Damage - proj.Target.DEF, 1);
}
}
public class ManaStrategy : IStrategy
{
public int Calculate(Projectile proj)
{
return Math.Max(proj.Damage - proj.Target.MDF, 1);
}
}
static void Main(string[] args)
{
var striker = new Unit(100, 50, 50, 50);
var victim = new Unit(200, 10, 20, 10);
striker.Attack(victim); // HP=200-(50-20)=170
striker.UseSkill(victim); // HP=170-(1.2x50-10)=120
}
}
}