设计模式 | 工厂方法(Factory Method)

定义

工厂方法是一种创建式设计模式,它提供了用于在超类中创建对象的接口,但允许子类改变将要创建的对象的类型。

问题

想象一下,你正在创建一个物流管理应用程序。 您的应用程序的第一个版本只能处理卡车运输,因此大部分代码都在Truck类中。

过了一段时间,您的应用程序变得非常流行,以至于您会收到大量包含海运的请求。

好消息,对吧?! 但是代码如何? 它看起来大部分代码都与Truck类相关联。 添加船只需要更改整个代码库。 此外,如果您决定为应用程序添加其他类型的交通工具,则可能需要再次进行所有这些更改。

你最终会遇到一些令人讨厌的代码,它们会根据传输对象的类别选择行为。

方案

工厂方法设计模式建议使用一个叫”工厂”的方法来替代直接通过 new 来创建对象。构造函数调用应该在该方法内移动。 工厂方法返回的对象通常被称为“产品”。

咋一看上去,这一举动貌似毫无意义。但是你可以在子类中重写工厂方法,并且改变将要创建的类对象。让我们看看它是如何工作的:

当然,有一些小小的限制:所有的产品都必须有一个通用的接口(比如Transport)。 基类中的Factory方法应该返回这个通用接口。

只要这些产品具有共同的基类或接口(例如卡车和船舶实现传输接口),子类就可以返回不同的具体产品。

工厂方法的客户端并不关心它们接收到的具体产品类型,它们通过一个共同的产品接口来与所有的具体产品协同工作。

构造

伪代码

创建产品接口及其实现类

运输接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package one.wangwei.designpatterns.factorymethod;

/**
* 运输
*
* @author wangwei
* @date 2018/05/10
*/
public interface Transport {

/**
* 发送快递
*/
public void deliver();

}

卡车运输子类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package one.wangwei.designpatterns.factorymethod;

import lombok.extern.slf4j.Slf4j;

/**
* 卡车运输
*
* @author wangwei
* @date 2018/05/10
*/
@Slf4j
public class Truck implements Transport {

/**
* 发送
*/
@Override
public void deliver() {
log.info("Deliver by land in a box. ");
}
}

水路运输子类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package one.wangwei.designpatterns.factorymethod;

import lombok.extern.slf4j.Slf4j;

/**
* 水路运输
*
* @author wangwei
* @date 2018/05/10
*/
@Slf4j
public class Ship implements Transport {

/**
* 发送
*/
@Override
public void deliver() {
log.info("Deliver by sea in a container. ");
}
}

创建工厂及其子类

物流抽象类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package one.wangwei.designpatterns.factorymethod;

/**
* 物流
*
* @author wangwei
* @date 2018/05/10
*/
public abstract class BaseLogistics {

public void planDelivery() {
Transport transport = createTransport();
transport.deliver();
}

protected abstract Transport createTransport();

}

道路运输,采用卡车

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package one.wangwei.designpatterns.factorymethod;

/**
* 道路物流
*
* @author wangwei
* @date 2018/05/10
*/
public class RoadLogistics extends BaseLogistics {

@Override
protected Transport createTransport() {
return new Truck();
}
}

海运,采用船

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package one.wangwei.designpatterns.factorymethod;

/**
* 海运
*
* @author wangwei
* @date 2018/05/10
*/
public class SeaLogistics extends BaseLogistics {

@Override
protected Transport createTransport() {
return new Ship();
}
}

调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package one.wangwei.designpatterns.factorymethod;

/**
* 客户端
*
* @author wangwei
* @date 2018/05/10
*/
public class Client {

public static void main(String[] args) {

BaseLogistics logistics = new RoadLogistics();
logistics.planDelivery();

BaseLogistics logistics1 = new SeaLogistics();
logistics1.planDelivery();
}

}

使用场景

  1. 问题:当你不知道对象的确切类型和依赖关系时,你的代码需要保证可以正常工作。

    例如,从多个数据源去读和写入数据——文件系统、数据库和网络。这些资源都有不同的类型,依赖关系和初始化代码。

    方案

    工厂方法可以一个产品的具体实现细节。 为了支持新的产品类型,您只需要创建一个新的子类并覆盖其中的工厂方法即可。

  2. 问题:当你想要为你的类库或框架的用户提供一种方式,好让他们能够扩展这些库或框架的内部组件。

    方案

    用户可以轻松地对某些特定组件进行子类化。 但是,框架将如何识别该子类并使用它而不是标准组件呢? 用户必须重写每个创建标准组件实例的方法,并将其更改为创建自定义子类的对象。 这很尴尬,不是吗?

    更好的解决方案不仅是给用户提供扩展特定类的手段,而且还将生产组件的代码减少到单个创建方法中。 换句话说,提供工厂方法。

    让我们看一下它是如何工作的。想象一下,你正在使用一个开源的UI框架开发APP,你的APP需要一个圆型按钮,但是这个框架只提供的方形按钮。

    你要做的第一件事情就是实现 RoundButton 类。但是现在你需要告诉 UIFramework 类去使用新创建的对象,而不是默认对象。

    为了达到这个目的,你需要创建一个子类 UIWithRoundButtons 并且重写 createButton 方法. 这个方法仍然返回 Button 类,但是你的新子类将会提供 RoundButton 对象。现在,在你的App中,你需要使用 UIWithRoundButtons 而不是 UIFramework 来初始化这个框架。

  3. 问题:当你想要保存系统资源并且重用已经存在的对象,而不是重新创建一个新的对象。

    例如,当处理大的或者资源密集性的对象,例如数据库连接,等等。

    方案:

    想象一下,重复使用现有对象需要做多少工作:

    • 首先,您需要创建一个池来保留现有的对象。
    • 当有人请求一个对象时,你会在该池内寻找一个空闲对象。
    • …并将其返回给客户端代码。
    • 只有在没有空闲对象时,才会创建一个新对象(并将其添加到池中)。

    此代码必须放置在某处。 最方便的地方是一个构造函数。 这样,只要有人试图创建一个对象,所有这些检查都会被执行。 但是,唉,构造函数必须按照定义返回新对象,所以它们不能返回现有的实例。

如何做

  1. 提取所有产品的通用接口。 这个接口应该声明对每个产品都有意义的方法。

  2. 在创建者类中添加一个空的工厂方法。 其方法签名应返回产品接口类型。

  3. 查看创建者的代码并查找对产品构造函数的所有引用。 一个接一个地将它们替换为对工厂方法的调用,但将产品创建代码提取到工厂方法。

    您可能需要向工厂方法添加一个临时参数,该参数将用于控制将创建哪个产品。

    在这一点上,工厂方法的代码可能看起来很丑陋。 它可能有一个大的开关操作员,可以选择要实例化的产品类别。 但别担心,我们会马上修复它。

  4. 现在,在子类中覆盖工厂方法,并在base方法中从switch操作符中移出相应的条件。

  5. 在基础创建者类中用到的控制参数也能够用到子类当中

    例如,您可能拥有一个创建者的层次结构,其中包含基类 Mail 以及AirGround类以及产品类:PlaneTruckTrainAirPlane 相匹配,同时GroundTruckTrain同时匹配。 您可以创建一个新的子类来处理这两种情况,但还有另一种选择。 客户端代码可以将参数传递给Ground类的工厂方法,以控制它接收的产品。

  6. 如果基础工厂方法在完成代码的迁移后变成了空壳,则可以将其抽象化。

优缺点

Pros

  • 遵循了开闭原则。
  • 避免了具体产品和使用它们的代码之间的紧密耦合。
  • 由于将所有创建代码移动到一个地方,因此简化了代码。
  • 向程序添加一个新的产品变得非常方便。

Cons

  • 需要额外的子类。

JDK中的运用

参考资料

请我喝杯咖啡吧~