前面一小节,咱们完成了坦克大战游戏中坦克类的设计和基本的移动、转向行为的实现。然而,我们的坦克还是自己手动new
出来的,更好的办法就是把生产坦克的任务交给工厂来完成,这就是本小节要给大伙儿介绍的用抽象工厂模式来生产坦克。
客户端API
我们先客户端使用的角度来感受下使用了抽象工厂后,构建不同类型的坦克实例有多么的便利:
package com.pf.java.tankbattle;
import ...
public class MyPanel extends JPanel {
private List<EnemyTank> enemyTanks;
private HeroTank heroTank;
...
public MyPanel(MyFrame frame) {
...
enemyTanks = new ArrayList<>();
// 获取单例的具体工厂——敌方坦克工厂实例
EnemyTankFactory enemyTankFactory = EnemyTankFactory.getInstance();
// 基于配置对象进行构建
enemyTanks.add(enemyTankFactory.build(new EnemyTankConfig(EnemyType.PAWN, 0, 32, frame)));
enemyTanks.add(enemyTankFactory.build(new EnemyTankConfig(EnemyType.AWL, 32 * 2, 32, frame)));
enemyTanks.add(enemyTankFactory.build(new EnemyTankConfig(EnemyType.ARTILLERY, 32 * 4, 32, frame)));
enemyTanks.add(enemyTankFactory.build(new EnemyTankConfig(EnemyType.IRON_MAN, 32 * 6, 32, frame)));
// 为钢铁侠类型的坦克设置装甲颜色
enemyTanks.add(enemyTankFactory.build(new EnemyTankConfig(EnemyType.IRON_MAN, 32 * 7, 32 * 2, TankColor.YELLOW, frame)));
enemyTanks.add(enemyTankFactory.build(new EnemyTankConfig(EnemyType.IRON_MAN, 32 * 8, 32, TankColor.GREEN, frame)));
// 从单例的英雄坦克工厂构建一个英雄实例
heroTank = HeroTankFactory.getInstance().build(new TankConfig(32 * 4, 32 * 6, frame));
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
// 以下为各种类型坦克的绘制
heroTank.paint(g);
for (EnemyTank enemyTank : enemyTanks) {
enemyTank.paint(g);
}
}
}
运行效果:
通过使用具体的工厂实例我们要build
一个坦克实例,只需要传一个TankConfig
的配置对象即可,在实例化配置对象时我们传入必要的参数即可。然后由具体的工厂帮我们创建各种坦克的实例,岂不快哉!
抽象工厂类设计
基本设计
从客户端API的使用,我们感受到抽象工厂的好处体现在,我们可以扩展出很多具体的工厂来负责具体产品的生产,而这里则是具体的坦克类型。回到我们最初的类设计上来理解下抽象工厂,所谓的抽象工厂,顾名思义,它是一个抽象类,生产的东西也应该是抽象的,而不是具体的实现。为此,咱们的坦克类应该设计为抽象的,即用abstract
来修饰类。看下坦克类设计调整后的类图:
说明
这里省略了之前定义好的属性和方法。为了证实我们有必要把
Tank
从一个普通类调整为一个抽象类,这里暂时为其添加了一个消费道具的方法(先做空实现),其实现逻辑为,判断吃到不同道具后,进行相关的逻辑处理。因此,在Tank
的consume(Prop prop)
方法中会封装这个判断和处理流程;而其中某个环节,比如吃到铲子,玩家坦克和敌方坦克的处理逻辑肯定不一样,则提供相应的抽象方法,让子类来实现。通过抽象类的设计,很显然我们会不自觉的使用到模板方法设计模式。
再回到正题,有了抽象的坦克类,则对应有一个抽象工厂来生产它:
package com.pf.java.tankbattle.pattern.factory;
import ...
public abstract class TankFactory<T extends TankConfig> {
public abstract Tank build(T config);
}
该抽象工厂类有一个抽象的build
方法,由它来生产Tank
,注意返回类型为抽象的Tank
类,而关于需要传入的参数,我们则是在TankFactory
类上声明了一个继承自TankConfig
基类的泛型,用该类型作为工厂方法的入参类型,实际传入的config
可以是TankConfig
的子类型,这个后文会介绍。注意,这里我们使用的是配置对象,而不是具体的参数列表,因为具体的工厂子类在创建具体的产品时需要的参数不尽相同,我们无法用一个固定的参数列表来涵盖所有的参数,如果我们这么做,这会让客户端很疑惑,究竟哪些参数是必传的。
看下基础配置类TankConfig
:
package com.pf.java.tankbattle.pattern.factory;
import ...
public class TankConfig {
private int x;
private int y;
private TankColor color;
private MyFrame frame;
public TankConfig(int x, int y, TankColor color, MyFrame frame) {
this.x = x;
this.y = y;
this.color = color;
this.frame = frame;
}
public TankConfig(int x, int y, MyFrame frame) {
this.x = x;
this.y = y;
this.frame = frame;
}
// 省略getter和setter
}
这里包含了坦克起始坐标点(x, y)
、坦克的颜色枚举类型和游戏窗体对象。坦克图片素材涉及三种颜色的坦克,枚举定义如下:
package com.pf.java.tankbattle.enums;
/**
* 坦克的颜色枚举
*/
public enum TankColor {
GRAY, YELLOW, GREEN
}
以上定义的配置类型TankConfig
可以被扩展,比如,接下来我们会实现一个生产敌军坦克的具体工厂类,它的build
方法会接收一个TankConfig
的子类型:
package com.pf.java.tankbattle.pattern.factory;
import ...
public class EnemyTankConfig extends TankConfig {
/** 敌方坦克类型 */
private EnemyType enemyType;
/** 聪明的坦克可以实现更多的智能 */
private boolean smart;
/** 击中有赏,可以产生道具 */
private boolean bonus;
public EnemyTankConfig(EnemyType enemyType, int x, int y, MyFrame frame) {
super(x, y, frame);
this.enemyType = enemyType;
}
public EnemyTankConfig(EnemyType enemyType, int x, int y, TankColor color, MyFrame frame) {
super(x, y, color, frame);
this.enemyType = enemyType;
}
public EnemyTankConfig(EnemyType enemyType, boolean smart, boolean bonus, int x, int y, TankColor color, MyFrame frame) {
super(x, y, color, frame);
this.enemyType = enemyType;
this.smart = smart;
this.bonus = bonus;
}
// 省略当前类字段的getter和setter
}
如果构建的是EnemyTank
,咱们可以设置更多的参数。这里的EnemyType
类型是我们对敌方坦克的分类,一共有4类,看下枚举类:
package com.pf.java.tankbattle.enums;
/**
* 敌军坦克枚举类
*/
public enum EnemyType {
/** 小兵 */
PAWN(4, 80),
/** 锥子 小快灵,跑得快 */
AWL(6, 160),
/** 炮手 炮弹快 */
ARTILLERY(8, 100),
/** 钢铁侠 装甲厚,跑得慢 */
IRON_MAN(10, 60);
private int picIndex;
private int speed;
EnemyType(int picIndex, int speed) {
this.picIndex = picIndex;
this.speed = speed;
}
// 省略getter
}
在这个枚举类中,按照不同的枚举类型,对敌方坦克要绘制的图片的picIndex
和坦克的移动速度做了出厂设置。picIndex
对应如下图:
具体工厂实现
这里包含了两个具体实现我方坦克工厂和敌方坦克工厂。分别看下具体实现:
我方坦克工厂:
package com.pf.java.tankbattle.pattern.factory.impl;
import ...
public class HeroTankFactory extends TankFactory<TankConfig> {
private static final HeroTankFactory INSTANCE = new HeroTankFactory();
private HeroTankFactory() {
System.out.println("init HeroTankFactory...");
}
@Override
public HeroTank build(TankConfig config) {
// 我方坦克初始化方向朝上,速度为每秒80像素,绘制为小黄坦克
return new HeroTank(Direction.UP, config.getX(), config.getY(), 80, 0, config.getFrame());
}
public static HeroTankFactory getInstance() {
System.out.println("invoke getInstance()");
return INSTANCE;
}
}
因为基类上声明了泛型类,这里在定义HeroTankFactory
时需要在继承的基类上指定泛型类型,以确定重写的build
方法的参数类型,而返回值为具体工厂所要创建的相应的坦克类型,这里是HeroTank
。在方法实现中,我们基于配置对象以及一些默认的参数值,通过new
的方式来创建对象实例。
扩展学习
这里对
HeroTankFactory
使用了单例模式,这种在内部使用私有的静态成员并提供一个公共的静态方法获取实例的方式,一些教程上也称之为“饿汉式”。究竟何时会调用私有的HeroTankFactory
无参构造呢,我们测试发现,第一次调用HeroTankFactory.getInstance()
时,才会有以下信息的打印输出:init HeroTankFactory... invoke getInstance()
可以理解为第一次访问其一个静态方法,此时才会基于其静态成员变量的初始化赋值进行当前类的实例化,然后才是该静态方法的调用。通过这种自己编码实践的形式,大伙儿对知识的认知会多一层自己的理解,作者的本意是不希望大家一味的将网上教程的理论总结视为宝典。
再来看敌方坦克工厂的实现:
package com.pf.java.tankbattle.pattern.factory.impl;
import ...
public class EnemyTankFactory extends TankFactory<EnemyTankConfig> {
private static final EnemyTankFactory INSTANCE = new EnemyTankFactory();
private EnemyTankFactory() {}
@Override
public EnemyTank build(EnemyTankConfig config) {
EnemyType type = config.getEnemyType();
int picIndex = type.getPicIndex();
TankColor color = config.getColor();
// 钢铁侠有三种颜色的可选
if (type == EnemyType.IRON_MAN) {
if (TankColor.YELLOW == color) {
picIndex += 3;
} else if (TankColor.GREEN == color) {
picIndex += 2;
}
}
return new EnemyTank(Direction.DOWN, config.getX(), config.getY(), type.getSpeed(), picIndex, config.getFrame());
}
public static EnemyTankFactory getInstance() {
return INSTANCE;
}
}
实现形式同前面的HeroTankFactory
。我们发现,通过具体工厂的工厂方法来创建对象,能很好的在内部封装参数之间的联系,而对外提供更明确的设置,从而避免了外界直接通过调用构造器时传入非法的口径不统一的参数,而最终导致的对象创建失败。比如这里,当敌方坦克类型为钢铁侠时,外部可提供额外的颜色设置,而在内部确定picIndex
参数值。钢铁侠默认的picIndex
为10,如果指定了颜色则pixIndex
变更的逻辑可参考下面的示意图:
最后,附上较为完整的设计类图,大家加油!