设计模式(四):抽象工厂模式

Published on 2024-03-04 14:55 in 分类: 博客 with 狂盗一枝梅
分类: 博客

一、抽象工厂定义

抽象工厂模式侧重于“产品族”的概念,有了这个概念之后,它允许工厂类中有多个抽象方法分别生产不同的对象,这些对象组成一个“产品族”,举个例子:

有这样一组对象:运输工具+ 引擎+ 控制器 。 它可能会有几个变体:

  1. 汽车+ 内燃机+ 方向盘
  2. 飞机+ 喷气式发动机+ 操纵杆

可以发现他们之间有规律的形成了“系列产品”,可以将系列产品的生产打包到一个工厂生产。

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

工厂方法模式 抽象工厂模式
针对的是单个产品等级结构 针对的是面向多个产品等级结构
一个抽象产品类 多个抽象产品类
可以派生出多个具体产品类 每个抽象产品类可以派生出多个具体产品类
一个抽象工厂类,可以派生出多个具体工厂类 一个抽象工厂类,可以派生出多个具体工厂类
每个具体工厂类只能创建一个具体产品类的实例 每个具体工厂类可以创建多个具体产品类的实例

二、案例

先来回顾下工厂方法模式下的“文件解析”案例:

简单工厂模式-工厂方法模式.drawio

使用了三个工厂分别生产三种解析器,似乎没什么问题,但是现在有个新需求:

张三和李四分别实现了自己的一套文件解析器,包含Xml、Csv以及Json,这如何将他们纳入到这个体系中呢?

解决方式一:还是用工厂方法模式

每个解析类和工厂类都派生出两个单独的子类,比如XmlFileParser派生出ZhangSanXmlFileParserLiSiXmlFileParserXmlFileParserFactory派生出ZhangSanXmlFileParserFactoryLiSiXmlFileParserFactory

这样似乎能解决问题,但是类太多了,如果我要新增一种文件解析类型,一下得新增四个类:张三的文件解析器、张三的文件解析工厂、李四的文件解析器、李四的文件解析工厂。

似乎过于繁琐了。

解决方式二:使用抽象工厂模式

分析:张三、李四两个对象进来实际上是新增加了一个“维度”,这个维度比较高,甚至能把所有的文件解析器和工厂都分成“张三的”或者“李四”的,类图如下

抽象工厂设计模式

这样就完成了抽象工厂模式的改造。现在工厂类能创建多种对象了,而且我将工厂类只划分了两类:张三和李四工厂类,工厂类生产的也只是各自的FileParser。

三、总结

抽象工厂模式的缺点:增加新的产品等级结构麻烦,需要对原有系统进行较大的修改,甚至需要修改抽象层代码,这显然会带来较大的不便,违背了“开闭原则”。

抽象工厂模式的优点:工厂方法模式只有1种create, 当你要新增一类产品的时候, 得重新设计工厂接口类。 而抽象工厂中, 直接工厂接口类上新增方法, 后面的子类同一实现即可。 新增接口方法, 不需要再新增接口类

因此这个工厂更像一个有很多能力的大工厂了, 不再是一个简单的create()了

抽象工厂模式隔离了具体类的生成,使得客户并不需要知道什么被创建。由于这种隔离,更换一个具体工厂就变得相对容易,所有的具体工厂都实现了抽象工厂中定义的那些公共接口,因此只需改变具体工厂的实例,就可以在某种程度上改变整个软件系统的行为。 当一个产品族中的多个对象被设计成一起工作时,它能够保证客户端始终只使用同一个产品族中的对象。 增加新的产品族很方便,无须修改已有系统,符合“开闭原则”。

四、工厂模式的应用

工厂模式几乎在所有的框架中都有应用,以下看看几个应用

1、日志框架slf4j

先看一段常用的代码:

private static final Logger logger = LoggerFactory.getLogger(Application.class);

然后打印日志的时候调用logger的info、warn、error等方法。

很明显,从名字上就能看出来这里使用了工厂模式创建了Logger对象,依次点进去查看源代码,可以看到抽象工厂的顶层接口

image-20240304140923529

它只有一个方法getLogger,而且它的返回值Logger也是一个interface

image-20240304141051667

由此可知,它是一个很标准的工厂方法模式无疑了。

2、jdk的Calendar类

一段经典的代码

Calendar calendar = Calendar.getInstance();

看看它创建实例的方法

private static Calendar createCalendar(TimeZone zone,
                                           Locale aLocale)
{
    CalendarProvider provider =
        LocaleProviderAdapter.getAdapter(CalendarProvider.class, aLocale)
                             .getCalendarProvider();
    if (provider != null) {
        try {
            return provider.getInstance(zone, aLocale);
        } catch (IllegalArgumentException iae) {
            // fall back to the default instantiation
        }
    }

    Calendar cal = null;

    if (aLocale.hasExtensions()) {
        String caltype = aLocale.getUnicodeLocaleType("ca");
        if (caltype != null) {
            switch (caltype) {
            case "buddhist":
            cal = new BuddhistCalendar(zone, aLocale);
                break;
            case "japanese":
                cal = new JapaneseImperialCalendar(zone, aLocale);
                break;
            case "gregory":
                cal = new GregorianCalendar(zone, aLocale);
                break;
            }
        }
    }
    if (cal == null) {
        // If no known calendar type is explicitly specified,
        // perform the traditional way to create a Calendar:
        // create a BuddhistCalendar for th_TH locale,
        // a JapaneseImperialCalendar for ja_JP_JP locale, or
        // a GregorianCalendar for any other locales.
        // NOTE: The language, country and variant strings are interned.
        if (aLocale.getLanguage() == "th" && aLocale.getCountry() == "TH") {
            cal = new BuddhistCalendar(zone, aLocale);
        } else if (aLocale.getVariant() == "JP" && aLocale.getLanguage() == "ja"
                   && aLocale.getCountry() == "JP") {
            cal = new JapaneseImperialCalendar(zone, aLocale);
        } else {
            cal = new GregorianCalendar(zone, aLocale);
        }
    }
    return cal;
}

这段代码根据不同的timezone和Locale创建不同的Calendar实例,可以确认使用的是简单工厂模式,但是Calendar类又有些特殊,特殊之处就在于Calendar类不是以Factory作为后缀来命名的,而且,返回的对象是自身,在实际的开发中,工厂类的设计往往比较灵活。



参考文档:

https://bbs.huaweicloud.com/blogs/338666

https://developer.aliyun.com/article/780994?spm=a2c6h.13262185.profile.20.1d934622twKSqB


#设计模式
目录
复制 复制成功