设计模式之工厂模式

在 Java 语言中,通过关键字 new 来创建一个新对象,例如: String str = new String();

在某些情况下,创建对象的时候同时需要进行一些初始化操作,比如加载配置文件、数据初始化等。如果把这些逻辑都放在构造函数中,违反了职责单一原则,也降低了代码的可读性。我们可以专门定义一个类来负责对象的创建和初始化操作,这个类就是工厂类,这种方式就是所谓的工厂模式。在这种需要创建复杂对象而不是简单的 new 一个对象的场景下,我们都可以使用工厂模式。

工厂模式包括:简单工厂、工厂方法和抽象工厂。其中简单工厂不属于 GoF 《设计模式》的 23 种设计模式之一,它将简单工厂模式看作是工厂方法模式的一种特例。

下边我将使用扫码支付作为例子来分别三种工厂模式的使用。扫码支付:你在购物时向商家出示微信/支付宝付款码,商家通过扫描付款码完成支付。

注:本文图例和代码中省略了支付金额、订单等信息,只列出了架构相关的内容

简单工厂(Simple Factory)

为实现上述扫码支付功能,我们编写如下代码:

public class PayService {
    public boolean scanPay(PayTypeEnum payType, String authCode) {
        AbstractScanPay scanPay;
        if (PayTypeEnum.WX == payType) {
            scanPay = new WxScanPay();
        } else if (PayTypeEnum.ALIPAY == payType) {
            scanPay = new AlipayScanPay();
        } else if (PayTypeEnum.UNIONPAY == payType) {
            scanPay = new UnionpayScanPay();
        } else {
            throw new InvalidPayTypeException("pay type is not supported: " + payType.name());
        }
        return scanPay.pay(authCode);
    }
}

为了让代码逻辑更加清晰,提高可读性,让类的职责更加单一,我们将上述代码中创建对象部分剥离到一个单独的类中,让这个类负责对象的创建。而这个类就是简单工厂模式类。具体代码如下:

// 支付类型枚举
public enum PayTypeEnum {
    WX, ALIPAY, UNIONPAY
}

// 扫码支付接口
public interface AbstractScanPay {
    boolean pay(String authCode);
}

// 微信扫码支付
public class WxScanPay implements AbstractScanPay {

    @Override
    public boolean pay(String authCode) {
        // ......微信扫码支付逻辑
        return true;
    }

}

// 支付宝扫码支付
public class AlipayScanPay implements AbstractScanPay {

    @Override
    public boolean pay(String authCode) {
        // .....支付宝扫码支付逻辑
        return true;
    }

}

// 银联扫码支付
public class UnionpayScanPay implements AbstractScanPay {

    @Override
    public boolean pay(String authCode) {
        // ......银联扫码支付逻辑
        return true;
    }

}

// 工厂类
public class ScanPayFactory {
    public static AbstractScanPay getInstance(PayTypeEnum payType) {
        AbstractScanPay scanPay;
        if (PayTypeEnum.WX == payType) {
            return new WxScanPay();
        } else if (PayTypeEnum.ALIPAY == payType) {
            return new AlipayScanPay();
        } else if (PayTypeEnum.UNIONPAY == payType) {
            return new UnionpayScanPay();
        }
        throw new InvalidPayTypeException("pay type is not supported: " + payType.name());
    }
}

从上述代码我们可以看到,简单工厂模式主要包含三部分:

  • 接口或抽象类:定义要创建对象的接口。

  • 具体实现:具体类型的实现,他们都实现了同样的接口或者继承自同一抽象父类。

  • 工厂类:负责创建对象。

将扫码支付对象的创建重构成简单工厂模式后,上述支付服务代码中的调用方式如下:

public class PayService {
    public boolean scanPay(PayTypeEnum payType, String authCode) {
        return ScanPayFactory.getInstance(payType).pay(authCode);
    }
}

当我们之后需要新增其他扫码支付方式(比如:京东扫码支付、美团扫码支付等)时,我们只需要扩展工厂的实现即可:新增扫码支付实现类,同时在工厂类中增加相应的创建逻辑。

上述简单工厂类 ScanPayFactory ,是一个具体的类,而不是基于一个接口或者抽象类来实现的,getInstance 方法也是使用 if-else 来进行创建并返回具体实例的,当我们新增扫码支付实现类时,工厂的创建方法中则需要增加新的 if-else。这种做法扩展性差、违背了开闭原则。因此,简单工厂模式适用于业务比较简单,工厂类不会经常发生变化的场景。

工厂方法(Factory Method)

为了解决简单工厂创建对象的 if-else 问题,我们可以为每一个扫码支付实现类创建一个对应的工厂子类,这些工厂子类都实现了同一个工厂接口。这样,当我们新增其他扫码支付时,只需要实现不同扫码实现类和创建工厂子类即可。这种方式就是工厂方法,它比简单工厂模式更加符合开闭原则。

// 工厂接口
public interface AbstractScanPayFactory {
    AbstractScanPay create();
}

// 微信扫码支付工厂
public class WxScanPayFactory implements AbstractScanPayFactory {
    @Override
    public AbstractScanPay create() {
        return new WxScanPay();
    }
}

// 支付宝扫码支付工厂
public class AlipayScanPayFactory implements AbstractScanPayFactory {
    @Override
    public AbstractScanPay create() {
        return new AlipayScanPay();
    }
}

// 银联扫码支付工厂
public class UnionpayScanPayFactory implements AbstractScanPayFactory {
    @Override
    public AbstractScanPay create() {
        return new UnionpayScanPay();
    }
}

从上述代码我们可以看到,工厂方法模式主要包含四部分:

  • 抽象工厂:声明工厂方法的接口

  • 具体工厂子类:实现工厂方法的接口,负责创建具体对象

  • 接口或抽象类:定义工厂方法要创建对象的接口。

  • 具体实现:具体类型的实现,他们都实现了同样的接口或者继承自同一抽象父类。

将扫码支付对象的创建重构成工厂方法模式后,上述支付服务代码中的调用方式如下:

public class PayService {
    public boolean scanPay(PayTypeEnum payType, String authCode) {
        AbstractScanPayFactory scanPayFactory;
        if (PayTypeEnum.WX == payType) {
            scanPayFactory = new WxScanPayFactory();
        } else if (PayTypeEnum.ALIPAY == payType) {
            scanPayFactory = new AlipayScanPayFactory();
        } else if (PayTypeEnum.UNIONPAY == payType) {
            scanPayFactory = new UnionpayScanPayFactory();
        } else {
            throw new InvalidPayTypeException("pay type is not supported: " + payType.name());
        }
        return scanPayFactory.create().pay(authCode);
    }
}

从上述代码我们可以看出,工厂类对象的创建逻辑又耦合到 service 中,这和我们最初的代码基本相似。引入工厂方法,让问题又回到了原点,同时也让类更多更加复杂了。那么如何解决这个问题:

为工厂类再创建一个简单工厂,用来创建工厂类对象。(工厂的工厂) 代码实现如下:

public class ScanPayFactoryMap {
    public static AbstractScanPayFactory getFactory(PayTypeEnum payType) {
        AbstractScanPayFactory scanPayFactory;
        if (PayTypeEnum.WX == payType) {
            return new WxScanPayFactory();
        } else if (PayTypeEnum.ALIPAY == payType) {
            return new AlipayScanPayFactory();
        } else if (PayTypeEnum.UNIONPAY == payType) {
            return new UnionpayScanPayFactory();
        }
        throw new InvalidPayTypeException("pay type is not supported: " + payType.name());
    }
}

service 中调用代码如下:

public class PayService {
    public boolean scanPay(PayTypeEnum payType, String authCode) {
        return ScanPayFactoryMap.getFactory(payType).create().pay(authCode);
    }
}

当我们需要添加的扫码支付多了之后,为每一个子类都要添加对应的工厂类。这样会使类的个数成倍增加,代码的复杂度也会随之上升。那么什么时候我们应该工厂方法模式咧?

当创建对象的逻辑比较复杂,不只是 new 一下,还需要组合其他类对象,或者做各种初始化操作时,推荐使用工厂方法模式,将复杂度拆分到各自对应的工厂类中,让每个工厂类不至于过于复杂。这时如果使用简单工厂模式,将所有创建逻辑放到一个工厂类中,会导致这个工厂类变得复杂,代码可读性差,后期也不好维护和扩展。

如果对象不可复用,每次都需要返回不同的对象,如果使用简单工厂模式来实现,则需要使用 if-else 分支方式来实现。这时可以使用工厂方法模式。

抽象工厂(Abstract Factory)

使用工厂方法模式时,为了减少工厂实现子类的数量,不需要给每一个业务类型创建一个对应的工厂类,我们可以将业务类型进行分组,每组中不同的业务类型使用同一个工厂类的不同方法来创建。

例如:上述支付示例中,除了支付,我们再添加付款功能。这时我们可以按照支付渠道(微信/支付宝/银联)来进行分组,为每个渠道建立一个工厂类,然后由支付渠道工厂类来创建具体的业务对象。

这种把业务类型分组,组内不同业务对象由同一工厂类的不同方法来创建的方法,我们称为抽象工厂模式。

如上图所示,抽象工厂主要包含4部分:

  • 抽象工厂:声明创建抽象业务对象的接口。

  • 具体分组工厂:实现抽象工厂的接口,负责业务对象的创建。

  • 业务接口或抽象类:定义业务接口。

  • 业务具体实现:实现业务接口,也即被具体工厂创建的业务对象定义。

相关实现代码如下:

// 企业付款接口
public interface AbstractPayTo {
    boolean payTo(Account account);
}

// 微信企业付款
public class WxPayTo implements AbstractPayTo {

    @Override
    public boolean payTo(Account account) {
        // ......微信企业付款逻辑
        return true;
    }

}

// 支付企业付款
public class AlipayPayTo implements AbstractPayTo {

    @Override
    public boolean payTo(Account account) {
        // .....支付宝企业付款逻辑
        return true;
    }

}

// 银联企业付款
public class UnionpayPayTo implements AbstractPayTo {

    @Override
    public boolean payTo(Account account) {
        // ......银联企业付款逻辑
        return true;
    }

}

// 抽象工厂类
public interface AbstractPayFactory {
    AbstractScanPay createScanPay();
    AbstractPayTo createPayTo():
}

// 微信工厂类
public class WxPayFactory implements AbstractPayFactory {
    @Override
    public AbstractScanPay createScanPay() {
        return WxScanPay();
    }

    @Override
    public AbstractPayTo createPayTo() {
        return WxPayTo();
    }
}

// 支付宝工厂类
public class AlipayPayFactory implements AbstractPayFactory {
    @Override
    public AbstractScanPay createScanPay() {
        return AlipayScanPay();
    }

    @Override
    public AbstractPayTo createPayTo() {
        return AlipayPayTo();
    }
}

// 银联工厂类
public class UnionpayPayFactory implements AbstractPayFactory {
    @Override
    public AbstractScanPay createScanPay() {
        return UnionpayScanPay();
    }

    @Override
    public AbstractPayTo createPayTo() {
        return UnionpayPayTo();
    }
}

综上所述,工厂模式用于创建同一个接口定义的具有复杂参数或初始化步骤的不同对象。在 Spring 项目中,我们可以结合 InitializingBean 接口,利用  @Resource 注解来优雅的实现工厂模式。

另外,可关注我的公众号或者QQ群(781374180),2021年我将系统化的分享 Java 技术栈相关内容,包括但不限于:Java 基础、MyBatis、MySQL、Spring 全家桶、MQ、Redis、ES、云原生、ServiceMesh、Serverless等。