设计模式——策略模式

时间:2022-07-21 21:59:02

策略模式

从今天开始介绍设计模式,其实我也是初学设计模式,主要参考书为《Head First 设计模式》,所以文中介绍的内容多引用此书,但我不会完全照搬书本中的案例,我会结合我日常生活中的实际使用情况,模仿书中的案例来介绍我自己的案例。当然,写文章的时候我也参考学习了网上很多的博客和资料,我会在文中一一注明出处,如您发现有侵权行为,请直接联系我。后续我还准备学习其他书籍,等学完之后我会再在此基础上更新文章。另外我也会在文中介绍我自己对设计模式的理解,如有遗漏或者错误的地方,欢迎各位指正,谢谢!

碎碎念完之后,让我们来介绍一下第一个设计模式——策略模式。

1、定义

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

乍一看这一段描述很拗口难懂,我来讲一下我的理解:首先在策略模式中定义了一系列的算法族,这里的算法族类似于一系列的行为、策略,策略模式将这些行为封装成类,可以理解成每一种行为都会封装成一个类,但是并不是所有的行为都会封装成类的,因为它们需要能够相互替换。所谓相互替换,就是指它们有一定的共性,这些行为需要有共同的方法。最后,这些算法的变化不能影响使用算法的客户,也就是说,如果行为变化了,对于使用行为的类来说是透明的,我们无需改动使用行为的类。

通过定义的描述我们还是很难理解为什么需要使用策略模式、策略模式到底要如何实现,下面我们将通过案例来详细介绍。

2、策略模式案例

2.1、需求

假设我们需要开发一款游戏,这个游戏中有各式各样的角色,每个角色都可以进行决斗,决斗时每个角色可以选择一款武器或者不选择武器,武器又有多种选择,角色可以在游戏中*切换武器,这样的功能我们该如何实现?

2.2、第一种实现方式

我们最先想到的是先定义一个抽象类(或接口)Character表示角色,这个抽象类中有两个方法决斗fight()和使用武器useWeapon(),然后定义一系列的角色来继承Character,这样每个角色就都具有了fight()和useWeapon()方法了,下面是我们基于此想法设计的类图:
设计模式——策略模式
下面我们将编码实现这些类,首先我们先定义超类Character:

public abstract class Character {
    protected String name;

    public void fight() {
        useWeapon(); 
        System.out.println(name + "开始决斗了");
    }

    public abstract void useWeapon();   
}

在这个类中,我们定义了一个属性name,还有两个方法,其中决斗方法fight()已经实现,我们规定角色在决斗时先使用武器然后再开始决斗,使用武器的方法useWeapon()未实现,可以由每个角色自行实现选择武器。下面我们来实现每个角色:

public class King extends Character {
    public King() {
        this.name = "国王";
    }

    @Override
    public void useWeapon() {
        System.out.println(name + "拿出大宝剑");
    }
}
//------------------------------------------------
public class Queen extends Character {
    public Queen() {
        this.name = "皇后";
    }

    @Override
    public void useWeapon() {
        System.out.println(name + "拿出魔法杖");
    }
}
//------------------------------------------------
public class Knight extends Character {
    public Knight() {
        this.name = "骑士";
    }

    @Override
    public void useWeapon() {
        System.out.println(name + "拿出了大宝剑");
    }
}
//------------------------------------------------
public class Soldier extends Character {
    public Soldier() {
        this.name = "士兵";
    }

    @Override
    public void useWeapon() {
        System.out.println(name + "拿出了大宝剑");
    }
}

在这个案例中我们定义了King、Queen、Knight和Soldier类,当然我们还可以实现更多的类来继承Character。可以看到,每个类中我们都实现了useWeapon()方法,每个角色都可以使用一种武器,接下来我们写一个测试方法来运行上面的代码:

public static void main(String[] args) {
    King king = new King();
    king.fight();

    Queen queen = new Queen();
    queen.fight();

    Knight knight = new Knight();
    knight.fight();

    Soldier soldier = new Soldier();
    soldier.fight();
}

最终运行结果如下:

国王拿出大宝剑
国王开始决斗了
皇后拿出魔法杖
皇后开始决斗了
骑士拿出了大宝剑
骑士开始决斗了
士兵拿出了大宝剑
士兵开始决斗了

从运行结果上看,我们基本实现了需求中描述的功能,每个角色都可以选择武器进行决斗,但再仔细想一下,以上的实现还存在一下三个问题:

  1. 需求中提到,每个角色在决斗时可以选择武器,也可以不选择武器,如果有一个角色Farmer没有武器,但是按照上面的方式,他必须要实现使用武器的方法,那么他在useWeapon()方法中将什么都不做,这与设计的初衷是不相符的;
  2. 需求中还提到,希望角色能够在游戏中*切换武器,但是按照上面的实现方式很难做到这一点,因为每个角色使用武器的代码都是写在各自的类中的,要修改的话必须修改各个类中的内容,无法做到游戏运行时切换武器;
  3. 在上面的实现中我们可以看到,King、Knight和Soldier使用的武器都是大宝剑,如果需要修改大宝剑武器的描述,那么就需要同时修改King、Knight和Soldier三个类,如果有很多的角色都是用了大宝剑作为武器,那么代码改动量将是很大的。

对于上面的问题,我们可以思考一下是否还有别的更好的实现方式?我们能否将武器抽象为一个接口,然后需要使用武器的角色只要实现这个接口就可以了?下面我们就来介绍第二种实现方式。

2.3、第二种实现方式

我们新定义一个接口Weapon表示武器,其中有一个方法useWeapon()表示使用武器,如果角色需要使用武器,只需要实现这个接口就可以了,如果角色不需要使用武器,就无需实现此接口,下面是我们基于此思路新设计的类图:

设计模式——策略模式

在这里我们修改了抽象类Character的定义,在抽象类中只定义了一个方法fight(),我们将useWeapon()方法抽出来定义在Weapon接口中,另外我们定义了五个角色,其中King、Queen、Knight、Soldier既继承Character也实现了Weapon接口,而Farmer类只继承了Character类,表明Farmer不使用武器。下面我们分别编码实现以上的类和接口,首先我们定义Character抽象类:

public abstract class Character {
    protected String name;

    public void fight() {
        System.out.println(name + "开始决斗了");
    }   
}

接着我们定义Weapon接口:

public interface Weapon {

    public void useWeapon();

}

然后我们定义五个角色:

public class King extends Character implements Weapon {
    public King() {
        this.name = "国王";
    }

    @Override
    public void useWeapon() {
        System.out.println(name + "拿出了大宝剑");
    }
}
//------------------------------------------------
public class Queen extends Character implements Weapon {
    public Queen() {
        this.name = "皇后";
    }

    @Override
    public void useWeapon() {
        System.out.println(name + "拿出了魔法杖");
    }
}
//------------------------------------------------
public class Knight extends Character implements Weapon {
    public Knight() {
        this.name = "骑士";
    }

    @Override
    public void useWeapon() {
        System.out.println(name + "拿出了大宝剑");
    }
}
//------------------------------------------------
public class Soldier extends Character implements Weapon {
    public Soldier() {
        this.name = "士兵";
    }

    @Override
    public void useWeapon() {
        System.out.println(name + "拿出了大宝剑");
    }
}
//------------------------------------------------
public class Farmer extends Character {
    public Farmer() {
        this.name = "农民";
    }
}

至此,我们已经完成了编码,现在我们可以写一个测试方法来测试功能:

public static void main(String[] args) {
    King king = new King();
    king.useWeapon();
    king.fight();

    Queen queen = new Queen();
    queen.useWeapon();
    queen.fight();

    Knight knight = new Knight();
    knight.useWeapon();
    knight.fight();

    Soldier soldier = new Soldier();
    soldier.useWeapon();
    soldier.fight();

    Farmer farmer = new Farmer();
    farmer.fight();
}

运行后结果如下:

国王拿出了大宝剑
国王开始决斗了
皇后拿出了魔法杖
皇后开始决斗了
骑士拿出了大宝剑
骑士开始决斗了
士兵拿出了大宝剑
士兵开始决斗了
农民开始决斗了

我们可以看到,因为Farmer没有实现Weapon接口,所以农民没有使用武器,这样避免了不使用武器仍然要实现useWeapon()方法的尴尬。但是上面的实现方式仍然没有解决另外两个问题:无法*地切换武器、如果要修改某种武器的描述需要改动大量的代码。接下来,我们可以通过策略模式来解决上述问题。

2.4、策略模式实现方式

首先我们再来分析一下需求,需求中描述武器有多种选择,也就是说,我们可以将使用武器功能独立出来,抽象为一个接口Weapon,并在其中定义一个方法useWeapon(),多种武器我们只需要定义多个Weapon接口的实现类即可。另外,因为角色在游戏中可以使用武器,那么,我们可以在角色Character中引入Weapon接口,而不是直接实现该接口,这样子不仅可以使角色在游戏中*切换武器,同时如果我们需要修改某个武器的属性,只需要修改Weapon接口的某一个实现类,武器与使用武器的角色之间是相互独立的,武器的变化不影响角色。因为Weapon是一个接口,所以对于角色在游戏中使用的具体武器,我们只需要将Weapon属性赋予指定的实现类即可,这样每个角色使用的武器就可以是任意实现Weapon接口的类。因为有角色可能不使用武器,那么我们只需要定义一个Weapon的实现类NoWeapon表示不使用武器。下面是我们基于此思路新设计的类图:

设计模式——策略模式

接下来我们就可以通过编码实现以上的功能,首先是定义Weapon接口:

public interface Weapon {
    public void useWeapon(String name); 
}

然后我们就可以分别定义几个武器类来实现Weapon接口:

public class SwordWeapon implements Weapon {

    @Override
    public void useWeapon(String name) {
        System.out.println(name + "拿出了大宝剑");
    }

}
//---------------------------------------------
public class WandWeapon implements Weapon {

    @Override
    public void useWeapon(String name) {
        System.out.println(name + "拿出了魔法杖");
    }

}
//---------------------------------------------
public class GunWeapon implements Weapon {

    @Override
    public void useWeapon(String name) {
        System.out.println(name + "掏出了手枪");
    }

}
//---------------------------------------------
public class NoWeapon implements Weapon{

    @Override
    public void useWeapon(String name) {
        System.out.println(name + "赤手空拳");
    }

}

其中我们定义了一个NoWeapon类表示没有武器,其实这个类中的useWeapon()方法可以不做任何操作,但是我们为了测试功能在其中添加了一句打印信息。

接下来是定义Character抽象类:

public abstract class Character {
    Weapon weapon;

    String name;

    public void fight() {
        takeWeapon();
        System.out.println(name + "开始决斗了");
    }

    public void takeWeapon() {
        weapon.useWeapon(name);
    }
}

我们可以看到,Character抽象类中定义了Weapon属性,并在使用武器的方法takeWeapon()中调用了Weapon的useWeapon()方法。最后我们定义几个角色即可:

public class King extends Character{

    public King() {
        this.weapon = new SwordWeapon();
        this.name = "国王";
    }

}
//---------------------------------------------
public class Queen extends Character {

    public Queen() {
        this.weapon = new WandWeapon();
        this.name = "皇后";
    }

}
//---------------------------------------------
public class Knight extends Character {

    public Knight() {
        this.weapon = new WandWeapon();
        this.name = "骑士";
    }

}
//---------------------------------------------
public class Soldier extends Character {

    public Soldier() {
        this.weapon = new SwordWeapon();
        this.name = "士兵";
    }

}
//---------------------------------------------
public class Farmer extends Character {

    public Farmer() {
        this.weapon = new NoWeapon();
        this.name = "农民";
    }

}

我们写一个测试方法来测试上述功能:

public static void main(String[] args) {
    King king = new King();
    king.fight();

    Queen queen = new Queen();
    queen.fight();

    Knight knight = new Knight();
    knight.fight();

    Soldier soldier = new Soldier();
    soldier.fight();

    Farmer farmer = new Farmer();
    farmer.fight();
    farmer.weapon = new GunWeapon();
    farmer.fight();
}

运行结果为:

国王拿出了大宝剑
国王开始决斗了
皇后拿出了魔法杖
皇后开始决斗了
骑士拿出了魔法杖
骑士开始决斗了
士兵拿出了大宝剑
士兵开始决斗了
农民赤手空拳
农民开始决斗了
农民掏出了手枪
农民开始决斗了

从运行结果我们可以看到,农民本来是没有武器的,他在运行时切换了武器,这样农民就有了武器,达到了角色可以*地切换武器的目的。另外,如果我们需要修改某个武器的属性,只需要修改其定义类中的信息即可,无需修改每个使用它的角色。

3、总结

通过上面的案例的讲述我们已经基本了解了策略模式,那么什么时候需要使用策略模式呢?我认为当一个系统中有多个相似的类,这些类的功能是类似的,能区分它们的就是它们的行为,这些类的变化对于类的使用者来说是独立的,这时候就可以使用策略模式。

策略模式的优点在于:1、算法之间可以*地切换;2、有较好的扩展性。

当然策略模式也有缺点,最明显的一个缺点就是如果系统中的策略较多,那么就要定义很多的策略类。

4、参考资料

[1] : http://www.runoob.com/design-pattern/strategy-pattern.html “策略模式”
[2] : https://blog.csdn.net/u012124438/article/details/70039943 “设计模式学习之策略模式”