背景
设计模式有3大类,分为:创建型模式、结构型模式和行为型模式。工厂模式属于创建型模式,创建型模式提供了一种在创建对象的同时隐藏创建逻辑的方式,而不是使用 new运算符直接实例化对象。这使得程序在判断针对某个给定实例需要创建哪些对象时更加灵活。
简单工厂
描述:定义一个类用于创建父类相同的子类对象,由传入参数决定创建哪个子类。
举个例子,我喜欢玩游戏。定义一个Game接口,让具体的游戏去实现这个接口1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17interface Game {
void play();
}
class HeartStone implements Game {
public void play() {
System.out.println("炉石传说,启动!");
}
}
class Gwent implements Game {
public void play() {
System.out.println("昆特牌,启动!");
}
}
现在我们要开始玩了1
2
3
4
5
6public class SimpleFactory {
public static void main(String[] args) {
Game game = new HeartStone();
game.play();
}
}
现在假设我们的程序是个大型系统,提供游戏启动的代理功能,一开始我们玩的是炉石传说,因此项目中有很多地方的代码都使用 Game game = new HeartStone(); 这样的语句来启动游戏。
某天,我觉得炉石传说随机性太大了,辣鸡炉石!咱们去玩昆特牌吧!那么问题来了,想更换启动的游戏,我就得将整个系统的每一处Game game = new HeartStone(); 改成 Game game = new Gwent(); 可见工作量是十分庞大的。
简单工厂模式就是为了解决这类问题的:具体玩什么游戏应该是随时变动的需求,不应该在程序中写死具体实例化哪个游戏子类。
现在添加一个游戏工厂类1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17class GameFactory {
private static final String HeartStone = "HeartStone";
private static final String Gwent = "Gwent";
public static Game playGame(String game) {
Game myGame = null;
switch (game) {
case HeartStone:
myGame = new HeartStone();
break;
case Gwent:
myGame = new Gwent();
break;
}
return myGame;
}
}
主程序改为1
2
3
4
5
6public class SimpleFactory {
public static void main(String[] args) {
Game game = GameFactory.playGame("Gwent");
game.play();
}
}
现在,我们想启动什么游戏,只需要改动playGame中的参数,而这个参数是一个字符串变量,这就意味着,我们还可以以配置文件的方式为这个字符串变量赋值,最终做到,不改动任何一处代码,只修改配置文件中的游戏信息,就可以切换具体实例化哪个游戏。
简单工厂模式的最大优点在于工厂类中包含了必要的逻辑判断(switch),根据客户端的选择动态实例化相关的 类,对于客户端来说,去除了与具体产品的依赖。
工厂方法
描述:定义一个接口用于创建对象,但是让子类决定初始化哪个类。工厂方法把一个类的初始化下放到子类。
在简单工厂模式中,我们发现在添加子类的时候,相应的也需要在工厂类中添加一个判断分支(多加一个case),是违背了开放-封闭原则的。而工厂方法模式就是主要解决这个问题的。
开放-封闭原则:软件实体(类、模块、函数等)应该可以扩展,但是不可修改。
回到玩游戏的例子,现在我又想玩LOL了,现在我需要添加一个LOL类1
2
3
4
5
6class LOL implements Game {
public void play() {
System.out.println("英雄联盟,启动!");
}
}
在简单工厂模式下,还需要改动GameFactory的代码,添加一个Case,这样修改了源代码,违背了开闭原则。现在将简单工厂模式改成工厂方法模式,把GameFactory改成接口1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17interface GameFactory {
Game playGame();
}
class HeartStoneFactory implements GameFactory {
public Game playGame() {
return new HeartStone();
}
}
class GwentFactory implements GameFactory {
public Game playGame() {
return new Gwent();
}
}
这样,想添加LOL这个游戏,只需要再添加一个工厂类即可,不用修改代码,而是扩展代码。1
2
3
4
5
6class LOLFactory implements GameFactory {
public Game playGame() {
return new LOL();
}
}
主程序为1
2
3
4
5
6
7public class FactoryMethod {
public static void main(String[] args) {
GameFactory gameFactory = new LOLFactory();
Game game = gameFactory.playGame();
game.play();
}
}
可以看到使用工厂方法模式之后,扩展性变高了,如果想增加一个游戏,只要扩展一个游戏工厂类就可以。但是随之而来的是在系统中增加了复杂度,每增加一个游戏时,都需要增加一个游戏类和工厂类。
抽象工厂
描述:为一个产品族提供了统一的创建接口。当需要这个产品族的某一系列的时候,可以从抽象工厂中选出相应的系列创建一个具体的工厂类。
抽象工厂模式是一种特殊的工厂方法模式。在上面的玩游戏例子中,游戏工厂接口(GameFactory)的子类,只实例化一种游戏父类(Game)。这时工厂方法模式就可以满足需求。但我们知道,游戏是分为很多种类型的,如MOBA、RPG、TCG等等。
现在我们把Game接口拆分成RPGGame接口和CardGame接口,需求变为:GameFactory的子类可以实例化多种游戏父类了(RPGGame、CardGame)。这时候就要用到抽象工厂模式。
原先我们讨论游戏时,说的是一个很宽泛的概念,我只知道你想玩游戏,不知道你想玩什么类型的游戏。现在给你两个选项:角色扮演游戏和卡牌游戏。有两家知名游戏厂商,波兰蠢驴和暴雪,他们都有角色扮演类游戏和卡牌类游戏。
那么众所周知,蠢驴的RPG游戏有巫师系列,卡牌游戏是昆特牌,暴雪的RPG游戏有魔兽世界,卡牌游戏有炉石传说。这些具体的游戏都叫做产品 而游戏这个大类则是产品族,巫师和昆特牌是蠢驴的产品族;魔兽世界和炉石传说是暴雪的产品族。
抽象工厂模式就是描述它们之间的关系的:将同一类的产品子类归为一类,让他们继承同一个接口,(巫师和魔兽世界都是RPG,让它们都继承RPG接口),然后将不同类的产品归为一族,让不同类的产品都可以被一个工厂子类实例化(魔兽世界和炉石传说是不同类的游戏,但都可以被暴雪公司实例化)。
通过代码直观展示:
游戏类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
35interface RPGGame {
void play();
}
interface CardGame {
void play();
}
class HeartStone implements CardGame {
public void play() {
System.out.println("炉石传说,启动!");
}
}
class Gwent implements CardGame {
public void play() {
System.out.println("昆特牌,启动!");
}
}
class WOW implements RPGGame {
public void play() {
System.out.println("魔兽世界,启动!");
}
}
class Witcher implements RPGGame {
public void play() {
System.out.println("巫师,启动!");
}
}
游戏工厂类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
29interface GameFactory {
RPGGame playRPGGame();
CardGame playCardGame();
}
class CDProjektRed implements GameFactory {
public RPGGame playRPGGame() {
return new Witcher();
}
public CardGame playCardGame() {
return new Gwent();
}
}
class Blizzard implements GameFactory {
public RPGGame playRPGGame() {
return new WOW();
}
public CardGame playCardGame() {
return new HeartStone();
}
}
主程序1
2
3
4
5
6
7
8
9
10
11
12
13
14public class AbstractFactory {
public static void main(String[] args) {
GameFactory gameFactory1 = new CDProjektRed();
GameFactory gameFactory2 = new Blizzard();
RPGGame game1 = gameFactory1.playRPGGame();
CardGame game2 = gameFactory1.playCardGame();
RPGGame game3 = gameFactory2.playRPGGame();
CardGame game4 = gameFactory2.playCardGame();
game1.play();
game2.play();
game3.play();
game4.play();
}
}
现在,我们的游戏启动代理系统的功能就被进一步完善了,如果你是蠢驴的舔狗,你只想代理启动蠢驴的游戏,只需要修改一处代码GameFactory gameFactory = new CDProjektRed(); 就可以一键设置为蠢驴游戏全家桶,对于客户端来说,它们并不知道自己会启动哪个厂商的游戏,因为这对它们是透明的,客户端只知道自己启动了RPGGame和CardGame. 如果哪天你又变成暴雪舔狗了,也只需要改动一处代码,客户端就会启动暴雪的游戏族。
参考资料: