从零开始学习Java设计模式 | 创建型模式篇:抽象工厂模式

Posted 李阿昀

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从零开始学习Java设计模式 | 创建型模式篇:抽象工厂模式相关的知识,希望对你有一定的参考价值。

在本讲,我们来学习一下创建型模式里面的第三个设计模式,即抽象工厂模式。

前言

前面介绍的工厂方法模式中考虑的是一类产品,如畜牧场只养动物、电视机厂只生产电视机(不生产空调、冰箱等其它的电器)、计算机学院只培养计算机软件专业的学生等。

这些工厂只生产同种类产品,而同种类产品又被称为同等级产品,也就是说,工厂方法模式只考虑生产同等级的产品,但是在现实生活中许多工厂都是综合型的工厂,能生产多等级(种类) 的产品,如电器厂既生产电视机又生产洗衣机或空调,苹果代工厂既生产苹果电脑又生产苹果手机或苹果iPad,大学既有软件专业又有生物专业等。

本讲要介绍的抽象工厂模式将考虑多等级产品的生产,将同一个具体工厂所生产的位于不同等级的一组产品称为一个产品族。这句话大家可能不是特别好理解,有点绕是不是?但没关系,下面我们来看一张图,相信大家就能很好地理解了。

上图所示横轴是产品等级,也就是同一类产品,从图中可以看到不管是一体机,还是笔记本,还是台式机,它们都是同一级别的产品,即电脑;纵轴是产品族,也就是同一品牌的产品,同一品牌的产品产自同一个工厂,例如苹果代工厂既生产苹果电脑又生产苹果手机或苹果iPad,生产出的这些产品就属于同一个产品族。

经过以上分析,相信大家对产品族和产品等级这两个概念有了一定的认识,接下来,我们再来看一张图,以便加深理解。

先来看左边部分,这部分的东东都是属于同一个产品族的,里面包含有外套、领带、衬衫、西裤、皮鞋等等这些产品,很明显能感受到这个产品族是属于商务风格的。再来看右边部分,这部分的东东同样属于同一个产品族,里面包含有运动上衣、毛衫、运动裤子、运动鞋等等这些产品,看得出来这个产品族是属于运动风格的。

那么在上面这张图中,如何去体现同级别的产品呢?同级别的产品就是同种类的产品,这点大家要知道哟!

商务风的上衣是上衣,运动风的上衣它也是上衣,所以它们属于同级别的产品。再比如鞋子,皮鞋和运动鞋就属于不同产品族里面的同种类产品。

至此,通过上面两张图我就为大家讲清楚了产品族和产品等级这两个概念,大家好好消化一下。

理解清楚以上概念之后,下面我就要为大家讲解抽象工厂模式了。

抽象工厂模式

概念

抽象工厂模式是一种为访问类提供一个创建一组相关或相互依赖对象的接口,且访问类无须指定所要产品的具体类就能得到同族的不同等级的产品的模式结构。

这概念真的蛮不好理解的,下面我用自己的话给大家详细解释一下。

先看前半段话,什么意思呢?创建对象肯定是由工厂来创建的,那工厂创建的又是哪些对象呢?一组相关的或者相互依赖的对象。例如,我现在开了一个服装工厂去生产运动风格的服装,那么不管是上衣还是内衣还是裤子还是鞋,这些就构成了一组相关的产品。

再来看后半段话,什么意思呢?我现在的服装工厂生产的是运动风格(运动风格指的就是产品族哟😊)的服装,那么该工厂必然就会有生产上衣、运动鞋等功。当我们访问这样一个工厂时,就可以直接使用它里面的功能去获取对应风格的上衣、内衣、裤子以及鞋了。

最后,我得给大家说一下,抽象工厂模式是工厂方法模式的升级版本,工厂方法模式只生产同一个等级的产品,而抽象工厂模式可生产多个等级的产品。

结构

抽象工厂模式的主要角色如下:

  • 抽象工厂(Abstract Factory):提供了创建产品的接口,它包含多个创建产品的方法,可以创建多个不同等级的产品,也就是说,每一个方法生产的是同一个产品族里面的不同级别的产品
  • 具体工厂(Concrete Factory):主要是实现抽象工厂中的多个抽象方法,完成具体产品的创建,也就是说,具体产品还是在具体的工厂里面去创建
  • 抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能,抽象工厂模式有多个抽象产品
  • 具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间是多对一的关系

你会发现以上这些角色和工厂方法模式里面的是一样的,只不过稍微有点区别,主要区别就在于抽象工厂和具体工厂这俩角色上。

实现

刚才我们聊了一下抽象工厂模式的概念,接下来我们便来实现一个案例,再深入的去理解一下抽象工厂模式。

依旧是咖啡店的那个案例,只不过需求有所变化,我们来看一下。

现在咖啡店的业务发生了改变,不仅要生产咖啡还要生产甜点,如提拉米苏、抹茶慕斯等,如果要是按照工厂方法模式,那么需要定义提拉米苏类、抹茶慕斯类、提拉米苏工厂类、抹茶慕斯工厂类以及甜点工厂类等,这就很容易发生类爆炸情况了,即类太多了。

我们不妨再分析一下以上这些类的特点,你会发现一个问题,就是拿铁咖啡、美式咖啡是一个产品等级,都是咖啡;提拉米苏、抹茶慕斯也是一个产品等级;拿铁咖啡和提拉米苏是同一产品族(也就是都属于意大利风味),美式咖啡和抹茶慕斯是同一产品族(也就是都属于美式风味)。

所以这个案例可以使用抽象工厂模式来实现,而且设计出来的类图应该是下面这样子。

先来看以上类图的左边部分,这块是甜点相关类,有甜点类及其子类,即提拉米苏类和抹茶慕斯类,它俩是属于同一个级别的产品;再来看右边部分,这块是咖啡相关类,有咖啡类及其子类,即拿铁咖啡类和美式咖啡类;最后来看下面部分,这块是工厂的一个设计,首先我们设计了一个甜点工厂,它具有生产咖啡与甜点的功能,不过要注意,它是一个抽象工厂(即接口),然后我们为其设计了两个具体工厂(即子实现类),一个是美式风味的甜点工厂,它可以生产美式风味的咖啡和甜点,一个是意大利风味的甜点工厂,它可以生产意大利风味的咖啡和甜点。

经过我们上面这样的一个设计,你会发现美式风味的甜点工厂可以生产抹茶慕斯和美式咖啡这两个产品,意大利风味的甜点工厂可以生产拿铁咖啡和提拉米苏这两个产品。

至此,以上类和类之间的关系我们就已经搞明白了,接下来,我们就要开始编写代码来实现了。

首先,打开咱们的maven工程,在com.meimeixia.pattern.factory包下新建一个子包,即abstract_factory,使用抽象工厂模式实现以上案例的代码我们都放在了该包下。

然后,创建咖啡相关的类。

  • 咖啡类

    package com.meimeixia.pattern.factory.abstract_factory;
    
    /**
    * 咖啡类
    * @author liayun
    * @create 2021-05-31 21:35
    */
    public abstract class Coffee 
        public abstract String getName();
    
        // 加糖
        public void addSugar() 
            System.out.println("加糖");
        
    
        // 加奶
        public void addMilk() 
            System.out.println("加奶");
        
    
    
  • 美式咖啡类

    package com.meimeixia.pattern.factory.abstract_factory;
    
    /**
    * 美式咖啡
    * @author liayun
    * @create 2021-05-31 21:40
    */
    public class AmericanCoffee extends Coffee 
        @Override
        public String getName() 
            return "美式咖啡";
        
    
    
  • 拿铁咖啡类

    package com.meimeixia.pattern.factory.abstract_factory;
    
    /**
    * 拿铁咖啡
    * @author liayun
    * @create 2021-05-31 21:53
    */
    public class LatteCoffee extends Coffee 
        @Override
        public String getName() 
            return "拿铁咖啡";
        
    
    

接着,创建甜点相关的类。

  • 甜点类

    package com.meimeixia.pattern.factory.abstract_factory;
    
    /**
    * 甜点抽象类
    * @author liayun
    * @create 2021-06-01 18:56
    */
    public abstract class Dessert 
        public abstract void show();
    
    
  • 提拉米苏类

    package com.meimeixia.pattern.factory.abstract_factory;
    
    /**
    * 提拉米苏类
    * @author liayun
    * @create 2021-06-01 18:59
    */
    public class Trimisu extends Dessert 
        @Override
        public void show() 
            System.out.println("提拉米苏");
        
    
    
  • 抹茶慕斯类

    package com.meimeixia.pattern.factory.abstract_factory;
    
    /**
    * 抹茶慕斯类
    * @author liayun
    * @create 2021-06-01 19:03
    */
    public class MatchMousse extends Dessert
        @Override
        public void show() 
            System.out.println("抹茶慕斯");
        
    
    

至此,抽象产品类和具体产品类我们就已经创建完毕了。接下来,我们就需要创建工厂相关的类了。

首先,创建一个甜点工厂接口,即DessertFactory,也就是抽象工厂角色。

package com.meimeixia.pattern.factory.abstract_factory;

/**
 * @author liayun
 * @create 2021-06-01 19:07
 */
public interface DessertFactory 
    // 生产咖啡的功能
    Coffee createCoffee();

    // 生产甜品的功能
    Dessert createDessert();

然后,创建美式风味的甜点工厂类,即AmericanDessertFactory,记住,要让该类去实现甜点工厂接口,并重写它里面的所有抽象方法。

package com.meimeixia.pattern.factory.abstract_factory;

/**
 * 美式风味的甜点工厂
 *      生产美式咖啡和抹茶慕斯
 * @author liayun
 * @create 2021-06-01 19:12
 */
public class AmericanDessertFactory implements DessertFactory 
    @Override
    public Coffee createCoffee() 
        return new AmericanCoffee();
    

    @Override
    public Dessert createDessert() 
        return new MatchMousse();
    

接着,创建意大利风味的甜点工厂类,即ItalyDessertFactory,同理,要让该类去实现甜点工厂接口,并重写它里面的所有抽象方法。

package com.meimeixia.pattern.factory.abstract_factory;

/**
 * 意大利风味的甜点工厂
 *      生产拿铁咖啡和提拉米苏甜点
 * @author liayun
 * @create 2021-06-01 19:17
 */
public class ItalyDessertFactory implements DessertFactory 
    @Override
    public Coffee createCoffee() 
        return new LatteCoffee();
    

    @Override
    public Dessert createDessert() 
        return new Trimisu();
    

至此,以上类图中所涉及到的类我们就全部都创建完毕了。

最后,我们创建一个测试类来测试一下。现在我们想点意大利风味的咖啡、甜点来吃,那么测试代码就应该像下面这样写,很简单,我就不再多说了。

package com.meimeixia.pattern.factory.abstract_factory;

/**
 * @author liayun
 * @create 2021-06-01 19:28
 */
public class Client 
    public static void main(String[] args) 
        // 创建的是意大利风味的甜点工厂对象
        ItalyDessertFactory factory = new ItalyDessertFactory();
        // 创建的是美式风味的甜点工厂对象
        // AmericanDessertFactory factory = new AmericanDessertFactory();
        // 获取拿铁咖啡和提拉米苏甜点
        Coffee coffee = factory.createCoffee();
        Dessert dessert = factory.createDessert();
        System.out.println(coffee.getName());
        dessert.show();
    

运行以上测试类看结果,如下图所示,发现确实打印的是拿铁咖啡和提拉米苏,这说明咱们写的代码是没有任何问题的。

如果要是我们的口味变了,想点美式风味的咖啡、甜点,那么应该怎么办呢?很简单,只须创建美式风味的甜点工厂对象即可,其他代码压根就不需要去修改。

现在,我们就能得出这样一个结论了,使用了抽象工厂模式之后,如果要加同一个产品族的话,那么我们只需要再加一个对应的工厂类即可,而不再需要修改其他的类了,这也符合了软件设计原则里面的开闭原则。

优缺点

抽象工厂模式有如下优缺点:

  • 优点:当一个产品族中的多个对象被设计成一起工作时,它能保证客户端始终只使用同一个产品族中的对象。

    这是什么意思呢?我给大家简单的去解释一下,就拿以上案例来说,意大利风味的甜点工厂它只生产拿铁咖啡和提拉米苏这样的产品,你如果没有去创建其它的工厂对象的话,那么你就只能点这俩产品,这保证了它俩的一个搭配。如果后期你想换口味了,想点拿铁咖啡和抹茶慕斯,那么对不起,做不到!那么问题是不是就随之产生了啊!

  • 缺点:当产品族中需要增加一个新的产品时,所有的工厂类都需要进行修改。

    这是什么意思呢?还是拿以上案例来说,现在生产的有两类产品,一类是甜点,一类是咖啡,这时如果我们再添加一类产品,即汉堡,那么你会发现所有的工厂类(包括抽象工厂和具体工厂)的代码都需要进行修改,因为我们需要再加一个方法,即生产汉堡的方法。

使用场景

明确了抽象工厂模式的优缺点之后,接下来我们就得知道它的使用场景了。

抽象工厂模式有如下这么几个使用场景:

  • 当需要创建的对象是一系列相互关联或相互依赖的产品族时,如电器工厂中的电视机、洗衣机、空调等
  • 系统中有多个产品族,但每次只使用其中的某一族产品,如有人只喜欢穿某一个品牌的衣服和鞋,他只需要去该品牌店里面购买就行了
  • 系统中提供了产品的类库,且所有产品的接口相同,客户端不依赖产品实例的创建细节和内部结构。也就是说,客户端并不关注产品实例创建的一些细节性的东西,而是直接通过工厂去获取就行了,因为这些产品是有一个相同的接口的。

当然了,同学看到这,还并不是特别明确抽象工厂模式具体的使用场景,这也没关系,下面我会举几个现实中的案例来跟大家说说。

第一个案例——输入法换皮肤。要是大家使用搜狗输入法的话,你会发现一套皮肤,例如觉醒年代主题的一套皮肤,有其logo、背景颜色、背景图片,你要是从默认皮肤换成觉醒年代主题皮肤的话,那么logo、背景颜色、背景图片等等这些是不是都得跟着换啊!而且是一整套一整套的去换。那么这是不是就跟我们上面每次只使用其中的某一族产品是一样的啊!你可以将一套皮肤中的logo、背景颜色、背景图片等等理解成是一个一个的产品。

第二个案例——生成不同操作系统的程序。只要我们的操作系统发生了一个变化,那么其他的对象是不是肯定也要一系列的发生变化呀!

以上是关于从零开始学习Java设计模式 | 创建型模式篇:抽象工厂模式的主要内容,如果未能解决你的问题,请参考以下文章

从零开始学习Java设计模式 | 创建型模式篇:原型模式

从零开始学习Java设计模式 | 创建型模式篇:原型模式

从零开始学习Java设计模式 | 创建型模式篇:抽象工厂模式

从零开始学习Java设计模式 | 创建型模式篇:抽象工厂模式

从零开始学习Java设计模式 | 创建型模式篇:工厂方法模式

从零开始学习Java设计模式 | 创建型模式篇:工厂方法模式