用王者荣耀分析<策略模式>

拿农药这个话题蹭波热度。
当然在此前简书德维恩魏已经写过这种文章了,这里注明一下,文章中所设计的代码可能相似,大家觉得我表述的不专业的话可以参考前辈代码。

其他参考:https://www.cnblogs.com/zhanglei93/p/6081019.html

王者农药

一. 游戏背景

为了避免直接进入代码的不适应,我们先聊点轻松的。
王者荣耀是一款5v5公平对战手游(1v9公平对战手游……),自诞生以来很受欢迎,成为主流moba游戏,我也是女票带入坑的,从此一发不可收拾。
当然游戏要适度,代码还是要写的。

首先我们了解一下大概的游戏的一些规则。
所有英雄分为几种不同的职业,当然各职业也有自己不同的特点,比如射手(ADC)射程远,伤害高,但是很脆容易死等。
每个英雄有3-4个技能,还有一个普通攻击。
同时在游戏中有一个召唤师技能,在出场时你可以选择适合自己的召唤师技能。

二.面向对象设计

背景就说这么多,我们试着用面向对象的方法来考虑如何设计英雄。

抛开召唤师技能,我们可以为一个职业构建一个基类,比如说射手,创建一个Shooter类,然后出新射手的时候,我们只需要把新射手继承自Shooter类。

1
2
3
public class Shooter{
//Shooter类
}

出了后羿这个英雄

1
2
3
public class Hy extent Shooter{
//后羿
}

出了鲁班七号

1
2
3
public class Lb extent Shooter{
//鲁班
}

出了公孙离

1
2
3
public class Ali extent Shooter{
//公孙离
}

每个英雄的自身的一,二,三技能和普通攻击是不一样的,所以可以在自己的类中实现,并且不可以被别人复用。

而召唤师技能是固定的,并且是可以复用的。
那么我们想应该怎么实现?

Q:如何使用OO思想添加召唤师技能?

A: 召唤师(英雄)与召唤师技能之间是一种Has-a的关系,且召唤师技能是必须要选择而且每次都不一样的。我们必须要将召唤师技能模块化。

三.策略步骤

取出英雄中,易于变化的部分,作为行为。

第一步:我们首先建立一个行为管理。
具体为什么这么做后面再说。

1
2
3
4
5
6
7
/**
* 召唤师技能策略
* */
public interface SkillBehavior {
//使用召唤师技能
public void useSkill();
}

第二步:然后我们借助这个接口,实现一些列的召唤师技能,这里列举两个。

1
2
3
4
5
6
public class Cj implements SkillBehavior {
@Override
public void useSkill() {
System.out.println("惩戒使用");
}
}

1
2
3
4
5
6
public class Sx implements SkillBehavior{
@Override
public void useSkill() {
System.out.println("闪现使用");
}
}

他们分别实现了SkillBehavior接口。

所有召唤师技能相关的东西已经搞定了,是一个独立的模块,暂时与英雄无关。

第三步:我们看一下Shooter类如何设计的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public abstract class Shooter{
//配置策略
SkillBehavior skillBehavior;
public SkillBehavior getSkillBehavior() {
return skillBehavior;
}
public void setSkillBehavior(SkillBehavior skillBehavior) {
this.skillBehavior = skillBehavior;
}

//使用技能
public void performSkill(){
skillBehavior.useSkill();
}
}

这里很好理解,shooter作为超类,需要执行召唤师技能的配置和使用。声明变量和get / set 方法,并且添加use方法。
为什么不写在子类里?
当然是节省代码。

第四步:设计子类

来看看我本命公孙离

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 阿离的实体
* */
public class Ali extends Shooter{
public Ali() {
System.out.println("我是阿离");
}

//……其他代码
public void A(){
System.out.println("阿离普通攻击");
}
}

咦?什么情况。
发现子类实体没有关于召唤师技能的相关代码。
同样的我们看后羿,

1
2
3
4
5
6
7
8
9
10
11
/**
* 后羿
* */
public class Hy extends Shooter{
public Hy() {
System.out.println("我是后羿");
}
public void A(){
System.out.println("后羿普通攻击");
}
}

也是没有。
最后我们看一下Main方法的调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Main {
public static void main(String[] args) {
Shooter houYi = new Hy();
//后羿带了闪现技能
houYi.setSkillBehavior(new Sx());

Shooter gongSunli = new Ali();
//公孙离带了惩戒
gongSunli.setSkillBehavior(new Cj());

//后羿释放了技能
houYi.performSkill();
//公孙离释放了技能
gongSunli.performSkill();
}
}

通过setSkillBehavior设置召唤师技能,然后通过performSkill()使用技能。

然后突然我们就会意识到,召唤师技能是作为模块配置给英雄的。
运行结果

四.策略模式

策略模式是行为型模式

1.定义

策略模式定义了算法族,分别封装起来,让他们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。

算法族:a family of algorithms,意思是一系列的算法,它们之间有联系,在上面的栗子中我们可以理解成闪现,斩杀等召唤师技能。

看这个句子可能晦涩难懂,但是结合我们的王者示例就可以很容易的理解,定义了一些召唤师技能,他们可以独立于使用他们的召唤师,一旦发生修改只需要修改它自身,而不需要修改召唤师。

2.策略

上面我们已经大致了解了策略模式的概念,那么为什么叫策略呢?
何为策略?
我们平时所说的策略,更多指办事的不同方法或集合

策略.png

比如说商场促销,同样是优惠,我们可以有不同的方式,比如满减,比如优惠券,比如买一赠一,这都是属于优惠。

那么策略在这个小栗子中的含义就是商场为了促销而实施的一系列措施

我们把这些策略独立出来,让他们不仅能用于衣服商城,而且能够用于家电商城。这样就实现了复用,大大节省代码量并且易于后期维护。

能想到的最简单的方法当然是if/else,如果满足什么条件实施买一赠一,如果满足什么条件发放优惠券,这是最low的一种方式,大家在代码中应该避免。

对于王者荣耀问题,使用策略模式其实是不太通顺的,这里仅仅是为了蹭热度才选择王者荣耀的。(希望不要被喷……)

3.角色

策略模式图

  • Context 上下文角色,或者是策略的使用者。对应王者荣耀示例的阿离,后羿等英雄(策略的使用者)类。
  • Istrategy 算法族抽象类,是一个包含抽象动作的接口,对应王者荣耀示例策略的SkillBehavior 抽象
  • ConcreteStrategy 具体策略角色,实现相应的策略功能。对应王者荣耀示例相应的召唤师技能。

现在在上面留下的问题已经有了答案,策略部分必须要有一个家族抽象,或者理解成家族规定(或者DNA),然后才能创建相应的策略,另一部分必须是一个策略的使用者,两者联系,让整个策略模式有意义。

4.优缺点

优点:

  • 通俗的讲,有了策略模式,你就可以不用if/else来条件判断,只需要在相应的策略接受者身上使用相应的策略就行了。
  • 因为策略的独立,我们可以随意添加或修改策略的实现,对于更新维护都是较友好的,这要换成if/else可得维护累死一个小组的程序猿。
  • 通过封装角色对其封装,保证对外提供“可自由切换”的策略。

    缺点:

  • 每个策略都是一个类,比较繁琐,可以想象一下,10个召唤师技能,要写10个类,要100个呢?(当然可能就会使用别的方法了,这里就是举个栗子),每一个类复用的可能性还不是很高(对于王者问题),那就费时费力了。
  • 所有的策略都需要对外暴露,上层模块必须知道有哪些策略,并自行决定使用哪种策略。【扩展阅读:迪米特原则

五.其他

给一个策略模式的标准写法吧,代码来自文章参考,转载请注明出处

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
//算法族抽象
public interface IStrategy {
//策略要执行的动作
public void doSomething();
}

//策略1
public class ConcreteStrategy1 implements IStrategy{
//实现要做的事情
public void doSomething(){
System.out.println("this is a concreteStrategy");
}
}
//策略2
public class ConcreteStrategy2 implements IStrategy{
@Override
public void doSomething() {
// TODO Auto-generated method stub
System.out.println("This is concreteStrategy2");
}
}
//策略使用客户
public class Context {
private IStrategy strategy = null;
//可以通过构造器直接初始化
public Context(IStrategy strategy){
this.strategy = strategy;
}
//直接调用
public void doSomething(){
this.strategy.doSomething();
}
}

public class Client {
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
Context text = new Context(new ConcreteStrategy2());
text.doSomething();
text = new Context(new ConcreteStrategy1());
text.doSomething();
}
}

OO设计原则(摘自《Head First 设计模式》)

1.针对接口编程,而不是针对实现编程

2.多用组合,少用继承

3.找出应用中可能变化的地方,把他们独立出来,不要和那些不需要变化的代码混在一起。

六.补充

作为一个Android爱好者,这里补充一下Android里用的策略模式。
Android中也有好多地方用到策略模式,这里我们说最熟悉的ListAdapter
作为Listview的适配器,ListAdapter连接了View与Model,作为桥的功能实现了列表。

问题来了,我们平时如何使用ListView的Adapter?
很简单,做过Android的都知道setAdaper()方法
那我们打开setAdapter()的方法
Android源码

我们看到setAdapter(),接受一个ListAdapter对象,那么我们深入一下,看看ListAdapter实现。

Android源码

可以看到ListAdapter是一个抽象,下面的代码没有截图,是两个抽象函数。

那么这样你就能够看到,这个ListAdapter就是算法族抽象,你自定义的Adapter或者系统内置的Adapter都是需要抽象自ListAdapter。

1
2
3
public abstract class BaseAdapter implements ListAdapter, SpinnerAdapter {
……
}

这些Adapter就是策略,那么setAdapter就是配置策略,ListView就是客户。

终于写完啦。
如果有错误或者疑问的话,可以留言哦。

该睡觉啦,晚安!