设计模式预计阅读 17 分钟6415 字3740 次浏览

设计模式修炼之路 ③ 工厂模式——简单工厂、工厂方法、抽象工厂三兄弟

简单工厂、工厂方法、抽象工厂三兄弟的演进与取舍、开闭原则得失

目录

上一篇《设计模式修炼之路-单例模式》里,我们折腾了六种写法,本质上都是在解决一个问题:一个类在整个应用里只允许存在一个实例。单例模式管的是"有几个",而这一篇要聊的工厂模式,管的是另一个同样高频的问题:实例从哪来。

new Circle() 谁都会写,但当 new 这个关键字散落在代码的各个角落时,麻烦就来了:一旦 Circle 的构造过程变了(比如要初始化依赖、要读配置、要打日志),所有 new Circle() 的地方都得跟着改。工厂模式的思路很简单:把"造对象"这件事收拢到一个专门的地方去,调用方只管"要",不管"造"。

一、什么是工厂模式?

工厂模式(Factory Pattern)是 Java 中最常用的设计模式之一,属于创建型模式。它的核心思想用一句话就能说清楚:

把对象的创建过程封装到一个"工厂"类中,调用方只需要告诉工厂"我要什么",完全不关心对象是怎么被造出来的。

拿去餐厅吃饭类比再合适不过:你坐下来跟服务员说"来一份宫保鸡丁",十分钟后菜就端上来了。你不需要知道厨师是谁、厨房在哪、鸡丁是先焯水还是先过油——厨房就是工厂,你就是调用方,菜单就是你们之间的接口。哪天厨师换了做法,甚至整个后厨翻新,只要端上来的还是那盘宫保鸡丁,你完全无感。

平时我们嘴上说的"工厂模式",其实是三兄弟的统称:

  • 简单工厂(Simple Factory):严格来说它并不属于 GoF 的 23 种设计模式,但它是理解工厂模式的敲门砖——一个工厂包揽所有产品的创建,要什么传什么参数。
  • 工厂方法(Factory Method):GoF 23 种之一。一个工厂只生产一种产品,想新增产品?再开一家工厂。
  • 抽象工厂(Abstract Factory):GoF 23 种之一。一个工厂生产一整个"产品族"(一组相关的产品),连工厂本身都是被"生产"出来的。

三兄弟是逐步演进的关系:简单工厂图省事,工厂方法补上开闭原则的漏洞,抽象工厂专门对付产品族。它们的具体区别放在第四节按顺序实现,实现完自然就懂了。

二、工厂模式的优缺点

优点

  1. 调用方和具体类解耦。调用方依赖的是工厂和抽象接口,而不是一堆具体实现类,具体类内部怎么变都不影响调用方。
  2. 创建逻辑收口。对象的创建过程集中在一处,想在创建时加缓存、打日志、读配置,都只需要改工厂一个地方。
  3. 符合开闭原则(工厂方法模式)。新增一种产品时,只需要新增对应的产品类和工厂类,已有代码一行不动——上一篇提过的开闭原则,工厂方法是教科书级的示范。

缺点

  1. 类的数量膨胀。工厂方法模式下每种产品都要配一个工厂类,产品一多,类文件成倍增加,代码结构变复杂。
  2. 简单工厂违背开闭原则。新增产品就得改工厂类里的判断逻辑,"对扩展开放,对修改关闭"直接破防。
  3. 抽象工厂纵向扩展难。产品族里想新增一条全新的产品线,抽象工厂接口一改,所有具体工厂都得跟着改,牵一发动全身。

可以看出,三种形态各有各的取舍,没有银弹——这也是设计模式一贯的画风:用复杂度换取扩展性。

三、工厂模式的应用场景

工厂模式太常见了,常见到你天天在用却未必意识到:

  • JDK 里:Calendar.getInstance()——你拿到的是 Calendar 的子类实例(比如 GregorianCalendar),但从来没 new 过它;Integer.valueOf() 也是类似的思路,还顺带做了缓存。
  • Spring 里:BeanFactory——整个 IoC 容器就是一个超级工厂,getBean() 就是工厂方法的放大版,你所有的 Bean 都是从这个工厂里"造"出来的。
  • JDBC 里:DriverManager.getConnection()——数据库连接的创建细节被完全屏蔽,换数据库只是换了个"供应商"。

什么时候该用:当对象的创建过程比较复杂(依赖多、要初始化、要读配置),或者调用方只关心"能用"而不关心"怎么造",或者同一族产品可能频繁增删时,就该考虑把创建逻辑收进工厂了。反过来,如果就是简单 new 一下的事,硬套工厂模式纯属过度设计。

四、工厂模式的实现方式

老规矩,光说不练假把式。本篇的示例代码按模式分包放在 design-pattern 项目里,目录结构如下:

design-pattern/src/main/java
├── singleton                     // 上一篇单例模式的六种实现
├── factory                       // 工厂模式的公共产品
│   ├── Shape.java                // 图形产品接口
│   ├── Circle.java / Rectangle.java / Square.java
│   ├── simple                    // 简单工厂
│   ├── method                    // 工厂方法
│   └── abstractfactory           // 抽象工厂
└── Test.java                     // 测试入口

需求场景很简单:画图形。三种图形都实现 Shape 接口:

public interface Shape {
    void draw();
}
public class Circle implements Shape {

    public Circle() {
        System.out.println("创建了 Circle 对象");
    }

    @Override
    public void draw() {
        System.out.println("画了一个圆形");
    }
}

Rectangle 和 Square 与之类似,不再重复贴。构造函数里特意加了一句打印,是为了从运行结果里看清对象到底是什么时候被创建的。

1. 简单工厂

简单工厂的思路最直白:开一家工厂,客户报什么名字,工厂就造什么。

public class ShapeFactory {

    public static Shape getShape(String shapeType) {
        if (shapeType == null) {
            return null;
        }
        if (shapeType.equalsIgnoreCase("CIRCLE")) {
            return new Circle();
        } else if (shapeType.equalsIgnoreCase("RECTANGLE")) {
            return new Rectangle();
        } else if (shapeType.equalsIgnoreCase("SQUARE")) {
            return new Square();
        }
        return null;
    }
}

测试代码(因为项目里有三个同名的 ShapeFactory,这里用全限定名调用,实际写代码时 import 对应包即可):

private static void simpleFactory() {
    Shape circle = factory.simple.ShapeFactory.getShape("CIRCLE");
    Shape rectangle = factory.simple.ShapeFactory.getShape("RECTANGLE");
    Shape square = factory.simple.ShapeFactory.getShape("SQUARE");
    circle.draw();
    rectangle.draw();
    square.draw();
}

运行结果:

创建了 Circle 对象
创建了 Rectangle 对象
创建了 Square 对象
画了一个圆形
画了一个长方形
画了一个正方形

调用方拿到的都是 Shape 类型,全程没出现一个 new Circle(),创建逻辑确实被收口了。但它的问题也很明显:想新增一种"三角形",就得回来改 getShape() 的 if-else,修改了已经存在的类,开闭原则就此失守——这也是它没能进 GoF 23 种名单的原因。

顺带一提,简单工厂还有一种进阶写法:用反射 Class.forName(shapeType).getDeclaredConstructor().newInstance() 把 if-else 干掉。能跑,但失去了编译期类型检查,参数传错要拖到运行时才炸,实际项目里并不推荐。

2. 工厂方法

简单工厂的病根在于"一个工厂包办所有产品",工厂方法模式的药方就是:把工厂也抽象出来,一个工厂只负责一种产品。

public interface ShapeFactory {
    Shape getShape();
}
public class CircleFactory implements ShapeFactory {

    @Override
    public Shape getShape() {
        return new Circle();
    }
}

RectangleFactory、SquareFactory 同理。测试代码:

private static void factoryMethod() {
    ShapeFactory circleFactory = new CircleFactory();
    ShapeFactory rectangleFactory = new RectangleFactory();
    ShapeFactory squareFactory = new SquareFactory();
    circleFactory.getShape().draw();
    rectangleFactory.getShape().draw();
    squareFactory.getShape().draw();
}

运行结果:

创建了 Circle 对象
画了一个圆形
创建了 Rectangle 对象
画了一个长方形
创建了 Square 对象
画了一个正方形

对比简单工厂的运行结果会发现一个有趣的细节:这次"创建"和"画图"是交替出现的,而简单工厂是先一口气全创建好再统一画。倒不是两种模式本身有这个区别,而是工厂方法交给调用方的是工厂对象,产品什么时候造、造几个,主动权完全在调用方手里——这就是把"创建能力"延迟交付的好处。

现在想新增三角形?新建一个 Triangle 类、一个 TriangleFactory 类,就能接入使用了,已有代码一行不动,开闭原则保住了。代价也摆在那里:三种产品配了三个工厂类,类数量直接翻倍。

3. 抽象工厂

前两种工厂都只管一条产品线,但现实需求里经常是一族产品搭配出现:画图程序里除了图形还有颜色,图形和颜色就是两条产品线。抽象工厂模式让"工厂"本身也变成了被创建的对象——由一个超级工厂(工厂的工厂)来生产具体的工厂。

先加一条颜色产品线:

public interface Color {
    void fill();
}
public class Red implements Color {

    public Red() {
        System.out.println("创建了 Red 对象");
    }

    @Override
    public void fill() {
        System.out.println("填充了红色");
    }
}

Green、Blue 同理。然后是抽象工厂接口,注意它同时声明了两条产品线的创建能力:

public interface AbstractFactory {
    Shape getShape(String shapeType);

    Color getColor(String colorType);
}

具体工厂各自负责一条产品线,造不了的产品类型返回 null:

public class ShapeFactory implements AbstractFactory {

    @Override
    public Shape getShape(String shapeType) {
        if (shapeType == null) {
            return null;
        }
        if (shapeType.equalsIgnoreCase("CIRCLE")) {
            return new Circle();
        } else if (shapeType.equalsIgnoreCase("RECTANGLE")) {
            return new Rectangle();
        } else if (shapeType.equalsIgnoreCase("SQUARE")) {
            return new Square();
        }
        return null;
    }

    @Override
    public Color getColor(String colorType) {
        return null;
    }
}

ColorFactory 则相反:getColor() 里做 RED/GREEN/BLUE 的判断,getShape() 返回 null。最后是"工厂的工厂":

public class FactoryProducer {

    public static AbstractFactory getFactory(String factoryType) {
        if (factoryType == null) {
            return null;
        }
        if (factoryType.equalsIgnoreCase("SHAPE")) {
            return new ShapeFactory();
        } else if (factoryType.equalsIgnoreCase("COLOR")) {
            return new ColorFactory();
        }
        return null;
    }
}

测试代码:

private static void abstractFactory() {
    AbstractFactory shapeFactory = FactoryProducer.getFactory("SHAPE");
    Shape circle = shapeFactory.getShape("CIRCLE");
    Shape square = shapeFactory.getShape("SQUARE");
    circle.draw();
    square.draw();
    AbstractFactory colorFactory = FactoryProducer.getFactory("COLOR");
    Color red = colorFactory.getColor("RED");
    Color green = colorFactory.getColor("GREEN");
    red.fill();
    green.fill();
}

运行结果:

创建了 Circle 对象
创建了 Square 对象
画了一个圆形
画了一个正方形
创建了 Red 对象
创建了 Green 对象
填充了红色
填充了绿色

注意 ColorFactory 里 getShape() 返回的是 null——抽象工厂的强项是同一条产品线内的横向扩展:新增一种颜色,只需要改 ColorFactory 一处;但要是想新增一条全新的产品线(比如"文字"),AbstractFactory 接口一改,ShapeFactory、ColorFactory 所有具体工厂全部得跟着改。一句话总结:横向(同族加产品)容易,纵向(加产品线)疼。

五、三种工厂怎么选

简单工厂 工厂方法 抽象工厂
是否属于 GoF 23 种 否 是 是
工厂与产品的关系 一个工厂造所有产品 一个工厂造一种产品 一个工厂造一族产品
新增同类产品 修改工厂代码 ✗ 新增类即可 ✓ 同族内新增类即可 ✓
新增产品线 修改工厂代码 ✗ 新增一套工厂接口 修改抽象工厂接口 ✗
类的数量 少 多 多

实际开发里的选型建议很朴素:产品少且稳定,用简单工厂,图个轻量;产品会持续增加,用工厂方法,守住开闭原则;产品天然成族、需要保证同一族产品搭配使用时,再上抽象工厂。绝大多数业务代码,一个简单工厂(或者简单工厂加策略模式)就够用了,别为了模式而模式。

写在最后

回顾一下这一篇:工厂模式把"造对象"从调用方手里收走,调用方从此只关心"要什么"。简单工厂一把梭但违背开闭原则;工厂方法一个产品一个工厂,类多了但扩展稳了;抽象工厂管住整个产品族,横向扩展容易、纵向扩展头疼。三种形态没有高下之分,只有取舍之别——而这,正是所有设计模式的共同底色。

示例代码都放在 design-pattern 项目里,按模式分了包(singleton、factory.simple、factory.method、factory.abstractfactory),和上一篇的单例代码放在同一个项目下,方便对照练习。

下一篇我们继续创建型模式,聊聊另一种"造对象"的思路——建造者模式(Builder):当一个对象的构造参数多到 new Pizza(10, true, false, null, "BIG") 这种程度时,该怎么优雅地把它造出来。

花

花开空白

开发者 · 西安

相关文章

评论(0)

还没有评论,来抢沙发吧

发表评论