2

写在前面

这篇博文介绍设计模式的形式将与其他篇博文不太一样,这里我们将从一个问题入手,逐步了解到简单工厂、工厂方法与抽象工厂模式。

PS:这篇博文涉及的内容较多,所以篇幅有点长,请耐心阅读。

为什么要使用工厂模式?

在 OO 设计中,有一个重要的设计原则:针对接口编程而不针对实现编程。每当我们使用 new 去实例化一个对象时,用到的就是实现编程,而不是接口。这样以来代码绑定着具体类,会导致代码更脆弱,缺乏弹性。

在技术上,使用 new 没有错,毕竟这是 Java 的基础部分。真正错的是“改变”,以及它会影响 new 的使用。针对接口编程,可以隔离掉以后系统可能发生的一堆改变。原因是,如果代码针对接口编程,那么通过多态,它可以与任何新类实现该接口。

与以往的使用 new 的方式不同,工厂模式使用“工厂”实例化对象。这样一来代码的扩展性变得更强,又可以降低代码之间的耦合。

一、从问题中引出工厂模式

1.1 问题描述

比萨店:假设你有一家比萨店,为了吸引更多的顾客,所以你们的比萨店提供了很多种类型的比萨,比如奶酪比萨,希腊比萨等。针对这个问题设计比萨从生产到售卖相关的类。

设计图
这里写图片描述

1.2 代码实现

比萨类 Pizza

package com.jas.simplefactory;

public abstract class Pizza {
    public String name;
    public String sauce;
    
    public void prepare(){
        System.out.println("准备 ..." + name);
        System.out.println("添加配料 ..." + sauce);
    }
    
    public void bake(){
        System.out.println("烘烤 25 分钟。");
    }
    
    public void cut(){
        System.out.println("把比萨饼切成对角片。");
    }
    
    public void box(){
        System.out.println("将比萨放到比萨商店的盒子中。");
    }
}

奶酪比萨类 CheesePizza

package com.jas.simplefactory;

public class CheesePizza extends Pizza {
    public CheesePizza(){
        name = "奶酪比萨";
        sauce = "大蒜番茄酱";
    }
}

希腊比萨类 GreekPizza

package com.jas.simplefactory;

public class GreekPizza extends Pizza{
    public GreekPizza(){
        name = "希腊比萨";
        sauce = "大蒜番茄酱";
    }
}

比萨商店类 PizzaStore

package com.jas.simplefactory;

public class PizzaStore {
    public Pizza orderPizza(String type){
        Pizza pizza = null;
        
        //比萨商店负责生产比萨
        if("cheese".equals(type)){
            pizza = new CheesePizza();
        }else if("greek".equals(type)){
            pizza = new GreekPizza();
        }
        
        pizza.prepare();
        pizza.bake();
        pizza.cut();
        pizza.box();
        
        return pizza;
    }
}

测试类 PizzaTestDrive

package com.jas.simplefactory;

public class PizzaTestDrive {
    public static void main(String[] args) {
        
        PizzaStore pizzaStore = new PizzaStore();
        // 点一份奶酪比萨
        pizzaStore.orderPizza("cheese");
    }
}

    /**
     * 输出
     * 
     * 准备 ...奶酪比萨
     * 添加配料 ...大蒜番茄酱
     * 烘烤 25 分钟。
     * 把比萨饼切成对角片。
     * 将比萨放到比萨商店的盒子中。
     * 
     */

1.3 设计引发的问题

上面代码中我们只列出了两种类型的比萨,在实际生产的过程中肯定不止这两种比萨。为了解决这个问题我们可以在 PizzaStore 类中的 orderPizza() 方法中添加类型判断,从而生产不同类型的比萨。所以,随着比萨类型的增多,这个方法会不停的进行判断。

假如某个比萨的销量不好,我们不想再生产这种类型的比萨,那么我们必须要找到 PizzaStore 类中的 orderPizza() 方法,将该种类型的比萨删除。

所以我们总结出:比萨的类型是多变的。这时候我们就要重新考虑设计的方式,简单工厂模式就可以帮我们解决这个问题。它将一些容易改变的代码抽取出来,单独封装。

正好符合我们的一个设计原则:找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起。

二、工厂模式登场

2.1 简单工厂模式亮相

(1)简单工厂模式

在《Head First 设计模式》一书中这样描述简单工厂:简单工厂其实并不是一个设计模式,反而比较像一种编程习惯。但是由于经常被使用,所以我们给它一个“Head First Pattern 荣誉奖”。

(2)简单工厂组成结构

  • 工厂类角色:含有一定的商业逻辑和判断逻辑,用来创建产品。
  • 抽象产品角色:它一般是具体产品继承的父类或者实现的接口。
  • 具体产品角色:工厂类所创建的对象就是此角色的实例。

(3)简单工厂设计图

这里写图片描述

(4)简单工厂代码实现

工厂类 SimplePizzaFactory

package com.jas.simplefactory;

public class SimplePizzaFactory {
    private SimplePizzaFactory(){};
    
    public static Pizza createPizzs(String type){
        Pizza pizza = null;
        
        if("cheese".equals(type)){
            pizza = new CheesePizza();
        }else if("greek".equals(type)){
            pizza = new GreekPizza();
        }

        pizza.prepare();
        pizza.bake();
        pizza.cut();
        pizza.box();

        return pizza;
    }
}

改写比萨商店类 PizzaStore

package com.jas.simplefactory;

public class PizzaStore {
    public Pizza orderPizza(String type){
        Pizza pizza = null;
        
        pizza = SimplePizzaFactory.createPizzs(type);
        
        return pizza;
    }
}

测试类 PizzaTestDrive

package com.jas.simplefactory;

public class PizzaTestDrive {
    public static void main(String[] args) {
        
        PizzaStore pizzaStore = new PizzaStore();
        //点一份希腊比萨
        pizzaStore.orderPizza("greek");
        
    }
}

    /**
     * 输出
     * 
     * 准备 ...希腊比萨
     * 添加配料 ...大蒜番茄酱
     * 烘烤 25 分钟。
     * 把比萨饼切成对角片。
     * 将比萨放到比萨商店的盒子中。
     * 
     */

(5) 简单工厂总结

使用简单工厂模式,我们将 PizzaStroe 中易改变的代码抽取出来单,独用一个工厂类封装起来。当我们想要增加或减少比萨种类时,我们不必在去修改 PizzaStroe 类,直接修改 SimplePizzaFactory 类中的代码即可。

这样以来代码变得更灵活了,代码的复用性变得更强。当其他类中也需要 Pizza 实例时,也可以直接从 SimplePizzaFactory 类中获得。

2.2 工厂方法模式亮相

(1)工厂方法模式

工厂方法模式定义了一个创建对象的接口,但由子类决定要实例化的类是哪一个。工厂方法让类把实例化推迟到子类。

(2)工厂方法模式组成结构

  • 抽象工厂角色:是具体工厂角色必须实现的接口或者必须继承的父类。在 java 中它由抽象类或者接口来实现。声明了抽象的工厂方法。
  • 具体工厂角色:它含有和具体业务逻辑有关的代码。由应用程序调用以创建对应的具体产品的对象。实现了父类或接口中定义的抽象工厂方法。
  • 抽象产品角色:它是具体产品继承的父类或者是实现的接口。在 java 中一般由抽象类或者接口来实现。
  • 具体产品角色:具体工厂角色所创建的对象就是此角色的实例。在 java 中由具体的类来实现。

(3)工厂方法模式的 UML 图解

这里写图片描述

(4) 比萨店连锁啦

由于比萨店的销量比较好,你挣了一大笔钱,因此你想开几家分店。最终你选择在北京和上海各开一家分店。但是由于地区的差异,北京和上海两地的口味也不一样。

北京人希望比萨的调料会多一些,而上海人则希望比萨的调料少一些。所以根据口味的不同,你要制造不同口味的比萨来满足顾客。

我们已经有了一个简单的比萨生产工厂 SimplePizzaFactory,但是这个工厂加工的比萨并不对北京和上海两地人的胃口。于是就想着再建两个比萨生产工厂 (BJPizzaFactorySHPizzaFactory),分别生产不同口味的比萨。

但是这样做有一个不好的地方,虽然不同的比萨生产工厂能够生产出对应的比萨,但是其他的方法比如:烘焙,切片和包装却使用着自己的生产流程 (生产一致则存在大量重复代码)。但是你希望这些生产流程在每个工厂都应该一样 (复用烘焙,切片和包装的代码,或者覆盖它们),使用简单工厂的方式就不再能够行的通了。

于是工厂方法就站了出来。

(5)工厂方法模式设计图

这里写图片描述

(6)工厂方法模式代码实现

代码用了上面的 Pizza 类。

抽象工厂角色 PizzaStore 抽象类

package com.jas.factorymethod;

import com.jas.simplefactory.Pizza;

public abstract class PizzaStore {
    public Pizza orderPizza(String type){
        Pizza pizza = null;
        
        pizza = createPizza(type);
        pizza.prepare();
        pizza.bake();
        pizza.cut();
        pizza.box();
        
        return pizza;
    }

    /**
     * 工厂方法,将生产比萨的方法定义成抽象方法,让子类去实现
     * @param type 比萨的类型
     * @return 对应的比萨实例
     */
    protected abstract Pizza createPizza(String type);
}

具体工厂角色 BJPizzaStore 类 (这里为了防止篇幅过长,省略了 SHPizzaStore 类和对应的比萨类)

package com.jas.factorymethod;

import com.jas.simplefactory.Pizza;

public class BJPizzaStore extends PizzaStore {
    @Override
    protected Pizza createPizza(String type) {
        Pizza pizza = null;
        
        if("cheese".equals(type)){
            pizza =  new BJCheesePizza();
        }else if("greek".equals(type)){
            pizza = new BJGreekPizza();
        }
        
        return pizza;
    }
}

具体产品角色 BJCheesePizza 类 (这里只定义一种比萨的种类)

package com.jas.factorymethod;

import com.jas.simplefactory.Pizza;

public class BJCheesePizza extends Pizza {
    public BJCheesePizza(){
        name = "北京人喜欢吃的奶酪比萨";
        sauce = "浓浓的大蒜番茄酱";
    }

    /**
     * 覆盖 curt() 方法将比萨切成块状
     */
    @Override
    public void cut(){
        System.out.println("把比萨饼切成方块状。");
    }
}

测试类

package com.jas.factorymethod;

public class PizzaTestDrive {
    public static void main(String[] args) {
        
        PizzaStore pizzaStore = new BJPizzaStore();
        //根据北京人的口味要一个奶酪比萨
        pizzaStore.orderPizza("greek");
        
    }
}

    /**
     * 输出
     * 
     * 准备 ...北京人喜欢吃的希腊比萨
     * 添加配料 ...浓浓的大蒜番茄酱
     * 烘烤 25 分钟。
     * 把比萨饼切成方块状。
     * 将比萨放到比萨商店的盒子中。
     * 
     */

(7)工厂方法模式总结

简单工厂模式中将生产比萨的代码单独抽取了出来,用一个工厂进行封装,由该工厂生产比萨实例。但是由于开了比萨连锁店,建立多个比萨工厂并不是好的解决办法。

所以我们使用了工厂方法模式,在比萨 PizzaStore 抽象类中定义了一个抽象方法 createPizza() 方法,由其子类去实现该方法,这样以来不同的连锁店就能生产自己的比萨了。

如果增加产品或者改变产品的实现,PizzaStore 并不需要做任何的改变。以很好的方式实现了 PizzaStore 与具体比萨之间的解耦。

2.3 抽象工厂模式亮相

(1) 抽象工厂模式

抽象工厂模式提供一个接口,用于创建相关或依赖对象的家族,而不需要明确指定具体类。

抽象工厂允许客户使用抽象的接口来创建一组相关的产品,而不需要知道或关心产出的具体产品类是什么。这样一来,客户就从具体的产品中被解耦。

(2) 抽象工厂模式的 UML 图解

这里写图片描述

(3)比萨店连锁店出了一些状况

比萨连锁店成功的关键是在于新鲜、高质量的原料。但是有的连锁店开始使用低价的原料来增加利润,为了防止比萨店的声誉遭到破坏,你必须采取相对应的措施。为了解决这个问题,你打算新建一家生产原料的工厂,并将原料运往各家加盟店,这样以一来,所有的加盟店都使用你生产的原料来制作比萨。

但是这样做却有一个问题:因为加盟店坐落于不同的地域,比如北京和上海,它们需要的酱料是不一样的。所以为了满足需求,你为它们准备了两组不同的原料。
于是你有了一个想法,你打算创建一个原料接口,由不同地域的工厂自己去实现。

(4)抽象工厂模式设计图

这里写图片描述

(5) 抽象工厂模式代码实现

抽象原料工厂 PizzaInfgredientFactory 接口

package com.jas.abstractfactory;

public interface PizzaInfgredientFactory {
    Dough createDough();
    Cheese createCheese();
}

具体原料工厂 SHPizzaInfgredientFactory

package com.jas.abstractfactory;

/**
 * 这里是上海原料加工厂,生产的原料是薄面团与 Reggiano 干酪
 */
public class SHPizzaInfgredientFactory implements PizzaInfgredientFactory {
    @Override
    public Dough createDough() {
        return new ThinCrustDough();
    }

    @Override
    public Cheese createCheese() {
        return new ReggianoCheese();
    }
}

抽象 Pizza

package com.jas.abstractfactory;

public abstract class Pizza {
    public String name;
    //为了篇幅过长,不再贴出原料相关代码
    public Dough dough;
    public Cheese cheese;

    public abstract void prepare();

    public void bake(){
        System.out.println("烘烤 25 分钟。");
    }

    public void cut(){
        System.out.println("把比萨饼切成对角片。");
    }

    public void box(){
        System.out.println("将比萨放到比萨商店的盒子中。");
    }
}

具体 CheesePizza 类 (这里只新建了一个比萨具体实现类)

package com.jas.abstractfactory;

public class CheesePizza extends Pizza {
    PizzaInfgredientFactory infgredientFactory = null;
    
    public CheesePizza(PizzaInfgredientFactory infgredientFactory){
        this.infgredientFactory = infgredientFactory;
    }
    
    @Override
    public void prepare() {
        System.out.println("准备" + name);
        dough = infgredientFactory.createDough();
        cheese = infgredientFactory.createCheese();
    }
}

具体 SHPizzaStore

package com.jas.abstractfactory;

public class SHPizzaStore {
    PizzaInfgredientFactory infgredientFactory = new SHPizzaInfgredientFactory();
    
    protected Pizza createPizza(String type){
        Pizza pizza = null;
        
        if("cheese".equals(type)){
            //生产比萨的原料来自上海的原料加工工厂
            pizza = new CheesePizza(infgredientFactory);
            pizza.name = "原料来自上海工厂加工的奶酪比萨";
        }
        
        pizza.prepare();
        pizza.bake();
        pizza.cut();
        pizza.box();
        return pizza;
    }
}

测试类 PizzaTestDrive

package com.jas.abstractfactory;

public class PizzaTestDrive {
    public static void main(String[] args) {

        SHPizzaStore pizzaStore = new SHPizzaStore();
        pizzaStore.createPizza("cheese");
    }
}

     /**
     * 准备原料来自上海工厂加工的奶酪比萨
     * 烘烤 25 分钟。
     * 把比萨饼切成对角片。
     * 将比萨放到比萨商店的盒子中。
     */

(6)抽象工厂模式总结

通过抽象工厂所提供的接口,可以创建产品的家族,利用这个接口书写代码,我们的代码将从实际工厂解耦,以便在不同上下文中实现各式各样的工厂,制造各样不同的产品。

三、工厂方法模式与抽象工厂模式总结

工厂方法与抽象工厂的工作都是负责创建对象,但是工厂方法使用的方法是继承,而抽象工厂使用的是组合。

这意味着,利用工厂方法创建对象,需要扩展一个类,并实现其中的工厂方法。由这个方法创建对象,只不过这个方法通过子类创建对象,用这种做法,客户只需要知道他们所使用的抽象类型就可以了,而由子类负责决定具体类型。所以,工厂方法只负责将客户从具体类型中解耦。

抽象工厂提供一个类来创建一个产品家族的抽象类型,这个类型的子类定义了产品被产生的方法。要使用这个工厂,必须先要实例化它,然后将它传入一些针对抽象类型所写的代码中。

所以,和工厂方法一样,抽象工厂也可以实现客户从所使用地实际具体产品中解耦。但是它还有另一个优点:可以把一群相关的产品集合起来创建。但是这也有一个缺点:如果新加入创建的产品,就必须要改变接口...这样做的后果是很严重的。

其实抽象工厂中的具体工厂经常使用工厂方法来创建产品。总之,它们两个都可以将对象的创建封装起来,使应用程序解耦,因此降低程序之间的依赖。

当你需要创建产品家族和想让制造的相关产品集合起来时,你可以使用抽象工厂。当你需要创建一种类型的对象时,你可以选择使用工厂方法。

参考资料

《Head First 设计模式》


留兰香
123 声望5 粉丝