持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第23天,点击查看活动详情
概念
在程序设计中,工厂类一般是对对象构造、实例化、初始化过程的封装。而 工厂方法(Factory Method) 则可以升华为一种设计模式,它对工厂制造方法进行接口规范化,以允许子类工厂决定具体制造哪类产品的实例,最终降低系统耦合,使系统的可维护性、可扩展性等得到提升。
实例演示
本文我们还是以飞机大战为例,来介绍工厂方法。
1. 传统实例对象
在介绍工厂方法模式之前,我们先看一下传统实例化对象,要实例化对象,就得泳道关键字 new,比如:Plane plane = new Plane();或者是还会有一些复杂的初始化代码,这就是我们常用的传统构造方式。
然而这样做的结果会使飞机对象的产生代码被牢牢地硬编码在客户端类里,也就是说客户端与实例化过程强耦合了。而事实上,我们完全不必关心产品的制造过程(实例化、初始化),而将这个任务交由相应的工厂来全权负责,工厂最终能交付产品供我们使用即可,如此我们便摆脱了产品生产方式的束缚,实现了与制造过程彻底解耦。
2. 抽象类建模
在使用工厂方法创建产品(飞机、坦克...)之前,我们得先给产品建模。我们先实现一个抽象类来定义所有敌人的父类。代码中我们仅定义了 show 方法,当然还会有其他更丰富的功能,比如攻击、移动等,这里我们只是举例说明一下。
public abstract class Enemy {
//敌人的坐标
protected int x;
protected int y;
//初始化坐标
public Enemy(int x, int y){
this.x = x;
this.y = y;
}
//抽象方法,在地图上绘制
public abstract void show();
}
接下来我们来实现两个敌人,飞机、坦克。飞机类 Airplane 和坦克类 Tank 都继承了敌人抽象类 Enemy,并且分别实现了各自独特的展示方法 show(),这里面有个小细节,其中坦克应该绘制在下层(但在地图层之上)图层,而飞机则绘制在上层图层,这样才能遮盖住下层的所有图层以达到期望的视觉效果。
public class Airplane extends Enemy {
public Airplane(int x, int y){
super(x, y);//调用父类构造方法初始化坐标
}
@Override
public void show() {
System.out.println("绘制飞机于上层图层,出现坐标:" + x + "," + y);
System.out.println("飞机向玩家发起攻击……");
}
}
public class Tank extends Enemy {
public Tank(int x, int y){
super(x, y); //调用父类构造方法初始化坐标
}
@Override
public void show() {
System.out.println("绘制坦克于下层图层,出现坐标:" + x + "," + y);
System.out.println("坦克向玩家发起攻击……");
}
}
3. 实例化对象
我们对产品建完模之后,接下来就该考虑如何来实例化和初始化这些敌人了。以我们的经验来说,这些敌人首先要出现在屏幕的正上方,这样就得设置其纵坐标初始化为 0 了,而为了保证一个更好的体验,敌人应该随机出现在屏幕各个位置,因为其横坐标要设置为随机值。相关代码如下:
public class Client {
public static void main(String[] args) {
int screenWidth = 100;//屏幕宽度
System.out.println("游戏开始");
Random random = new Random();//准备随机数
int x = random.nextInt(screenWidth);//生成敌机横坐标随机数
Enemy airplan = new Airplane(x, 0);//实例化飞机
airplan.show();//显示飞机
x = random.nextInt(screenWidth);//坦克同上
Enemy tank = new Tank(x, 0);
tank.show();
/*输出结果:
游戏开始
飞机出现坐标:94,0
飞机向玩家发起攻击……
坦克出现坐标:89,0
坦克向玩家发起攻击……
*/
}
4. 简单工厂实例化(工厂类)
然而,上面的代码还有些问题,即 制造随机出现的敌人这个动作貌似不应该出现在客户端类中。
试想如果我们还有其他敌人也需要构造的话,那么 同样的代码就会再次出现,尤其是当初始化越复杂的时候重复代码就会越多。
既然如此,那么我们为什么不把这些实例化逻辑抽象出来作为一个工厂类呢?接下来我们来看看相关实现代码。
public class SimpleFactory {
private int screenWidth;
private Random random;//随机数
public SimpleFactory(int screenWidth) {
this.screenWidth = screenWidth;
this.random = new Random();
}
public Enemy create(String type){
int x = random.nextInt(screenWidth);//生成敌人横坐标随机数
Enemy enemy = null;
switch (type) {
case "Airplane":
enemy = new Airplane(x, 0);//实例化飞机
break;
case "Tank":
enemy = new Tank(x, 0);//实例化坦克
break;
}
return enemy;
}
}
客户端只需要传入不同的敌人种类,即可生产出相应的实例对象。如此一来,制造敌人这个任务就全权交由简单工厂来负责了,于是客户端便可以直接从简单工厂取用敌人了。相关代码如下:
public class Client {
public static void main(String[] args) {
System.out.println("游戏开始");
SimpleFactory factory = new SimpleFactory(100);
factory.create("Airplane").show();
factory.create("Tank").show();
}
}
5. 工厂方法实例对象
上面的代码中,客户端类的代码变得异常简单、清爽,这就是分类封装、各司其职的好处。
然而,这个简单工厂的确很“简单”,但并不涉及任何的模式设计范畴,虽然客户端中不再直接出现对产品实例化的代码,但其实只是 制造逻辑被换了个地方,挪到了简单工厂中而已,并且客户端还要告知产品种类才能产出,这无疑是另一种意义上的耦合。
除此之外,简单工厂一定要保持简单,否则就不要用简单工厂。
随着游戏项目需求的演变,简单工厂的可扩展性也会变得很差,例如对于那段对产品种类的判断逻辑,如果有新的敌人类加入,我们就需要再修改简单工厂。随着生产方式不断多元化,工厂类就得被不断地反复修改,严重缺乏灵活性与可扩展性,尤其是对于一些庞大复杂的系统,大量的产品判断逻辑代码会被堆积在制造方法中,看起来好像功能强大、无所不能,其实维护起来举步维艰,简单工厂就会变得一点也不简单了。
当然系统中并不是处处都需要调用这样一个万能的“简单工厂”,有时系统只需要一个坦克对象,所以我们不必大动干戈使用这样一个臃肿的“简单工厂”。另外,由于用户需求的多变,我们又不得不生成大量代码,这其实是我们要解决的问题。
对于这里问题,我们的解决方式是,将简单工厂的制造方法进行拆分,构建起抽象化、多态化的生产模式,即所谓的工厂方法。
因此,我们需要先建立一个工厂接口,工厂接口 Factory 其实就是工厂方法模式的核心。代码如下:
public interface Factory {
Enemy create(int screenWidth);
}
接着我们来重构下之前的简单工厂类,将其按照产品种类拆分成两个类:
public class AirplaneFactory implements Factory {
@Override
public Enemy create(int screenWidth) {
Random random = new Random();
return new Airplane(random.nextInt(screenWidth), 0);
}
}
public class TankFactory implements Factory {
@Override
public Enemy create(int screenWidth) {
Random random = new Random();
return new Tank(random.nextInt(screenWidth), 0);
}
}
如上所示,飞机工厂类 AirplaneFactory 与坦克工厂类 TankFactory 的代码简洁、明了,它们都以关键字 implements 声明了本类是实现工厂接口 Factory 的工厂实现类,并且给出了工厂方法 create() 的具体实现,其中飞机工厂制造飞机,坦克工厂制造坦克,各自有其独特的生产方式。
这样一来,我们新增产品实例就方便多了。比如在游戏中一定会有一个大 BOSS,那我们来实现一下 BOSS 的工厂类。
public class Boss extends Enemy {
public Boss(int x, int y){
super(x, y);
}
@Override
public void show() {
System.out.println("Boss出现坐标:" + x + "," + y);
System.out.println("Boss向玩家发起攻击……");
}
}
public class BossFactory implements Factory {
@Override
public Enemy create(int screenWidth) {
// 让Boss出现在屏幕中央
return new Boss(screenWidth / 2, 0);
}
}
这里需要注意一下,因为 Boss 出现的坐标总是处于屏幕的中央位置,所以关底 Boss 工厂类 BossFactory 在初始化时设置 Boss 对象的横坐标为屏幕宽度的一半,而不是随机生成横坐标。
客户端生成实例代码如下:
public class Client {
public static void main(String[] args) {
int screenWidth = 100;
System.out.println("游戏开始");
Factory factory = new TankFactory();
for (int i = 0; i < 5; i++) {
factory.create(screenWidth).show();
}
factory = new AirplaneFactory();
for (int i = 0; i < 5; i++) {
factory.create(screenWidth).show();
}
System.out.println("抵达关底");
factory = new BossFactory();
factory.create(screenWidth).show();
}
}
通过工厂方法实现类的实例化之后,之后若要加入新的敌人类,只需添加相应的工厂类,无须再对现有代码做任何更改,保证游戏系统具有良好的兼容性和可扩展性。
总结
最后我们来做一下总结。
不同于简单工厂,工厂方法模式可以被看作由简单工厂演化而来的高级版,后者才是真正的设计模式。
在工厂方法模式中,不仅产品需要分类,工厂同样需要分类,与其把所有生产方式堆积在一个简单工厂类中,不如把生产方式放在具体的子类工厂中去实现,这样做对工厂的抽象化与多态化有诸多好处,避免了由于新加入产品类而反复修改同一个工厂类所带来的困扰,使后期的代码维护以及扩展更加直观、方便。
工厂方法模式的各角色定义如下。
Product(产品):所有产品的顶级父类,可以是抽象类或者接口。对应本章例程中的敌人抽象类。ConcreteProduct(子产品):由产品类 Product 派生出的产品子类,可以有多个产品子类。ConcreteProduct 继承 Product。对应本章例程中的飞机类、坦克类以及关底 Boss 类。Factory(工厂接口):定义工厂方法的工厂接口,当然也可以是抽象类,它使顶级工厂制造方法抽象化、标准统一化。ConcreteFactory(工厂实现):实现了工厂接口的工厂实现类,并决定工厂方法中具体返回哪种产品子类的实例。
工厂方法模式不但能将客户端与敌人的实例化过程彻底解耦,抽象化、多态化后的工厂还能让我们更自由灵活地制造出独特而多样的产品。
参考文档
- 《秒懂设计模式》—— 刘韬