设计模式实践笔记第二节:抽象工厂模式

Posted 滕哥

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了设计模式实践笔记第二节:抽象工厂模式相关的知识,希望对你有一定的参考价值。

抽象工厂模式

提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。

抽象工厂模式(Abstract Factory)也是一种创建型设计模式,通常用来解决「产品族」的创建问题。

抽象工厂模式结构

工厂模式的三要素是产品定义(即接口)产品实现工厂,抽象工厂模式是工厂模式的升级,核心是对工厂进行抽象,区分工厂定义和具体实现。抽象工厂模式主要解决的是 接口选择 的问题。

那么什么是产品族呢?

产品族可以理解为一个工厂生产的一系列产品。抽象工厂定义一系列产品(产品族)的创建方法,具体工厂实现各自的产品族的生产过程。

于是客户端在使用抽象工厂的时候无需关心具体的实现。

电子设备工厂

假设你要为自己的系统开发一款代工厂模拟程序,其中包括电子设备的生产服务。

定义抽象工厂接口和产品接口。

public interface AbstractFactory 
    /**
     * 创建手机
     */
    Phone createPhone();
  
    /**
     * 创建平板电脑
     */
    Pad createPad();


public interface Phone 
  	/**
     * 打电话
     */
	void call(String phoneNumber);


public interface Pad 
  	/**
     * 看视频
     */
	void watchVideo();

抽象工厂定义了PhonePad 两种产品的生产方法,我们规定手机可以打电话,平板电脑可以看视频,客户端只依赖以上三个类便可以实现自己的业务。此时,我们应该给每种产品找到他们的代工厂,这里无需担心的是由于客户端对代工厂是无感知的,只要你找到新的供应商,客户端总能拿到最新的具体产品。

我们准备占领高端电子消费市场,所以先找了华为帮我们生产设备。

public class HuaweiFactory implements AbstractFactory 
  	public Phone createPhone() 
      	return new Mate40Pro();
    
  
    public Pad createPad() 
      	return new MatePad();
    


public class Mate40Pro implements Phone 
  	@Override
  	public void call(String phoneNumber) 
      	 System.out.println("使用华为 Mate 40 Pro 手机给" + phoneNumber + "打电话");
    


public class MatePad implements Pad 
  	@Override
  	public void watchVideo() 
      	 System.out.println("使用华为Mate平板看视频就是爽");
    

后来我们发现下沉市场也潜力无穷,又抓紧找了小米代工厂。

public class XiaomiFactory implements AbstractFactory 
  	public Phone createPhone() 
      	return new Xiaomi6();
    
  
    public Pad createPad() 
      	return new XiaomiPad();
    


public class Xiaomi6 implements Phone 
  	@Override
  	public void call(String phoneNumber) 
      	 System.out.println("使用小米6手机给" + phoneNumber + "打电话");
    


public class XiaomiPad implements Pad 
  	@Override
  	public void watchVideo() 
      	 System.out.println("使用小米平板看视频就是爽");
    

工厂和产品都有了,我们如何选择具体工厂可以交给客户端去处理,但更合理的做法一般是通过读取系统配置来实现。

public class DeviceClient 
    public static void main(String[] args) 
        // 实例化一个工厂
        AbstractFactory factory = new HuaweiFactory();

        // 生产一台手机
        Phone phone = factory.createPhone();
        phone.call("13100001111");

        // 生产一台平板
        Pad pad = factory.createPad();
        phone.watchVideo();
    

输出:

使用华为 Mate 40 Pro 手机给13100001111打电话
使用华为Mate平板看视频就是爽

从例子中看起来似乎使用抽象工厂模式的收益并不高,反而增加了大量的代码导致更难理解,这是因为示例的主要目的是方便了该模式的结构,所以使用了过于简单场景来进行阐述。事实上在实际开发过程中我们会遇到更为复杂的业务场景和需求,如果不选择一种合适的开发方式,不光会产生大量的重复代码,还会打破开闭原则,最终导致系统难以维护。

Spring中的抽象工厂

Spring 框架中使用了大量的设计模式,其中 BeanFactoryFactoryBean 经常被拉出来解读。

阅读 FactoryBean 的源码,通过第一句 Interface to be implemented by objects used within a @link BeanFactory whichare themselves factories for individual objects. (由 BeanFactory 中使用的对象实现的接口,这些对象本身就是各个对象的工厂)很容易知道这是一个提供各种 Bean 的抽象工厂。具体工厂实现有很多:

通过这些不同的工厂实现,让 Spring 框架的基础架构变得非常稳定。下图是 FactoryBean 的类头注释,写得相当清楚。留一个小思考,为什么这个工厂的命名叫 xxxBean 而不是 xxxFactory

重构发货模块!

如果我们的代码需要与不同的产品族打交道,或者考虑到未来扩展性,那么使用抽象工厂模式将是一个好的选择。

假如我们现在维护一个电商平台,我们卖的不光有日用百货,还有水果生鲜,那这两个品类的派送我们都委托给第三方物流公司来做。最开始规模小,只跟顺丰一家物流商合作各方面成本最低,这时候代码随便写,怎么都能跑起来,但是随着贸易规模的扩大,要全国各地发货,不同地区的时效要求也不一样,那么接入更多的物流公司来参与合作无异更好。但是系统发展往往是跟业务发展脚步不齐的,日久天长以后,我们系统里充斥着大量的「兼容性」代码,每新增加一个物流商,就要把之前的程序员拉出来鞭一次尸。。。

原发货代码

1.发货服务代码:

public interface ShippingService 
    /**
     * 普通快递发货
     */
    void shipNormal(String goodsName, String logisticsChannel);
    /**
     * 冷链物流发货
     */
    void shipFresh(String goodsName, String logisticsChannel);

public class ShippingServiceImpl implements ShippingService 
    private ShunfengLogisticsAdapter shunfengLogisticsAdapter;
    private JingdongLogisticsAdapter jingdongLogisticsAdapter;
    // 省略构造器

    @Override
    public void shipNormal(String goodsName, String logisticsChannel) 
        String orderNo;
        int logisticsFee;
        if ("shunfeng".equalsIgnoreCase(logisticsChannel)) 
            JingdongOrder order = jingdongLogisticsAdapter.createOrder(goodsName);
            orderNo = order.getOrderNo();
            logisticsFee = order.getLogisticsFee();
         else if ("jingdong".equalsIgnoreCase(logisticsChannel)) 
            ShunfengOrder order = shunfengLogisticsAdapter.createOrder(goodsName);
            orderNo = order.getOrderNo();
            logisticsFee = order.getLogisticsFee();
         else 
            throw new RuntimeException("物流渠道错误");
        

        System.out.println(goodsName + "发货完成,运单号=" + orderNo + ",费用=" + logisticsFee + "元.");
    

    @Override
    public void shipFresh(String goodsName, String logisticsChannel) 
        String orderNo;
        int logisticsFee;
        if ("shunfeng".equalsIgnoreCase(logisticsChannel)) 
            JingdongFreshOrder order = jingdongLogisticsAdapter.createFreshOrder(goodsName);
            orderNo = order.getOrderNo();
            logisticsFee = order.getLogisticsFee();
         else if ("jingdong".equalsIgnoreCase(logisticsChannel)) 
            ShunfengFreshOrder order = shunfengLogisticsAdapter.createFreshOrder(goodsName);
            orderNo = order.getOrderNo();
            logisticsFee = order.getLogisticsFee();
         else 
            throw new RuntimeException("物流渠道错误");
        

        System.out.println(goodsName + "发货完成,运单号=" + orderNo + ",费用=" + logisticsFee + "元.");
    

2.不同物流商的订单服务代码

/**
 * 顺丰订单服务
 */
public class ShunfengLogisticsAdapter 

    /**
     * 创建速运订单
     */
    public ShunfengOrder createOrder(String goodsName) 
        return new ShunfengOrder("SF" + System.currentTimeMillis());
    

    /**
     * 创建生鲜订单
     */
    public ShunfengFreshOrder createFreshOrder(String goodsName) 
        return new ShunfengFreshOrder("SF" + System.currentTimeMillis());
    

/**
 * 京东物流订单服务
 */
public class JingdongLogisticsAdapter 

    /**
     * 创建物流订单
     */
    public JingdongOrder createOrder(String goodsName) 
        return new JingdongOrder("JD" + System.currentTimeMillis());
    

    /**
     * 创建生鲜订单
     */
    public JingdongFreshOrder createFreshOrder(String goodsName) 
        return new JingdongFreshOrder("JD" + System.currentTimeMillis());
    

3.不同物流商的订单模型代码

// 顺丰速运订单
public class ShunfengOrder 
    private String orderNo;
    private int logisticsFee = 10;
    // ...省略getter/setter


// 顺丰生鲜订单
public class ShunfengFreshOrder extends ShunfengOrder 

    /**
     * 生鲜订单配送费要贵
     */
    @Override
    public int getLogisticsFee() 
        return super.getLogisticsFee() * 2;
    


// 京东速运订单
public class JingdongOrder 
    private String orderNo;
    private int logisticsFee = 8;
    // ...省略getter/setter


// 京东生鲜订单
public class JingdongFreshOrder extends JingdongOrder 
    /**
     * 京东生鲜配送费固定价15块
     */
    private int logisticsFee = 15;

测试代码:

public class ShippingServiceTest 

    @Test
    public void shipNormal() 
        ShunfengLogisticsAdapter shunfengLogisticsAdapter = new ShunfengLogisticsAdapter();
        JingdongLogisticsAdapter jingdongLogisticsAdapter = new JingdongLogisticsAdapter();
        ShippingService shippingService = new ShippingServiceImpl(shunfengLogisticsAdapter, jingdongLogisticsAdapter);

        shippingService.shipNormal("高端智能空调", "shunfeng");
        shippingService.shipNormal("低端不智能只制冷空调", "jingdong");
    

    @Test
    public void shipFresh() 
        ShunfengLogisticsAdapter shunfengLogisticsAdapter = new ShunfengLogisticsAdapter();
        JingdongLogisticsAdapter jingdongLogisticsAdapter = new JingdongLogisticsAdapter();
        ShippingService shippingService = new ShippingServiceImpl(shunfengLogisticsAdapter, jingdongLogisticsAdapter);

        shippingService.shipFresh("新鲜海底捞出来大螃蟹", "shunfeng");
        shippingService.shipFresh("新鲜獐子岛逃跑扇贝", "jingdong");
    

输出:

// 普通快递发货
新鲜海底捞出来大螃蟹发货完成,运单号=JD1657108945504,费用=10元.
新鲜獐子岛逃跑扇贝发货完成,运单号=SF1657108945505,费用=20元.

// 冷链物流发货
高端智能空调发货完成,运单号=JD1657108945510,费用=8元.
低端不智能只制冷空调发货完成,运单号=SF1657108945510,费用=10元.

虽然代码跑起来也没问题,但是只要增加新的供应商,就难免要对 ShippingService 里每个发货的业务代码都改一遍,这是一件极其恶心的事情,而且随着系统越来越庞大、复杂,随意改动老代码将变得很危险。

所以我们一定要赶在业务起飞之前重构掉它,解决历史包袱,提高系统的扩展性和可维护性。

按照业务规则进行分析,ShippingService 是系统的业务入口,对具体物流服务和订单有直接依赖,那我们第一步就是先抽象出物流服务和订单,使ShippingService 只依赖接口。

代码如下:

// 物流服务
public interface LogisticsService 
    Order createOrder(String goodsName);
    Order createFreshOrder(String goodsName);


// 订单
public interface Order 
    String getOrderNo();
    int getLogisticsFee();

在设计完抽象工厂和抽象产品之后,由于 ShippingService 已经不对实现有依赖,所以完全可以把原来用于选择物流服务的 logisticsChannel 字段从方法签名中移除。那么优化以后的发货代码如下:

// 发货服务
public interface ShippingService 
    void shipNormal(String goodsName);
    void shipFresh(String goodsName);


// 发货服务实现
public class ShippingServiceImpl implements ShippingService 
    private LogisticsService logisticsService;
		// ...省略构造器

    @Override
    public void shipNormal(String goodsName) 
        Order order = logisticsService.createOrder(goodsName);
        String orderNo = order.getOrderNo();
        int logisticsFee = order.getLogisticsFee();

        System.out.println(goodsName + "发货完成,运单号=" + orderNo + ",费用=" + logisticsFee + "元.");
    

    @Override
    public void shipFresh(String goodsName) 
        Order order = logisticsService.createFreshOrder(goodsName);
        String orderNo = order.getOrderNo();
        int logisticsFee = order.getLogisticsFee();

        System.out.println(goodsName + "发货完成,运单号=" + orderNo + ",费用=" + logisticsFee + "元.");
    

此时业务代码已经相当简洁,不过由于我们把抽象工厂作为发货服务的成员变量,所以在实际的使用过程中会比原来稍微复杂一些。

测试代码:

public class ShippingServiceImplTest 

    @Test
    public void shipNormal() 
        LogisticsService logisticsService = new ShunfengLogisticsAdapter();
        ShippingService shippingService = new ShippingServiceImpl(logisticsService);
        shippingService.shipNormal("高端智能空调");


        logisticsService = new JingdongLogisticsAdapter();
        shippingService = new ShippingServiceImpl(logisticsService);
        shippingService.shipNormal("低端不智能只制冷空调");
    

    @Test
    public void shipFresh() 
        LogisticsService logisticsService = new ShunfengLogisticsAdapter();
        ShippingService shippingService = new ShippingServiceImpl(logisticsService);
        shippingService.shipFresh("新鲜海底捞出来大螃蟹");

        logisticsService = new ShunfengLogisticsAdapter();
        shippingService = new ShippingServiceImpl(logisticsService);
        shippingService.shipFresh("新鲜獐子岛逃跑扇贝");
    

输出:

// 普通快递发货
新鲜海底捞出来大螃蟹发货完成,运单号=SF1657110011628,费用=20元.
新鲜獐子岛逃跑扇贝发货完成,运单号=SF1657110011628,费用=20元.

// 冷链物流发货
高端智能空调发货完成,运单号=SF1657110011635,费用=10元.
低端不智能只制冷空调发货完成,运单号=JD1657110011649,费用=8元.

重构完成以后,如果增加新的物流供应商,完全无需对发货服务做任何改变。当然代码优化到这里并不算完,现在一个物流供应商会对应生成一个 ShippingService 实例,是完全没有必要的,不过这不算个大问题,我们有很多的方法来让代码变得更优雅,这里不做赘述。

小结

抽象工厂模式和工厂方法模式的差异:

构成\\模式工厂方法模式抽象工厂模式
工厂接口
具体工厂实现有1个有多个
产品接口有1个有多个产品接口构成产品族
具体产品实现有多个每个产品都有多个实现

前面提到抽象工厂模式主要用来解决「产品族」的创建问题,核心点是产品族和对工厂进行抽象。当抽象工厂的实现类只创建一种产品,也就是不存在「产品族」的时候,抽象工厂会退化成工厂方法模式,值得注意的是并不是把工厂方法模式里的工厂加上 Abstract 前缀就变成抽象工厂模式了。

设计模式虽然看着复杂,其实并不难学,当你带着设计模式的思想再去review实际情况,往往很容易就完成代码的优化工作,还是那句话,实践出真知,一起加油吧!

关注「Java实践笔记」公众号获取全部源代码!

版权声明:本文为公众号「Java实践笔记」的原创文章,转载请联系作者,附上原文出处链接及本声明。
原文链接:https://mp.weixin.qq.com/s/33YfZdDnIFJfDw5wDaa0bA

以上是关于设计模式实践笔记第二节:抽象工厂模式的主要内容,如果未能解决你的问题,请参考以下文章

设计模式实践笔记第二节:抽象工厂模式

设计模式抽象工厂模式 ( 简介 | 适用场景 | 优缺点 | 产品等级结构和产品族 | 代码示例 )

抽象工厂模式的优缺点和适用场景

Java设计模式——抽象工厂模式

一天一个设计模式:抽象方法模式

#yyds干货盘点#-设计模式分享-抽象工厂模式