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

Posted 李阿昀

tags:

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

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

引子:咖啡店点餐系统

在学习工厂方法模式之前,我们先来看一个需求,设计一个咖啡店点餐系统。下面我们就来分析一下该需求。

咖啡店点餐系统点的肯定是咖啡,所以我们需要设计一个咖啡类(即Coffee),而咖啡它又有不同的品种:美式咖啡、拿铁咖啡等。这样,我们又得设计两个类,即美式咖啡类(即AmericanCoffee)和拿铁咖啡类(即LatteCoffee),很显然,我们得让这俩类去继承咖啡类,因为它俩可以向上抽取出共性的东西,而这些共性的东西可以放在咖啡类里面,用以提高代码的一个复用性。关于咖啡的这三个类设计完了之后,咱们还得设计一个咖啡店类,该类里面就具有点咖啡的功能,客户来了之后可以进行咖啡的一个点餐。

根据以上简单的分析,相信大家能画出下面这样的一个类图来。

先看以上类图的右边部分,顶层有一个父类,即咖啡类(Coffee),它里面定义有如下三个方法:

  1. getName方法:获取咖啡的名称。由于不同品种的咖啡返回的名称是不一样的,所以该方法应该是一个抽象的方法,要求子类自己去重写
  2. addMilk方法:加奶。几乎所有的咖啡都需要加奶
  3. addSugar方法:加糖。几乎所有的咖啡都需要加糖

再来看咖啡类下面的两个子类,一个是美式咖啡类(即AmericanCoffee),一个是拿铁咖啡类(即LatteCoffee),它俩都重写了父类中的getName方法。

然后,我们再来看一下以上类图的左边部分,有一个咖啡店类(即CoffeeStore),它里面只有一个orderCoffee方法,它是用来点咖啡的,并且它里面还有一个String类型的参数。该参数表示的是什么含义呢?客户要点什么咖啡,不管是美式咖啡还是拿铁咖啡,他是不是得告诉咖啡店的前台服务员啊,然后咖啡店的前台服务员再根据客户所点咖啡类型去点餐。如果客户点的是美式咖啡,那么他只须给咖啡店的前台服务员传递一个american字符串就行;如果客户点的是拿铁咖啡,那么他只须给咖啡店的前台服务员传递一个latte字符串就行。

以上就是我们对咖啡店点餐系统的设计类图的一个分析。分析完了之后,接下来,我们就要开始编写代码来实现了。

首先,打开咱们的maven工程,在com.meimeixia.pattern包下新建一个子包,即factory.before,我们是在该包中来编写以上咖啡店点餐系统的代码的。

然后,创建一个咖啡类,即Coffee。

package com.meimeixia.pattern.factory.before;

/**
 * 咖啡类
 * @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("加奶");
    

接着,我们再创建以上咖啡类的两个子类。先创建第一个子类,即美式咖啡类(AmericanCoffee),注意,该美式咖啡类要去继承Coffee类并重写里面的getName方法哟!

package com.meimeixia.pattern.factory.before;

/**
 * 美式咖啡
 * @author liayun
 * @create 2021-05-31 21:40
 */
public class AmericanCoffee extends Coffee 
    @Override
    public String getName() 
        return "美式咖啡";
    

再创建第二个子类,即拿铁咖啡类(LatteCoffee),同样,该类得去继承Coffee类并重写里面的getName方法。

package com.meimeixia.pattern.factory.before;

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

至此,Coffee类以及它的子类就已经全部创建完毕了。

接下来,我们再创建一个咖啡店类,即CoffeeStore。根据以上设计类图,相信你能写出来下面这样一个类。

package com.meimeixia.pattern.factory.before;

/**
 * 咖啡店
 * @author liayun
 * @create 2021-05-31 21:54
 */
public class CoffeeStore 

    public Coffee orderCoffee(String type) 
        // 声明Coffee类型的变量,根据不同类型创建不同的Coffee子类对象
        Coffee coffee = null;
        if ("american".equals(type)) 
            coffee = new AmericanCoffee();
         else if ("latte".equals(type)) 
            coffee = new LatteCoffee();
         else 
            throw new RuntimeException("对不起,您所点的咖啡没有");
        
        // 加配料
        coffee.addMilk();
        coffee.addSugar();

        return coffee;
    


最后,我们来创建一个测试类来测试一下。

package com.meimeixia.pattern.factory.before;

/**
 * @author liayun
 * @create 2021-05-31 22:09
 */
public class Client 
    public static void main(String[] args) 
        // 1. 创建咖啡店类
        CoffeeStore store = new CoffeeStore();
        // 2. 点咖啡
        Coffee coffee = store.orderCoffee("american");
        System.out.println(coffee.getName());
    

运行以上测试类,发现控制台打印了如下内容,我们想点的是美式咖啡,结果打印的也是美式咖啡,还加奶、加糖了,这说明咱们写的代码是没有任何问题的。

至此,以上咖啡店点餐系统的代码我们就已经实现完毕了。

接下来,我们来分析一下以上咖啡店点餐系统的实现代码有没有什么问题。

大家不妨再来看一下咖啡店类,该类里面的点咖啡功能是要依赖于美式咖啡类和拿铁咖啡类的,如果后期我们再加一种咖啡的品种的话,那么势必就要修改这个方法里面的代码了,那么这就违背软件设计原则里面的开闭原则(即对修改关闭)了,而我们现在是肯定要修改之前的代码的。

用更加专业一点的话说,就是在Java中,万物皆对象,这些对象都需要创建,如果创建的时候直接new该对象,那么就会对该对象耦合严重,假如我们要更换对象,那么所有new对象的地方都需要修改一遍,这显然违背了软件设计的开闭原则。

问题是知道了,那么如何进行解决呢?其实大家想一想现实生活中的场景就知道解决方案了。比如说,我现在要用一台电脑,那么我是自己去生产一台电脑吗?很显然不是,我们直接购买一台就行,那么电脑是由谁去生产的呢?由具体的电脑工厂去生产,是不是啊?手机也是一样的,我们用一个手机,并不需要自己去生产,而是直接去买一个就行,手机也是由具体的手机工厂去生产的。

所以我们要改进以上点咖啡的案例,就得去解除咖啡店和具体咖啡的一个耦合了,这样,我们就要引入工厂思想了,通过工厂来生产对象,而我们只需要通过工厂去获取对象就行了,也就是说,我们只需要和工厂打交道就行,如此一来,就彻底和对象解耦了,当然,这儿的对象指的就是具体的咖啡对象。如果要更换对象,那么直接在工厂里面更换该对象即可,于此,便能达到与对象解耦的目的了。所以说,工厂模式最大的优点就是解耦

在本教程中我会向大家介绍三种工厂的使用,它们分别是:

  1. 简单工厂模式:它并不属于GOF的23种经典设计模式,也即没被收录到里面,但是在真正的开发中也有很多人习惯去用简单工厂模式
  2. 工厂方法模式
  3. 抽象工厂模式

下面,我先来为大家介绍一下简单工厂模式。

简单工厂模式

概念

简单工厂模式不是一种设计模式,反而比较像一种编程习惯,如果我们以后开发有遇到适用的场景,那么就可以使用简单工厂模式了。

结构

简单工厂模式里面包含如下角色:

  • 抽象产品:定义了产品的规范,描述了产品的主要特性和功能。上面点咖啡的案例中就用到了抽象产品这个角色,就是咖啡类(即Coffee),它里面定义了一套规范
  • 具体产品:实现或者继承抽象产品的子类。在上面点咖啡的案例中,具体产品指的就是拿铁咖啡、美式咖啡
  • 具体工厂:提供了创建产品的方法,调用者通过该方法来获取产品

可以看到,如果我们现在使用简单工厂模式来改进以上点咖啡的案例的话,那么只需要多一个角色(即具体工厂)就行了。

接下来,我们就来看一下如何使用简单工厂模式来改进以上点咖啡的案例。

实现

现在使用简单工厂模式对上面案例进行改进,类图设计如下:

可以看到,咖啡相关的类是没有发生任何变化的,而咖啡店类发生了一点变化,即该类里面的点咖啡功能发生了变化,现在我们是通过简单咖啡工厂来生产咖啡,而不用自己再去new对象了。此外,相比之前的类图,我们还加入了一个简单咖啡工厂类,它就是用来生产咖啡的。

分析完了之后,接下来,我们就要开始编写代码来实现了。

首先,在com.meimeixia.pattern.factory包下新建一个子包,即simple_factory,我们是在该包中来编写以上点咖啡案例改进之后的代码的。

由于咖啡相关的类没有发生任何变化,所以我们可以将以上案例中的咖啡相关的类拷贝过来。

  • 咖啡类

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

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

然后,创建一个简单咖啡工厂类,即SimpleCoffeeFactory,该类是用来生产咖啡的。

package com.meimeixia.pattern.factory.simple_factory;

/**
 * 简单咖啡工厂类,用来生产咖啡
 * @author liayun
 * @create 2021-05-31 22:48
 */
public class SimpleCoffeeFactory 
    public Coffee createCoffee(String type) 
        // 声明Coffee类型的变量,根据不同类型创建不同的Coffee子类对象
        Coffee coffee = null;
        if ("american".equals(type)) 
            coffee = new AmericanCoffee();
         else if ("latte".equals(type)) 
            coffee = new LatteCoffee();
         else 
            throw new RuntimeException("对不起,您所点的咖啡没有");
        

        return coffee;
    

可以看到,简单咖啡工厂类中的生产咖啡功能的实现逻辑和我们之前咖啡店类中的点咖啡功能的实现逻辑是一样的。

接着,再创建一个咖啡店类,即CoffeeStore。根据以上设计类图,相信你能写出来下面这样一个类。

package com.meimeixia.pattern.factory.simple_factory;

/**
 * 咖啡店
 * @author liayun
 * @create 2021-05-31 21:54
 */
public class CoffeeStore 

    public Coffee orderCoffee(String type) 
        SimpleCoffeeFactory factory = new SimpleCoffeeFactory();
        // 调用生产咖啡的方法
        Coffee coffee = factory.createCoffee(type);
        // 加配料
        coffee.addMilk();
        coffee.addSugar();

        return coffee;
    


现在,对于咖啡店类来说,它是不是就不依赖于具体的咖啡产品对象了啊!这样,就解除了咖啡店和具体的咖啡产品对象之间的一个耦合。

最后,我们来创建一个测试类来测试一下。

package com.meimeixia.pattern.factory.simple_factory;

/**
 * @author liayun
 * @create 2021-05-31 23:04
 */
public class Client 
    public static void main(String[] args) 
        // 创建咖啡店类对象
        CoffeeStore store = new CoffeeStore();
        Coffee coffee = store.orderCoffee("latte");
        System.out.println(coffee.getName());
    

运行以上测试类,发现控制台打印了如下内容,我们想点的是拿铁咖啡,结果打印的也是拿铁咖啡,还加奶、加糖了,这说明咱们写的代码是没有任何问题的。

接下来,我们不妨再来分析一下以上改进后的案例的代码。

上面我们引入了一个简单咖啡工厂类,它是用来处理创建对象的细节的,也就是说,我们把到底创建的是美式咖啡还是拿铁咖啡这段逻辑放在了该简单咖啡工厂类里面。如果我们以后要去创建具体的咖啡对象的话,那么直接从工厂中获取就行了,因为现在咖啡店类中的orderCoffee方法变成了工厂对象的客户。这样,便解除了咖啡店类和具体咖啡子类之间的一个耦合。

但同时它又产生了新的耦合,哪个耦合呢?现在工厂对象和具体的产品对象已经耦合死了。如果我们后期要添加一个新的咖啡的品种的话,那么我们还需要去修改简单工厂类里面的代码,这个就是一个新的耦合。

而且,如果我们后期要添加新的咖啡的品种的话,那么势必还要修改简单工厂类里面的代码,而这违背了开闭原则(开闭原则指的就是对修改关闭,对扩展开放),现在很明显我们要修改原有的代码。

那么,使用简单工厂模式和我们之前的做法有什么区别呢?工厂类的客户端可能有很多(只是现在咱们工厂类的客户端只有一个,就是咖啡店),比如创建美团外卖、甜品店(甜品店也有卖咖啡)等,如果要是我们之前的做法,那么就得修改很多客户端的代码了,有咖啡店客户端的代码,有美团外卖客户端的代码,有甜品店客户端的代码。而如果我们要是引入了简单工厂类的话,那么我们只需要去修改简单工厂类的代码即可,省去了其他的修改操作,这就是使用简单工厂模式所带来的好处。

优缺点

简单工厂模式有如下优缺点:

  • 优点:封装了创建对象的过程,可以通过参数直接获取对象。把对象的创建和业务逻辑分开(现在的业务逻辑就是咖啡店点咖啡的这个业务,而该业务已经与具体的咖啡对象解除耦合了),这样以后就避免了修改客户端代码,如果要实现新产品,那么直接修改工厂类即可,而不需要再在原代码上进行修改了,这样就降低了客户端代码修改的可能性,更加容易扩展
  • 缺点:增加新产品时还是需要修改工厂类的代码,违背了开闭原则。虽然违背了开闭原则,但是比之前不用简单工厂模式还是要更好一些

扩展

静态工厂模式

在本小节,我们对简单工厂模式进行一个扩展,即静态工厂模式。

在以后真正的开发中也有一部分人将工厂类中的创建对象的功能定义为静态的,这个就是静态工厂模式,注意了,它也不是23种设计模式中的,只不过是一种习惯的写法而已。

下面我们就使用静态工厂模式来实现以上案例。

实现非常简单,只需要把之前的工厂类(即SimpleCoffeeFactory)中的创建具体咖啡对象的方法定义为静态的就可以了,其他类的代码都不需要进行一个改动。

package com.meimeixia.pattern.factory.static_factory;

/**
 * 简单咖啡工厂类,用来生产咖啡
 * @author liayun
 * @create 2021-05-31 22:48
 */
public class SimpleCoffeeFactory 
    public static Coffee createCoffee(String type) 
        // 声明Coffee类型的变量,根据不同类型创建不同的Coffee子类对象
        Coffee coffee = null;
        if ("american".equals(type)) 
            coffee = new AmericanCoffee();
         else if ("latte".equals(type)) 
            coffee = new LatteCoffee();
         else 
            throw new RuntimeException("对不起,您所点的咖啡没有");
        

        return coffee;
    

从上可以看到,我们在com.meimeixia.pattern.factory包下新建了一个子包,即static_factory,也就是说,使用静态工厂模式实现以上案例的代码我们都放在了该包下。注意了,大家还要记得将其他的类都拷贝到static_factory包下哟!

以上SimpleCoffeeFactory类修改完毕之后,客户端(即咖啡店类)也得稍微修改一下,即无需再去创建咖啡工厂对象,而只需要通过SimpleCoffeeFactory类调用它里面的静态方法去获取一个具体的咖啡对象就行了。

package com.meimeixia.pattern.factory.static_factory;

/**
 * 咖啡店
 * @author liayun
 * @create 2021-05-31 21:54
 */
public class CoffeeStore 

    public Coffee orderCoffee(String type) 
        /*SimpleCoffeeFactory factory = new SimpleCoffeeFactory();
        // 调用生产咖啡的方法
        Coffee coffee = factory.createCoffee(type);*/
        Coffee coffee = SimpleCoffeeFactory.createCoffee(type);
        // 加配料
        coffee.addMilk();
        coffee.addSugar();

        return coffee;
    


那么使用静态工厂模式有什么好处呢?使用静态工厂模式唯一的一个好处就是我们在其他的客户端,例如美团外卖,从工厂对象里面去获取咖啡对象时,就不需要再去创建咖啡工厂对象了,直接通过类名去调用它里面的方法就能获取到。

简单工厂模式 + 配置文件

在本小节,我们再来学习一个简单工厂模式的扩展,即使用简单工厂模式+配置文件的方式来解除耦合,注意,这种方式可以完全的解除耦合哟!而且在真正的开发中,我们会经常使用这种固定的套路,并且使用的还比较多哟!对了,我们无比熟悉的Spring框架,其实它底层就用到了这种方式。

我们再来看一下更为专业的描述:

可以通过工厂模式+配置文件的方式解除工厂对象和产品对象之间的耦合。在工厂类中加载配置文件中的全类名(注意,配置文件只需要加载一次即可),并通过反射创建对象进行存储,客户端如果需要对象的话,那么直接进行获取即可。

看完上面这段描述之后,我们知道我们还得创建一个配置文件,并还需要在配置文件中去配置类的全类名。接下来,我就来教大家如何使用简单工厂模式+配置文件的方式来实现以上点咖啡的案例。

首先,在com.meimeixia.pattern.factory包下新建一个子包,即config_factory,使用简单工厂模式+配置文件的方式来实现以上点咖啡案例的代码我们都放在了该包下。

然后,将咖啡类、美式咖啡类、拿铁咖啡类这三个类从上面拷贝到config_factory包下,因为这三个类并不需要进行修改。本想着就不在这里贴出来这三个类的代码了,但转念一想,为了让大家看得更清楚,还是贴出来吧!

  • 咖啡类

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

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

接着,在maven工程的src > main > resources目录下创建一个配置文件,为了演示方便,这里我们不妨使用properties文件来作为配置文件,文件名字就起为bean.properties,你看咋样?文件内容如下:

american=com.meimeixia.pattern.factory.config_factory.AmericanCoffee
latte=com.meimeixia.pattern.factory.config_factory.LatteCoffee

紧接着,创建工厂类,当然了,这里我们创建的是静态工厂模式的工厂类。

package com.meimeixia.pattern.factory.config_factory;

import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Properties;
import java.util.Set;

/**
 * @author liayun
 * @create 2021-06-01 22:26
 */
public class CoffeeFactory 
    /**
     * 我们所要所做的事情,就是加载配置文件,然后去获取配置文件中配置的全类名,并创建该类的对象进行存储。
     */

    // 1. 定义容器对象存储咖啡对象
    /*
     * 容器选择双列集合
     *      键:(咖啡)名称
     *      值:咖啡对象
     */
    private static HashMap<String, Coffee> map = new HashMap<String, Coffee>();

    // 2. 加载配置文件中的全类名,并通过反射创建对象进行存储,注意,配置文件只需要加载一次。
    // 由于配置文件只需要加载一次,所以我们最好将代码写在静态代码块里面
    static 
        // 2.1 创建Properties对象
        Properties p = new Properties();
        // 2.2 调用p对象的load方法进行配置文件的加载,注意,配置文件(即bean.properties)是在类路径下面哟!
        InputStream is = CoffeeFactory.class.getClassLoader().getResourceAsStream("bean.properties");
        try 
            p.load(is);
            // 从p集合中获取全类名并创建对象
            Set<Object> keys = p.keySet();
            for (Object key : keys) 
                // 获取到全类名
                String className = p.getProperty((String) key);
                // 通过反射技术创建对象
                Class clazz = Class.forName(className);
                Coffee coffee = (Coffee) clazz.newInstance();
                // 将名称和对象存储到容器中
                map.put((String) key, coffee);
            
         catch (Exception e) 
            e.printStackTrace();
        
    

    /**
     * 根据名称获取Coffee对象
     * @param name:咖啡的名称
     * @return
     */
    public static Coffee createCoffee(String name) 
        return map.get(name);
    


从以上工厂类的代码中可以看到,静态成员变量用来存储创建的对象(键存储的是名称,值存储的是对应的对象)的,读取配置文件以及创建对象的代码是写在静态代码块中的,目的就是只需要执行一次,也就是说配置文件只需要加载一次。

最后,我们创建一个测试类来测试一下。相信你能看懂下面的测试代码吧!非常简单。

package com.meimeixia.pattern.factory.config_factory;

/**
 * @author liayun
 * @create 2021-06-01 22:47
 */
public class Client 
    public static void main(String[] args) 
        Coffee coffee = CoffeeFactory.createCoffee(从零开始学习Java设计模式 | 创建型模式篇:单例设计模式

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

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

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

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

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