文章目录
- 定义
- 图纸
- 一个例子:如何切换坦克的攻击方式
- GameElement(游戏元素)
- TankFactory(坦克工厂)
- Tank(坦克)
- 医疗车和飞行车
- 策略模式
- Behavior(行为)
- Tank
- TankFactory
- 碎碎念
- 策略和状态
- 为什么我们需要策略模式,是继承不好用吗?
- 那我们为什么要用组合呢?
定义
定义一系列算法,把他们一个个封装起来,并且使他们可以互相替换。本模式使得算法可独立于使用他的客户而变化
图纸
一个例子:如何切换坦克的攻击方式
假定我们现在要设计一个RTS(即时战略)游戏,这个游戏中玩家主要通过战车工厂生产坦克来实现对对手进行攻击以取得胜利,为此我们必须创建属于坦克的类,就像这样:
GameElement(游戏元素)
/**
* 游戏元素
*/
public class GameElement {
/**
* 所属者
* 一般来说这个应该是玩家,这里简化用String代替
*/
private String owner;
/**
* 生命值
*/
private int HP;
/**
* 单位类型
*/
private String type;
public GameElement(String type, String owner, int HP) {
this.type = type;
this.owner = owner;
this.HP = HP;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getOwner() {
return owner;
}
public void setOwner(String owner) {
this.owner = owner;
}
public int getHP() {
return HP;
}
public void setHP(int HP) {
this.HP = HP;
}
}
TankFactory(坦克工厂)
/**
* 战车工厂
*/
public class TankFactory extends GameElement {
public TankFactory(String owner) {
super("战车工厂", owner, 20);
}
/**
* 制造一辆坦克
*/
public Tank makeTank() {
return new Tank(getOwner());
}
}
Tank(坦克)
/**
* 坦克类
*/
public class Tank extends GameElement {
public Tank(String owner) {
super("坦克", owner, 10);
}
public void attack(GameElement e) {
if(e.getOwner().equals(getOwner())){
System.out.println("无法攻击友军");
}else {
e.setHP(e.getHP() - 5);
}
}
public void move() {
System.out.println("坦克进行了移动");
}
}
首先,我们将所有游戏内玩家可以操控的元素通过一个根类体现出来,也就是 GameElement(游戏元素)
。所有的游戏元素都必然会有自己所属的 owner(玩家)
、HP(生命值)
和 type(类型)
接着,我们给 Tank(坦克)
赋予了 attack(攻击)
和 move(移动)
两种能力,每个 Tank 在攻击的时候都会先判断被攻击者是否是友军,如果不是的话,降低被攻击方的生命值
最后,所有的坦克都是从 TankFactory(战车工厂)
中被生产出来的,而且 TankFactory 自身也是一个 GameElement 所以有自己的 所属玩家 和 生命值
医疗车和飞行车
为了增加游戏的可玩性,我们添加了两种不同的坦克,功能是这样的:
- 医疗车:可以攻击己方单位,攻击时给对方回血
- 飞行车:移动时既可以在地面跑,也可以在空中飞行
加上原有的坦克,我们的游戏中出现了三种类型的坦克,他们的本质都是坦克,都应该拥有 move 和 attack 方法,只是不同类型的坦克需要对对应的方法进行重写。于是第一种解决方案跃然纸上,我必须立刻使用继承!就像这样:
我们通过继承实现了各种坦克的行为,这是没问题的,可是在编码的时候却很别扭。为了迁就 FlyTank 的飞行能力,我们给所有坦克类上都添加了 fly 方法。但事实上,这个方法除了对 FlyTank 有意义之外毫无作用
那你会说了,那我直接把 Tank 根类做成一个抽象类,默认实现一个空的 fly 方法不就行了吗?
这个办法可以,只不过治标不治本,将来策划突发奇想,整一个会飞的医疗车咋办?那我就只能再建一个 FlyCureTank ?
到这里,我们陷入了泥潭,我们创建新的子类不再是因为存在不同类型的内容,而是在不断的把原有内容进行排列组合,每有一种新组合,我们就必须成倍的增长子类(这取决于有多少种攻击方式和多少种移动方式),这显然违背了我们的初衷
而且将来不熟悉程序的人接手我们的代码时,他一定会奇怪,为什么在 Tank 类里面,会有一个空的 fly 方法,他真的有存在的意义吗?
我们不禁要思考,有没有办法可以直接把 move 和 fly 内的算法直接提取出来,复用他?
答案当然是肯定的,要不然为啥要讲策略模式
策略模式
就像这样:
Behavior(行为)
/**
* 攻击行为
*/
public abstract class AttackBehavior {
/**
* 使用策略的元素
*/
private GameElement user;
public GameElement getUser() {
return user;
}
public void setUser(GameElement user) {
this.user = user;
}
public abstract void attack(GameElement e);
}
/**
* 普通攻击
*/
public class BasicAttack extends AttackBehavior {
@Override
public void attack(GameElement e) {
if (e.getOwner().equals(getUser().getOwner())) {
System.out.println("无法攻击友军");
} else {
e.setHP(e.getHP() - 5);
}
}
}
/**
* 医疗攻击
*/
public class CureAttack extends AttackBehavior{
@Override
public void attack(GameElement e) {
if(e.getOwner().equals(getUser().getOwner())){
e.setHP(e.getHP() + 3);//恢复三点生命值
}else {
System.out.println("无法攻击敌军");
}
}
}
/**
* 飞行策略
*/
public abstract class FlyBehavior {
/**
* 使用策略的元素
*/
private GameElement user;
public GameElement getUser() {
return user;
}
public void setUser(GameElement user) {
this.user = user;
}
public abstract void fly();
}
public class NoFly extends FlyBehavior{
@Override
public void fly() {
//不会飞
}
}
/**
* 螺旋桨飞行
*/
public class PropellerFly extends FlyBehavior{
@Override
public void fly() {
System.out.println("采用螺旋桨飞行");
}
}
Tank
/**
* 坦克类
*/
public class Tank extends GameElement {
/**
* 攻击策略
*/
private AttackBehavior attackBehavior;
/**
* 飞行策略
*/
private FlyBehavior flyBehavior;
public Tank(String owner) {
super("坦克", owner, 10);
//默认使用普通攻击
setAttackBehavior(new BasicAttack());
//默认无法飞行
setFlyBehavior(new NoFly());
}
public void attack(GameElement e) {
attackBehavior.attack(e);//调用攻击策略的攻击方法
}
public void move() {
System.out.println("坦克进行了移动");
}
/**
* 飞行
*/
public void fly() {
flyBehavior.fly();//调用飞行策略的飞行方式
}
public void setAttackBehavior(AttackBehavior attackBehavior) {
this.attackBehavior = attackBehavior;
attackBehavior.setUser(this);
}
public void setFlyBehavior(FlyBehavior flyBehavior) {
this.flyBehavior = flyBehavior;
flyBehavior.setUser(this);
}
}
TankFactory
/**
* 战车工厂
*/
public class TankFactory extends GameElement {
public TankFactory(String owner) {
super("战车工厂", owner, 20);
}
/**
* 制造一辆基础坦克
*/
public Tank makeBasicTank() {
return new Tank(getOwner());
}
/**
* 制造一辆医疗车
*/
public Tank makeCureTank() {
Tank tank = makeBasicTank();
tank.setAttackBehavior(new CureAttack());
return tank;
}
/**
* 制造一辆用螺旋桨飞行的坦克
*/
public Tank makePropellerTank() {
Tank tank = makeBasicTank();
tank.setFlyBehavior(new PropellerFly());
return tank;
}
}
我们去掉了 Tank 所有因为不同的行为方式而出现的子类
那这些行为方式去哪了呢?他们去了 Behavior(行为)
类簇中
我们把攻击和飞行这两种行为,独立到两个不同的算法类簇
中,这样一来不同的攻击方式和飞行方式之间可以随意的组合。而在这个过程中,我们不需要动 Tank 的类簇,这跟他没关系。
请注意 TankFactory 在这个实现中做的事情,他没有根据不同类型的坦克产出不同的子类,而是给他们装上对应的算法类对象,以实现不同坦克的体现
这种写法让你不单在创建 Tank 对象的时候可以随意组合行为模式,他甚至在游戏进行的过程中都可以改变一辆坦克的行为模式,只要改变对应 Tank 对象的对应行为类对象就可以实现了,这是继承无法触及的领域
而这正是一个标准的策略模式实现
碎碎念
策略和状态
策略模式和状态模式就像是本体和镜像之间的关系
他们都是实现算法和算法调用者之间的解耦,而且实现这种解耦的方式都是通过把 不同的算法 封装成 算法类簇 实现的
那他们有区别吗?
- 策略模式通过从外部直接切换对象即将要执行的算法来改变对象的行为
- 状态模式通过改变内部的状态来实现算法对象的切换以改变对象的行为
这就是他们本质的区别
策略模式一定是外界 直接
驱动的,他把组装的“权限“交给了外部。所以对上级代码来说,当前这个对象将要执行哪个算法对象是显而易见的
状态模式不是这样的,一个对象内部状态的改变未必都是外部驱动的,他也可以是内部执行某些动作后触发的变化。而且即使外部去驱动这种变化,上级代码看到的也是自己改变状态的动作,而不是直接和算法对象之间的关联。也就是说,上级代码对执行算法的对象内部到底发生什么事情感知不强,甚至有的时候完全无感,毕竟他只是推倒了多米诺骨牌的第一张
这样的区别直接导致了状态模式的可拓展性终究比不上策略模式。在新的算法簇加入的情况下,策略模式可以通过新增一个算法类的方式直接把新的算法加入到程序中;而对状态模式来说,你只能去修改已被封装的代码
那你会说,不对啊,为什么我用状态模式要新增算法类的时候不可以对应的新增一个算法调用类的子类的?
从技术上来说是没问题的,而且对应的例子在前面的 状态模式 一文里面的转笔刀里已经演示过了。可是你有没有想过如果算法越来越多,是不是意味着每一种新算法都要对应一种新的算法调用类。这样一来,算法类簇和算法调用类簇之间甚至有可能形成 平行类层次(这个概念在前面的 工厂方法 一文已经聊过,这里不再赘述)
为什么我们需要策略模式,是继承不好用吗?
如果把设计模式比喻成一部小说,那么策略模式就是在我心目中当之无愧的主人公
几乎所有的 OO 设计模式出现的初衷,都是为了把改变的部分和不变的部分解耦,以实现代码的复用和动态组合
那你会说了,不对啊,继承不是也实现这个效果吗?
我们把多个类所共有的共有部分抽象出来,并通过继承的形式在子类中实现个性化,这难道不是继承诞生的初衷吗?
假如我们再往前推一步,就会发现其实策略模式所能实现的所有效果,我们用继承也都可以实现:在上例中,我们完全可以把多功能车设置成一个超类,然后把攻击车、医疗车和防空车作为多功能车的子类
但是策略模式放弃了继承,而是采用把部分算法“委托”出去的方式来组合。这不仅是给我们提供了一种解题思路,而是给我们提供了一种思考方式,尝试把继承转移到组合中
那我们为什么要用组合呢?
因为 有一个
比 是一个
有用得多
【是一个】是系统设计者预设的 模具
,在实现效果的时候,你只能去找合适的模具;如果没有,你就得再刻一个
【有一个】是在 搭积木
。框架的设计者创造的是积木块,是写字时用的点竖横折钩;没有人规定积木要如何组合在一起,就像没有人规定笔画要如何组合一样(当然乱组合的话别人不认识这个字,但别人不认不意味着你不能这样写)。而处理具体业务的程序员的任务则是把组件组装起来以产出更大的组件或成品
高下立判,【有一个】的方式为系统带来了巨大的弹性,他不仅实现了模块颗粒度的细化,还实现这些模块之间在执行期间的动态组合
当然,即便如此,抽象、继承、多态这些概念也没有什么问题,还是那句话,知识怎么可能会有问题,问题一定是出在使用他的人身上。这些概念不会让你立刻就能设计出优秀的系统,她们是锛凿斧锯,可是设计师关注的不是如何切割这块木材,而是如何搭建一个具有弹性的系统,一如搭建一间可以屹立百年的高楼
万分感谢您看完这篇文章,如果您喜欢这篇文章,欢迎点赞、收藏。还可以通过专栏,查看更多与【设计模式】有关的内容