Java设计模式实战 ~ 工厂模式剖析与实战

<<<返回总目录

1、工厂模式概述

工厂模式 负责将大量有共同接口的类实例化,工厂模式主要有如下几种形态:

  • 简单工厂(Simple Factory)模式

    又称之为静态工厂方法模式(Simple Factory Method Pattern)

  • 工厂方法(Factory Method)模式

    又称之为多态性工厂(Polymorphic Factory)模式或虚拟构造函数(Virtual Constructor)模式

  • 抽象工厂(Abstract Factory)模式

    抽象工厂是所有形态的工厂模式中最为抽象和最具一般性的形态

不管是哪种形态的工厂模式,它们的核心原则都是:屏蔽对象的创建细节,客户端只负责“消费”对象,不关心对象的创建。

2、简单工厂模式

简单工厂模式将对象的创建交给工厂类,工厂类提供一个专门创建某一类对象的静态方法,所以说简单工厂又称之为静态工厂方法模式

这样的客户端只负责消费对象,而不负责对象的创建,工厂模式屏蔽了对象的创建细节

简单工厂由三个角色组成:

  • 抽象产品(Abstract Product)
  • 具体产品(Concrete Product)
  • 工厂类(Factory)

简单工厂模式的类图:

简单工程模式UML类图

下面通过一段代码实现一个简单的工厂模式:

// 抽象产品
public interface Animal {
    void eat();

    void run();

    void sleep();
}

// 具体产品1
public class Cat implements Animal {
    @Override
    public void sleep() {
        System.out.println("小猫正在睡觉...");
    }

    @Override
    public void eat() {
        System.out.println("小猫正在吃...");

    }

    @Override
    public void run() {
        System.out.println("小猫正在跑...");
    }
}

// 具体产品2
public class Dog implements Animal {
    @Override
    public void sleep() {
        System.out.println("小狗正在睡觉...");
    }

    @Override
    public void eat() {
        System.out.println("小狗正在吃...");

    }

    @Override
    public void run() {
        System.out.println("小狗正在跑...");
    }
}

// 工厂类,动物管理员
public class ZooKeeper {
    public static Animal create(String type) {
        switch (type) {
            case "cat":
                return new Cat();
            case "dog":
                return new Dog();
            default:
                throw new IllegalArgumentException("不支持的类型:" + type);
        }
    }
}

// 测试类
public class Client {
    public static void main(String[] args) {
        Animal dog = ZooKeeper.create("dog");
        Animal cat = ZooKeeper.create("cat");
    }
}

如果具体产品类没有共同的逻辑,那么抽象产品的角色可以是一个 Java 接口,如果有相同的逻辑,那么公有逻辑移到抽象角色里,此时抽象角色可以是抽象类。为了方便起见,在实际开发中可以对 简单工厂模式 中的三个角色进行适当的 省略或合并,主要分为三种情况:

  • 省略抽象产品角色
  • 工厂角色和抽象产品角色合并
  • 三个角色全部合并

1)省略抽象产品角色

如果只有一个具体产品角色,那么可以省略抽象产品角色,这样就只剩下两个角色:具体产品和工厂

// 具体产品
public class Dog {
    public void sleep() {
        System.out.println("小狗正在睡觉...");
    }

    public void eat() {
        System.out.println("小狗正在吃...");

    }

    public void run() {
        System.out.println("小狗正在跑...");
    }
}

// 工厂类,动物管理员
public class ZooKeeper {

    public static Dog create() {
        return new Dog();
    }
}

2)工厂角色和抽象产品角色合并

工厂角色和抽象产品角色合并意思就是说抽象产品具有工厂角色的功能

这样的例子在 JDK 源码中也是很常见,比如 Calendar/DateFormat,这里仅以 Calendar 为例 :

首先 Calendar 是一个抽象类,它是一个抽象产品,然后它提供了一系列的静态工厂方法来获取实例

public abstract class Calendar implements Serializable, Cloneable, Comparable<Calendar> {
    
    // 静态工厂方法
    public static Calendar getInstance() {
        //...
    }
    public static Calendar getInstance(TimeZone zone) {
        //...
    }
    public static Calendar getInstance(Locale aLocale) {
        //... 
    }
    public static Calendar getInstance(TimeZone zone, Locale aLocale) {
        //...
    }
}

Calendar 是抽象类,不能直接被实例化,所以静态工厂方法返回的 Calendar 的子类

这样就将具体子类实例化的工作隐藏起来了,使用者无需考虑子类该如何实例化

将来如果有新的子类(新的具体产品)被添加,静态工厂方法可以将新的子类实例返回给调用者,对客户端使用者完全透明,扩展性更好。

3)三个角色全部合并

在实际开发中,根据情况也可以将工厂类和具体产品类合并,这样就只有一个角色了:具体产品类。也就是说产品类自行创建自己的实例:

public class Cat{
    public void sleep() {
        System.out.println("小猫正在睡觉...");
    }
    public void eat() {
        System.out.println("小猫正在吃...");

    }
    public void run() {
        System.out.println("小猫正在跑...");
    }
    // 静态工厂方法
    public static Cat create(String type) {
        return new Cat();
    }
}

三个角色全部合并的简单工厂方法模式在 JDK 中也有着大量的应用,如 Java 中基本类型的装包类型中的 valueOf 方法:

public static Float valueOf(float f) {
    return new Float(f);
}

public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

public static Boolean valueOf(boolean b) {
    return (b ? TRUE : FALSE);
}

// 还有其他的装包类型就一一列举了

除此以外,在依赖注入框架 Dagger2 中也有应用:

public final class FragmentPresenter_Factory
    implements Factory<FragmentPresenter> {
  private final Provider<FragmentContract.View> viewProvider;

  private final Provider<Repository> RepositoryProvider;

  public FragmentPresenter_Factory(
      Provider<FragmentContract.View> viewProvider,
      Provider<Repository> RepositoryProvider) {
    assert viewProvider != null;
    this.viewProvider = viewProvider;
    assert RepositoryProvider != null;
    this.RepositoryProvider = RepositoryProvider;
  }

  @Override
  public FragmentPresenter get() {
    return new FragmentPresenter(
        viewProvider.get(), RepositoryProvider.get());
  }

 
  // 简单工厂方法创建实例
  public static Factory<FragmentPresenter> create(
      Provider<FragmentContract.View> viewProvider,
      Provider<Repository> RepositoryProvider) {
    return new FragmentPresenter_Factory(
        viewProvider, RepositoryProvider);
  }
}

还有在介绍 建造者模式 的时候讲到的简单工厂方法模式的对比案例:接口请求参数的封装,比如某个接口用获取订单各个状态下总数,店铺又分为 “餐饮行业”“零售行业”,这个接口的调用需要告知后台是获取餐饮的还是零售的,这个参数当然就可以使用常量来表示,这个参数不用每次都从外面传递进来,因为创建的过程可以屏蔽,代码如下:

public class OrderStatusRequest{
    
    // 省略其他代码...

    // 餐饮行业,只需要传递用户ID
    public static OrderStatusRequest createForCatering(String userId) {
        OrderStatusRequest request = new OrderStatusRequest();
        request.setUserId(userId);
        request.setType(IndustryTypeUtils.getIndustryType(IndustryType.CATERING));
        return request;
    }
    
    // 零售行业,只需要传递用户ID
    public static OrderStatusRequest createForRetail(String userId) {
        OrderStatusRequest request = new OrderStatusRequest();
        request.setUserId(userId);
        request.setType(IndustryTypeUtils.getIndustryType(IndustryType.RETAIL));
        return request;
    }
}

3、简单工厂模式的优缺点

1)简单工厂模式优点

简单工厂模式的核心是工厂类,工厂类负责产品实例的创建,对使用这屏蔽了具体的创建对象的细节,使用者只负责对产品的消费,很好的对职责进行划分。相较于通过构造方法创建对象的方式有如下优势:

  • 简单工厂模式有自己的方法名称,可读性更好

    例如上面的例子:接口请求参数的封装,通过两个静态工厂方法,区分不同的“行业”,开发者很容易选择使用哪个方法。

  • 对返回的对象有更好的控制

    通过静态工厂方法获取的对象可以是通过缓存获取的,构造函数则实现不了这一点
    例如,IntegervalueOf 静态工厂方法,只要整型值在 [-128, 127] 区间,都会返回缓存好的对象:

    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }
    

    需要注意的是 127 这个值不是死的,可以配置的:

    private static class IntegerCache {
            static final int low = -128;
            static final int high;
            static final Integer cache[];
    
            static {
                // high value may be configured by property
                int h = 127;
                String integerCacheHighPropValue =
                    sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
                if (integerCacheHighPropValue != null) {
                    try {
                        int i = parseInt(integerCacheHighPropValue);
                        i = Math.max(i, 127);
                        // Maximum array size is Integer.MAX_VALUE
                        h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
                    } catch( NumberFormatException nfe) {
                        // If the property cannot be parsed into an int, ignore it.
                    }
                }
                high = h;
    
                cache = new Integer[(high - low) + 1];
                int j = low;
                for(int k = 0; k < cache.length; k++)
                    cache[k] = new Integer(j++);
    
                // range [-128, 127] must be interned (JLS7 5.1.7)
                assert IntegerCache.high >= 127;
            }
    
            private IntegerCache() {}
        }
    
    
  • 简单工厂方法可以返回原类型的任何子类型对象

    这个已经在介绍 工厂角色和抽象产品角色合并 一节介绍过了。

2)Dart 语言中的简单工厂模式

正是由于简单工厂模式的优越性,在很多情况都是优于 构造器 的,所以有些语言将 简单工厂模式 作为一项 语法特性 集成语言当中,方便开发者使用简单工厂模式,例如 Dart 语言中使用 factory 关键字实现工厂模式。现在流行的跨平台技术 Flutter,就是使用 Dart 语言来开发的。在 Dart 中有一个概念是工厂构造器 (Factory Contructor ),语法就是在 构造器 前面加上 factory 关键字就变成了工厂构造器,但是这并不是真正意义上的构造器,因为在里面无法访问 this,更像一个静态的方法, 只不过方法的名字就是类名:

// 工厂构造器
factory Person(String name){
	// 省略代码...
	// 如何创建一个对象,自己说了算
	// 你也可以从缓存里取一个对象返回
}

// 使用上面的工厂构造器
 var person = Person();

可以看出,对于客户端来说和普通的 new 对象没有任何区别,但是实际上这个对象不见得是 new 出来的, 也有可能是从缓存里取的,这个取决于工厂构造器里面的具体实现,所以对于客户端来说是透明的,屏蔽了对象的创建细节,只管消费即可。

我们在上面提到了简单工厂方法模式的一个优点是有名字, 可读性很强。但是上面的工厂构造器名字就是类名,从名字上看,我们看不出来它的功能。所以 Dart 支持自定义 工厂构造器 的名字,如:

// 创建一个女人
factory Person.female(String name){
    // 省略代码...
}

// 使用有名字的工厂构造器
var person = Person.female("FlowerQueen");

Dart 源码中很多就用了这个特性,如 Future 这个类:

factory Future.delayed(Duration duration, [FutureOr<T> computation()]) {
	// 省略无关代码...	
}

factory Future.error(Object error, [StackTrace stackTrace]) {
	// 省略无关代码...	
}

// Future 是一个抽象类,所以里面的工厂方法返回的是它的子类
// 所以工厂角色和抽象角色是合并的
// Future 的子类又是私有的,外面是不能直接使用
// 所以外界只能接触到工厂角色/抽象角色,扩展性更好

可以看出有名字的工厂构造器可读取性很好,开发者知道这个方法是干什么的。

那么 Dart 为什么还要提供名字为类名的工厂构造器呢?

我个人的理解是:简单工厂模式有很多优点,不是说就不用了构造方法的方式 new 一个对象, 所以一般有两种请使用简单工厂模式:一种是开发前我就知道要用简单工厂模式,还有一种是开发前我不知道要使用简单工厂模式,使用了构造器的方式 new 对象,后面随着业务的发展,这个类是应该使用简单工厂模式的,如果 Dart 不支持名字为类名的工厂构造器,那么 new 这个类的对象的地方我都需要改成静态工厂方法的名字,这可能需要需改的地方就很多了。Dart 提供了在 构造器 前加 factory 关键字,可以将 传统的构造器 平滑的改成 工厂构造器(简单工厂方法模式),而对于使用的地方不需要做任何改动,这样让开发者更加方便的将已有代码改成简单工厂模式,将重构的影响降到最低。

3)简单工厂模式缺点

简单工厂模式的缺点主要体现在两个方面:

  • 工厂类比较臃肿

    因为工厂类包含了所有产品的创建逻辑,形成一个无所不知的类,如果产品比较多,工厂类就变得比较庞大

  • 开闭原则 支持的不够彻底

    对于 开闭原则 不是很了解的,可以看我前面的文章《设计模式 ~ 面向对象 6 大设计原则剖析与实战》
    开闭原则 简而言之就是 对修改关闭,对扩展开放。如果我们需要创建一个新的产品,对消费的地方(Client)是成立,它不需要修改,所以从这个角度看是符合开闭原则的
    但是工厂类需要修改,每新增一个新的产品,都需要修改工厂类的代码,从这个角度看是不符合开闭原则的。所以说简单工厂对开闭原则支持不彻底

4、工厂方法模式

上面我们提到了 简单工厂 的一些缺点,接下来我们看下 工厂方法模式是如何保持简单工厂的优点,克服了简单工厂模式的缺点的。

我们先看下工厂模式的定义: 定义一个用于创建对象的接口,让子类决定实例化哪个类,工厂方法使一个类的实例化延迟到其子类

工厂方法模式包含下面4个角色:

  • 抽象工厂

    该角色是工厂方法模式的核心,任何创建对象的工厂都需要实现该接口

  • 具体工厂

    该角色实现抽象工厂接口,用于Client调用创建具体产品

  • 抽象产品

    该角色是产品对象的共同父类,抽象具体产品的公共逻辑

  • 具体产品

    该角色实现了抽象产品接口,一个具体工厂创建对应的具体产品

工厂方法模式的类图如下所示:

工厂方法模式类图

下面我们还是上面 动物园 的例子,现在改成每中动物一个工厂类,而不是用一个工厂类

抽象工厂类

public interface AnimalFactory {
    Animal create();
}

Dog工厂类

public class DogFactory implements AnimalFactory {
    @Override
    public Animal create() {
        return new Dog();
    }
}

Cat工厂类

public class CatFactory implements AnimalFactory {
    @Override
    public Animal create() {
        return new Cat();
    }
}

测试类

public class Client {
    public static void main(String[] args) {
        AnimalFactory dogFactory = new DogFactory();
        Animal dog = dogFactory.create();
        dog.eat();

        AnimalFactory catFactory = new CatFactory();
        Animal cat = catFactory.create();
        cat.eat();
    }
}

// 控制台输出:

小狗正在吃...
小猫正在吃...

需要注意的是工厂方法应当返还的是产品的抽象类型,而不是具体类型,只有这样才能保证针对产品的多态性。如果工厂方法返回类型声明为具体类型,客户端从工厂方法的声明上可以得知将要得到的是什么类型的对象,这违背了工厂方法模式的用意,这也就是为什么工厂方法模式也叫做多态性工厂。另一方面,如果只有一个具体工厂,抽象工厂角色也可以省略。

5、抽象工厂模式

抽象工厂是所有形态的工厂模式中最为抽象和最具一般性的形态,它的类图如下所示:

abstract-factory

抽象工厂可以向客户端提供一个接口,使得客户端再不必指定产品的具体类型的情况下,创建多个 产品族 中的产品对象。

抽象工厂模式与工厂方法模式的最大区别在于工厂方法模式针对的是一个 产品等级结构,而抽象工厂模式则需要多个 产品等级结构

上面提到了两个关键字:产品族产品等级结构

那什么是产品等级结构?什么是产品族?这是理解抽象工厂的关键

产品的等级结构 ,比如苹果这个产品的等级结构: Fruit <- Apple <- RedApple,这整个属于 1 个产品等级结构

产品族 是指位于不同产品等级结构中,功能相关联的产品组成的家族

使用一个二维坐标系来表示,如下图所示:

抽象工厂产品族

上面的坐标系中,横轴表示 产品等级结构,纵轴表示 产品族。一共 4 个产品族 ,3 个等级结构

上图给出的三个不同的等级结构具有平行的结构,意思就是说这三类产品都是平级的。

如果使用工厂方法模式的话,需要三个独立的工厂等级来应付这三个产品等级结构。随着产品等级结构(产品类目)的数目增加,工厂方法模式所需要的工厂等级结构数目也会随着增加。

这个时候抽象工厂模式就闪亮登场了,使用同一工厂等级结构来应对这些相同或者相似的产品等级结构,也就是说一个工厂等级可以创建分属于不同的产品等级结构的一个产品族中的所有对象,这样描述可能比较难以理解,通过下面的图帮助理解:

抽象工厂
从上图可以看出,一个工厂等级结构可以创建出分属于不同产品等级结构的一个产品族中的所有对象,显然,抽象工厂模式比工厂方法模式更加有效率。

这个时候读者可能有疑问了,从上面的图来看, 如果使用 工厂方法模式 貌似效率更高呀,只需要 3 个工厂类,使用了抽象工厂模式反而需要 4 个工厂类,你怎么能说抽象工厂模式比工厂方法模式效率更高呢?这需要从两个方面来谈一谈:

  • 抽象工厂模式主要是解决多个产品等级结构的

    也就是说抽象工厂模式的侧重点是多个产品等级结构,看下面的图就明白了:
    多个产品族

  • 抽象工厂模式主要是用来创建具有共性的、分布在不同产品等级结构中的所有产品

    比如下面的 抽象工厂模式的应用 中的例子,WinButtonWinText,分别属于不同的产品等级结构中,但是它们有一个共性就是都属于 Windows 控件,如果使用工厂方法模式的话,需要有 ButtonFactoryTextFactory,此时如果只需要创建 Windows 平台的 ButtonText,需要用到 ButtonFactoryTextFactory 两个类,这两个类即可创建 Windows 控件,也包含了 Unix 控件的功能,但是创建 Unix 控件的功能我们不需要。如果使用抽象工厂模式的话,只需要 WinFactory 即可,可以很好的解决这个问题, 因为它就是用来创建具有共性的、分布在不同产品等级结构中的产品族中的所有对象。

1)抽象工厂模式的应用

假设子系统中需要一些产品对象,而这些产品对象又属于一个以上的产品等级,那么为了将消费这些产品对象的责任和创建这些产品对象的责任分割开,可以使用抽象工厂。这句话怎么理解呢?下面我们可以通过一个代码案例来分析。

创建一个不同系统的视图控件,例如在 Windows 系统有 ButtonText 等控件,同样在 Unix 系统也有。也就是说有 2 个产品等级结构了:ButtonText

Button和Text产品等级结构

同时也有 2 个产品族,通过坐标系表示如下图所示:

坐标系表示法

下面通过代码的方式来实现下上面描述的场景

抽象产品

public interface Button {
}

public interface Text {
}

具体产品

public class WinButton implements Button {
}

public class UnixButton implements Button {
}

抽象工厂

public interface Factory {

    Button createButton();
    
    Text createText();
}

具体工厂

public class WinFactory implements Factory {
    @Override
    public Button createButton() {
        return new WinButton();
    }

    @Override
    public Text createText() {
        return new WinText();
    }
}


public class UnixFactory implements Factory {
    @Override
    public Button createButton() {
        return new UnixButton();
    }

    @Override
    public Text createText() {
        return new UnixText();
    }
}

测试

public class Client {
    public static void main(String[] args) {
        Factory winFactory = new WinFactory();
        Button winBtn = winFactory.createButton();
        Text winTxt = winFactory.createText();

        Factory unixFactory = new UnixFactory();
        Button unixBtn = unixFactory.createButton();
        Text unixTxt = unixFactory.createText();
    }
}

从上面代码可以看出, 有多少个产品等级结构,就会在工厂角色中发现有多少个工厂方法。每个产品等级结构中有多少具体产品,就会有多少个产品族,也就会在工厂等级结构总发现多少个具体工厂。

2)抽象工厂模式对开闭原则的支持

开闭原则就是对修改关闭对扩展开放,也就是在不修改原有代码的情况下,通过扩展的方式增强功能。

通过上面的分析,抽象工厂有两个核心概念:产品族产品等级结构

所以增强抽象工厂模式的功能,要么增加产品族,要么增加新的产品等级结构,总结一句话就是横向扩展还是纵向扩展

先来看下 增加产品族,也就是纵向扩展。这个时候只需要增加新的具体工厂即可,原来的工厂类不需要修改,从只增加产品族的角度看抽象工厂是支持开闭原则的,如下图所示:

抽象工厂模式纵向扩展

再来看下 增加产品等级结构,也就是横向扩展。这个时候需要修改所有的工厂角色,因为需要添加新的方法。从只新增产品等级结构的角度看抽象工厂是不支持开闭原则的,如下图所示:

抽象工厂模式横向扩展

也就是说,抽象工厂模式和工厂方法模式一样,对 开闭原则 的支持不够彻底。

小结

本文详细介绍了工厂模式中的简单工厂模式、工厂方法模式、抽象工厂模式。虽然 简单工厂模式 相较于构造方法有很多优势,但是对 开闭原则 支持不够彻底,然后引出了 工厂方法模式 是如何克服这些不足的。最后介绍了 工厂方法模式 无法很好应对多个产品等级结构的情况,于是引出了 抽象工厂模式,同时也介绍了抽象工厂适用的应用场景。希望本文能够帮助你深入掌握工厂模式,在实际开发中能够选择对应的工厂模式来解决实际问题。

Reference

  • 《设计模式之禅》
  • 《Java设计模式及实践》
  • 《Java设计模式深入研究》
  • 《Java与模式》
  • 《Effective Java》
  • 9
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Chiclaim

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值