浅谈设计模式 - 工厂模式(六)
前言:
在第一篇里面已经介绍过简单工厂了,但是工厂模式里面不仅仅是简单工厂,还存在工厂方法和抽象工厂,并且从严格意义来讲简单工厂不能算是一种设计模式,本次的文章针对工厂的进化来展开讲一讲工厂模式的三种常见形式:简单工厂、工厂方法、抽象工厂。
文章目的
- 了解简单工厂这种代码编写形式的优点,回顾工厂模式
- 了解如何从简单工厂扩展到工厂方法以及抽象工厂
- 对比工厂方法和抽象工厂的异同。
- 总结简单工厂,工厂方法和抽象工厂,对比优缺点和特点
如何辨别工厂模式
工厂模式一般从类的命名就可以直接看到含义,所以一般情况下很容易看出工厂模式的应用。
- 工厂模式主要是负责对象的创建
- 无论是创建者还是使用者,都是针对一个抽象对象的实现。
- 工厂模式最关注的是对象是如何创建的而不是对象的使用。它针对的是创建这一个过程。
工厂模式的具体介绍
简单工厂模式
简单工厂模式的介绍:https://juejin.cn/post/692206...
之前文章已经介绍过简单工厂模式,我们直接看一下简单工厂是如何设计的,从严格的意义上来说,简单工厂是一种良好的“编程习惯”,他很好的解耦了创建对象和使用对象这两个不同的过程。做到“单一职责”的原则
从上面的图当中我们构建基本的工厂类和对应的实现子类以及对应的产品抽象类。
下面回顾一下简单工厂的优缺点
优点:
- 使用创建工厂的方法,我们实现了获取具体对象和生产对象的解耦,由生产对象的工厂通过我们传入的参数生产对应的对象,调用方只需要传递需要生产的对象来实现具体的效果。
- 解耦了创建和被创建的过程。
- 根据不同的逻辑判断生成不同的具体对象。
缺点:
- 每增加一个工厂对象具体的实现类,就需要增加
if/else
不利于维护 - 大量的子类会造成工厂类的代码迅速膨胀和臃肿
- 简单工厂的方法一般处理简单的业务逻辑,如果创建逻辑复杂不建议使用。
从上面的优缺点分析可以知道,简单工厂并不能完全解决对象的创建解耦,对于对象的创建细节容易造成耦合,同时如果创建的对象过多容易出现臃肿的工厂代码。
工厂方法模式
工厂方法模式:定义了创建对象的接口方法,但是具体的创建过程由子类来决定。工厂方法将创建的过程延迟到子类,工厂方法是对简单工厂的扩展和升级,为了解决简单工厂破坏了“开放-关闭原则”的问题而做的改进。我们将具体的产品进行了抽象的同时,将创建对象的过程延迟到子类进行实现。
工厂方法的结构图
下面为工厂方法的结构图,我们由简单工厂转变为工厂方法之后,工厂类定义增加了抽象的对象创建方法,由子类通过继承的方式实现工厂的抽象方法并且实现自己的构建过程。
+ Product 产品类,定义产品的公用方法和抽象类
+ ConcreteProduct 产品的具体实现子类,包含具体产品的实现
+ Factory 工厂类,定义工厂的创建方法以及需要子类继承实现的方法
+ ConcreteFactory 工厂的实现类,由子工厂来决定生成的具体产品和定义生产的具体过程。
工厂方法的特点
下面是工厂方法的具体特点
- 创建的过程解耦到子类,由子类决定创建的过程和结果
- 具体的产品和工厂之间存在必要关联,同时可以使用任意子类产品进行替换
- 需要依靠继承的形式由子工厂来决定生产的过程,子类决定产品创建的结果
提醒:子类决定创建的结果并不是字面上的创建,而是由调用者决定的。子类决定的是具体针对哪一个实例进行生产,但是生成的具体结果还是控制在创建者的身上
简单工厂和工厂方法有什么区别
- 简单工厂是对产品的创建过程进行“封装”,同时创建新的产品必须改动工厂代码。
- 工厂方法是对简单工厂的升级,工厂方法可以控制具体对象的创建以及由子类来决定具体需要创建哪一个对象。
- 简单工厂只是单纯的解耦创建者和使用者,但是简单工厂无法改变创建的结果。
抽象工厂模式
抽象工厂模式:提供接口,通过定义抽象方法的形式,通过实现具体工厂方法实现创建具体对象家族,同时不需要指定特殊的类。
抽象工厂的内部往往使用工厂方法进行实现,两者经常被弄混,从结构上来看,他们最大的区别在于工厂方法往往使用继承实现,而抽象工厂往往使用内部继承工厂方法的接口实现。区分工厂方法和抽象工厂也是工厂模式的学习关键。
抽象工厂的结构图
由于抽象工厂更像是对工厂方法的改进,我们定义抽象工厂的结构图,抽象工厂的结构相比工厂方法要复杂一些:
可以参考抽象工厂和工厂方法的结构图,看看两者的异同
+ FactoryInterface 抽象工厂接口,定义一批抽象对象的生产接口
+ ConcreteFactoryA 抽象工厂实现类A,实现抽象工厂接口。
+ ConcreteFactoryB 抽象工厂实现类B,实现抽象工厂接口。
+ ProductA 抽象产品A,定义公共的抽象方法或者公用属性
+ ConcreteProductA 具体实现产品A
+ ConcreteProductA 具体实现产品A
+ ProductB 抽象产品B,定义公共的抽象方法或者公用属性
+ ConcreteProductB 具体实现产品B
+ ConcreteProductB 具体实现产品B
抽象工厂的特点:
- 所有的具体工厂都实现同一个抽象工厂接口。
- 生产的结果实现类可以自由实现具体类或者其扩展类的实例。
- 抽象工厂的痛点在于扩展一个新的产品生产会造成所有的具体工厂的改动,也包含了产品类的变动。
- 抽象工厂往往包含了一系列的工厂方法
抽象工厂和工厂方法的区别
- 抽象工厂定义抽象接口依靠子类实现创建的过程,而工厂方法针对子类实现具体的对象创建细节
- 工厂方法需要使用继承的手段实现工厂方法“埋藏”工厂创建具体对象的细节
- 工厂方法对于处理“独立”产品的创建非常有效,而抽象工厂往往用于处理生产多个存在关联的产品对象。
实际案例
依旧参考坦克大战的案例,介绍如何改造坦克大战的具体代码。
模拟场景
依然以经典的任天堂游戏坦克大战为例,在进入游戏的关卡的时候,会出现我方的坦克和敌人的坦克,我方坦克和地方坦克不仅形状不同,而且很脆,但是敌人的坦克根据颜色需要打好几枪才会毁灭,那么如果用代码来模拟是什么样的呢?
简单工厂实现:
使用简单工厂实现的代码如下:
使用简单工厂类来管理坦克的创建过程,简单工厂顾名思义,就是简单的将创建对象的过程进行管理。
增加工厂类 TankFactory.java
用工厂来管理具体的坦克创建过程:
/**
* 坦克工厂,专门负责生产坦克
*
* @author zxd
* @version 1.0
* @date 2021/1/25 22:27
*/
public class TankFactory {
/**
* 创建坦克
* @return
*/
public Tank createTank(String check){
Tank tank = null;
if(Objects.equals(check, "my")){
tank = new MyTank();
}else if(Objects.equals(check, "mouse")){
tank = new MouseTank();
}else if (Objects.equals(check, "big")){
tank = new BigTank();
}else {
throw new UnsupportedOperationException("当前坦克不支持生产");
}
return tank;
}
}
下面是对应的坦克以及坦克的子类实现
/**
* 坦克的父类,定义坦克的行为
*
* @author zxd
* @version 1.0
* @date 2021/1/25 0:14
*/
public abstract class Tank {
/**
* 坦克hp
*/
protected int hp;
/**
* 坦克子弹
*/
protected List
我方的坦克继承坦克的父类:
/**
* 我方坦克
*
* @author zxd
* @version 1.0
* @date 2021/1/25 21:58
*/
public class MyTank extends Tank {
public MyTank() {
// 我方坦克假设只有一条命
hp = 1;
bullet = new ArrayList<>();
// 初始化添加三发子弹
bullet.add(new Object());
bullet.add(new Object());
bullet.add(new Object());
}
@Override
void move() {
System.err.println("移动");
}
@Override
void attack() {
System.err.println("攻击地方坦克");
// ..弹出子弹
if(bullet.size() == 0){
System.err.println("没有子弹了");
return;
}
bullet.remove(bullet.get(bullet.size() -1));
}
@Override
void stop() {
System.err.println("停止");
}
}
敌人的坦克如下:
/**
* 老鼠坦克
*
* @author zxd
* @version 1.0
* @date 2021/1/25 22:02
*/
public class MouseTank extends Tank implements Runnable {
public void display() {
System.err.println("长得尖尖的,很像老鼠");
}
public MouseTank() {
// 坦克假设只有一条命
hp = 1;
new Thread(this).start();
bullet = new ArrayList<>();
// 初始化添加六发子弹
bullet.add(new Object());
bullet.add(new Object());
bullet.add(new Object());
bullet.add(new Object());
bullet.add(new Object());
}
@Override
void move() {
System.err.println("老鼠坦克移动");
}
@Override
void attack() {
System.err.println("老鼠坦克开枪");
// ..弹出子弹
if (bullet.size() <= 0) {
System.err.println("老鼠坦克没有子弹了");
return;
}
// 老鼠坦克一次性开两枪
bullet.remove(bullet.get(bullet.size() - 1));
}
@Override
void stop() {
System.err.println("停止");
}
@Override
public void run() {
while (true) {
// 一旦创建就开始移动
move();
// 漫无目的开枪
attack();
attack();
// 做完一轮操作歇一秒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 随机停止
if (new Random(100).nextInt() % 2 == 0) {
stop();
}
}
}
}
最后编写单元测试如下,我们使用简单工厂生产出不同的坦克,但是客户端不需要纠结生产的细节:
/**
* 单元测试
*
* @author zxd
* @version 1.0
* @date 2021/1/25 22:15
*/
public class Main {
/**
* 我们将生产坦克的过程全部交给了工厂来处理
* 可能还是奇怪,这和刚才没有什么区别呀?
* 我们来看下区别:
* 1. 创建的过程没有了,虽然是一个简单的new,但是new的过程交给了工厂
* 2. 我们后续如果要在坦克加入别的东西,只需要去改工厂类和具体的实现类,不需要该此处代码
* 3. 如果不支持的操作,工厂还可以通知我们这样做不对
* @param args
*/
public static void main(String[] args) {
TankFactory tankFactory = new TankFactory();
Tank my = tankFactory.createTank("my");
Tank mouse = tankFactory.createTank("mouse");
Tank big = tankFactory.createTank("big");
// 我要一个没有的设计过的坦克
Tank mybig = tankFactory.createTank("mybig");
}/*//
运行结果:
Exception in thread "main" 老鼠坦克移动
巨型坦克移动
老鼠坦克开枪
巨型坦克开枪
老鼠坦克开枪
java.lang.UnsupportedOperationException: 当前坦克不支持生产
at com.headfirst.factory.use.TankFactory.createTank(TankFactory.java:27)
at com.headfirst.factory.use.Main.main(Main.java:33)
*/
}
从上面的代码可以看到,对于坦克的创建和使用过程虽然进行解耦了,但是可以看到创建的过程耦合在了简单工厂的内部,工厂创建的方法耦合了过多的细节,同时如果需要创建新的产品需要改动工厂代码,这违背了开放-关闭原则。
针对上面的问题,我们显然需要使用工厂方法进行改良,我们让工厂的创建细节延迟到子类去实现,子类只需要关注创建的细节,不需要了解客户端的调用,下面我们针对上面的代码使用工厂方法进行改进。
这种改动也符合开放-关闭原则
工厂方法实现:
从简单工厂可以看出,如果每次修改产品都需要牵动工厂的代码改动,同时针对创建的过程都被“耦合”在单独的工厂创建方法内部,我们根据工厂方法的结构图看一下如何改进坦克大战的代码:
首先,我们需要依照工厂方法的定义,将原本的简单工厂类改造为具备工厂方法的工厂,在下面的代码当中,工厂类具备两个方法,一个用于创建具体的对象,由客户端调用,并且提供一个抽象的方法,由工厂子类实现并且定义具体的工厂生产过程。
/**
* 坦克工厂
* 工厂增加抽象方法由子类进行构建
*
* @author zxd
* @version 1.0
* @date 2021/2/16 17:33
*/
public abstract class TankFactory {
/**
* 创建坦克
* @return
*/
public Tank createTank(String check){
return createConcreteTankMethod(check);
}
/**
* 构建具体产品过程的方法
* @return
*/
protected abstract Tank createConcreteTankMethod(String check);
}
子类不需要关心createTank()
方法是如何运行的,只需要实现自己的工厂方法同时定义生产的细节提供支持即可。
下面的代码为我方坦克的生产工厂
/**
* 我方坦克的创建工厂
*
* @author zxd
* @version 1.0
* @date 2021/2/16 14:28
*/
public class OurTankFactory extends TankFactory {
@Override
public Tank createConcreteTankMethod(String check) {
Tank tank = null;
if(Objects.equals(check, "my")){
tank = new MyTank();
}
return tank;
}
}
下面的代码为敌人的坦克的生产工厂实现子类。
/**
* 敌人坦克的构建工厂
* 老鼠坦克
* @author zxd
* @version 1.0
* @date 2021/2/16 14:28
*/
public class MouseTankFactory extends TankFactory {
@Override
public Tank createConcreteTankMethod(String check) {
Tank tank = null;
if(Objects.equals(check, "mouse")){
tank = new MouseTank();
}
return tank;
}
}
通过这样的调整之后,我们每次增加新的产品,只需要继承具备工厂方法的工厂并且实现对应的方法完成自己的坦克创建细节,就将原本耦合的创建规则从父类从剥离,延迟到子类完成,下面来看下单元测试的代码,可以看到工厂的生产具体具体化到子类工厂的内部,而对外依旧是坦克的生成工厂,这样既符合依赖倒转
的原则,也方便后续的扩展和更多实现工厂的添加:
/**
* 工厂方法的单元测试
*
* @author zxd
* @version 1.0
* @date 2021/2/16 17:50
*/
public class Main {
public static void main(String[] args) {
TankFactory tankFactory = new MouseTankFactory();
TankFactory ourTankFactory = new OurTankFactory();
Tank my = tankFactory.createTank("mouse");
Tank mouse = ourTankFactory.createTank("my");
System.err.println(my);
System.err.println(mouse);
}/*运行结果:
com.headfirst.factory.use.MouseTank@677327b6
老鼠坦克移动
com.headfirst.factory.use.MyTank@14ae5a5
老鼠坦克开枪
老鼠坦克开枪
*/
}
工厂方法的问题:虽然工厂方法很好的为我们解决了创建过程由子类进行构建的问题,但是如果我们需要往坦克的产品里面提供配对的零件,此时会发现一些问题,我们的工厂方法只能提供一种产品的生产,如果我们需要生产很多的产品,工厂方法此时就遇到的瓶颈,因为需要调整继承结构,同时扩展非常不便。
注意点:工厂方法的另一个问题在于他需要依赖继承来实现对象创建过程定义,此时如果改动整个顶层的抽象方法会导致依赖磁铁导致所有的子类都需要改变。假如需要加入多个产品的生产,此时对于所有的子类改动来看都是十分麻烦的事情.
总结:工厂方法在构建一类产品的时候非常有效,但是需要构建很多种产品的时候会产生大量的继承具体化问题
抽象工厂的实现:
我们之前讲过抽象工厂实际上是对工厂方法的进一步提取,抽象工厂需要的是一系列产品的接口,由子工厂负责一系列产品的接口生产,同时更多的需要依赖组合的形式为具体的产品进行扩展。
在具体的案例代码介绍之前,我们需要对于案例进行改动,由于之前只存在坦克父类和具体的实现子类,为了详细介绍抽象工厂,我们针对坦克类增加一个大炮类,大炮类提供展示外观的方法,和坦克类的产品完全不同,我们需要定义坦克的大炮产品父类和具体的不同实现子类,在抽象工厂提供大炮的生产接口抽象同时,我们需要在大炮的类内部组合大炮的对象,为坦克增加不同的大炮外观,下面我们根据抽象工厂的结构图,构建如下的结构图:
我们参考结构图,定义类似的坦克结构,下面是加入新需求之后的结构图:
根据上面的结构图,我们先将工厂有具体类改造为工厂接口,不再持有具体的创建过程,将一系列创建的细节分布到子类进行,同时定义接口的方式可以创建多个产品。(这里简化为2个不同的产品)
/**
* 坦克工厂,专门负责生产坦克
*
* @author zxd
* @version 1.0
* @date 2021/1/25 22:27
*/
public interface TankFactory {
/**
* 坦克创建方法抽象
* @return
*/
Tank createTank();
/**
* 大炮的创建方法
* @return
*/
Cannon createCannon();
}
接下来我们根据抽象工厂接口创建具体的生产工厂,我们在子类可以返回具体的产品子类也可以返回抽象的父类,下面定义我方坦克的工厂类,同时定义一个特定敌人坦克的工厂类。
/**
* 我方坦克的创建工厂
*
* @author zxd
* @version 1.0
* @date 2021/2/16 14:28
*/
public class OurTankFactory implements TankFactory {
/**
* 创建自带大炮的坦克
* @return
*/
public Tank createTanAndCannon() {
Tank myTank = createTank();
myTank.setCannon(createCannon());
return myTank;
}
@Override
public Tank createTank() {
return new MyTank();
}
@Override
public Cannon createCannon() {
return new Artillery();
}
}
敌人坦克的工厂实现子类:敌人的坦克工厂实现子类,可以生产不同抽象产品的不同具体实现子类
/**
* 敌人坦克的构建工厂
* 老鼠坦克
* @author zxd
* @version 1.0
* @date 2021/2/16 14:28
*/
public class MouseTankFactory implements TankFactory {
@Override
public MouseTank createTank() {
return new MouseTank();
}
@Override
public Cannon createCannon() {
return new RocketLauncher();
}
}
接着我们定义另一个独立的产品,定义顶层的抽象类
/**
* 大炮抽象类
* 子类具备不同的大炮形式
*
* @author zxd
* @version 1.0
* @date 2021/2/16 18:15
*/
public abstract class Cannon {
/**
* 外观
*/
public abstract void display();
}
根据对应上面的抽象父类,定义对应点具体实现子类,这里为了简单将两个具体实现子类放到一块:
/**
* 火炮
*
* @author zxd
* @version 1.0
* @date 2021/2/16 19:06
*/
public class Artillery extends Cannon{
@Override
public void display() {
System.out.println("火箭炮");
}
}
/**
* 火箭炮
*
* @author zxd
* @version 1.0
* @date 2021/2/16 19:06
*/
public class RocketLauncher extends Cannon{
@Override
public void display() {
System.out.println("火箭炮");
}
}
这里扩展了一下坦克类,为坦克类组合了大炮的对象:
/**
* 坦克的父类,定义坦克的行为
*
* @author zxd
* @version 1.0
* @date 2021/1/25 0:14
*/
public abstract class Tank {
/**
* 坦克hp
*/
protected int hp;
/**
* 坦克子弹
*/
protected List
下面是单元测试代码,我们在坦克的对象里面设置或者组合其他的对象,并且由工厂提供生产:
/**
* 单元测试
* 抽象工厂
* @author zxd
* @version 1.0
* @date 2021/2/16 16:32
*/
public class Main {
public static void main(String[] args) {
TankFactory ourTankFactory = new OurTankFactory();
TankFactory mouseTankFactory = new MouseTankFactory();
Tank ourTankFactoryTank = ourTankFactory.createTank();
Cannon cannon = ourTankFactory.createCannon();
Tank mouseTankFactoryTank = mouseTankFactory.createTank();
Cannon cannon1 = mouseTankFactory.createCannon();
ourTankFactoryTank.setCannon(cannon);
mouseTankFactoryTank.setCannon(cannon1);
System.err.println("our = " + ourTankFactoryTank);
System.err.println("mouse = " + mouseTankFactoryTank);
}/*
our = Tank{hp=1, bullet=[java.lang.Object@677327b6, java.lang.Object@14ae5a5, java.lang.Object@7f31245a],
cannon=com.headfirst.factory.abstractfac.Artillery@6d6f6e28}
老鼠坦克移动
mouse = Tank{hp=1, bullet=[java.lang.Object@135fbaa4, java.lang.Object@45ee12a7, java.lang.Object@330bedb4, java.lang.Object@2503dbd3, java.lang.Object@4b67cf4d],
cannon=com.headfirst.factory.abstractfac.RocketLauncher@7ea987ac}
老鼠坦克开枪
*/
}
工厂模式的变化:
从上面的案例和具体实现我们分析了工厂模式的三种变化:简单工厂、工厂方法、抽象工厂。他们的递进次序也是简单工厂 -> 工厂方法 -> 抽象工厂
这种顺序。
我们可以发现简单工厂是一种非常简单的设计思路,他仅仅定义了的创建和使用过程的接口,同时产品具备最基本的抽象和继承设计,这类设计往往用于简单的对象构建。而一旦出现大量的具体对象,简单工厂的代码将会不断的膨胀,同时产生很多的if/else
代码。
此时就需要使用工厂方法对于简单工厂的结构进行升级,工厂方法通过继承的方式(定义抽象的方法),推迟具体对象的创建到子类,工厂父类既可以控制子类的创建结果,同时又不需要关心具体对象的创建过程,这种设计非常巧妙,很好的解决了工厂的对象创建方法代码臃肿的问题。
但是我们也发现了问题,工厂方法扩展会导致所有的子类进行强制实现,不利于后期的维护,同时如果需要一系列相关产品的生成,使用工厂方法进行继承实现会造成高度的继承耦合,不利于工厂的产品生产扩展,此时就可以运用抽象工厂进行改进,我们用抽象工厂扩展工厂方法,使用接口的形式定义一批接口,由子类工厂进行实现和后续的所有生产细节,同时还可以自定义生产的具体产品。
上面是根据案例对于本次的设计模式进行一个模式的总结,可以看到工厂模式的应用还是非常多的,在WEB领域最常用的Spring
框架就是的Bean工厂就是一个非常良好的工厂模式的实践案例。
工厂模式的总结:
下面用一张表格总结工厂模式的三种形态,优缺点以及相关总结:
模式名称 | 简单工厂 | 工厂方法 | 抽象方法 |
---|---|---|---|
特点 | 根据产品按照客户端的需求生产不同的具体对象,将生产和使用的过程进行解耦 | 将工厂的创建细节延迟到工厂的子类实现。 | 定义一系列工厂方法,由子工厂负责具体的多类产品的生产 |
派生方式 | 需要修改简单工厂的代码 | 顶层工厂增加方法需要所有的子类强制实现。生产多个产品需要改动继承结构 | 扩展产品和生产具体产品非常方便。但是扩展新对象需要改动抽象工厂接口 |
优点 | 1. 简单工厂将创建对象的过程和使用对象的过程进行解耦 2. 工厂可以创建生产对象的不同实现子类,扩展子类实现非常方便 |
1. 工厂方法将工厂生产对象的创建细节延迟到子类 2. 克服了简单工厂部分缺点,比如符合开放-关闭原则 3. 同样可以对客户端和创建对象工厂进行解耦 |
1. 有利于多个产品的对象创建扩展 2. 将抽象类转变为接口,可以定义更高级的抽象。方便向上扩展 3. 类似制定工厂的生产规则,而具体的细节交由实现接口的子类完成 |
缺点 | 1. 工厂扩展新的对象需要改动代码,不符合开放-关闭原则 2. 简单工厂对应简单的创建过程,所以创建过程复杂会造成工厂的臃肿 |
1. 不利于维护,加入工厂方法需要扩展所有的子类都需要实现工厂方法 2. 当需要多个产品类的时候,更改会相当的麻烦 |
1. 面对新的产品,需要所有的工厂实现类进行实现。 2. 最大的缺点是难以扩展新的产品,或者说扩展新产品的代价很大 |
总结 | 是一种良好的编码和思考方式,但是严格意义上不能算是设计模式 | 将具体对象的创建过程延迟到子类,符合开放-关闭原则 | 抽象工厂是对工厂方法的升级,分离了多个产品的生产同时,子工厂可以对多个产品的生产细节进行自由控制。 |
总结:
本次设计模式的文章内容比较长,由于本次设计模式虽然是一个设计模式,但是他存在三种“变体”,所以在什么使用哪一种设计还是需要依靠具体的需求环境来决定。可以看到该设计模式最容易混淆的是工厂方法和抽象工厂。希望通过本文的总结和案例可以让读者更好的了解工厂模式下这两者的使用场景和区别。