设计模式——4、工厂模式
  • 作者:ZJWave
  • 分类: 设计模式
  • 发表:2019-03-27 10:10
  • 围观:619
  • 评论:0

准备好开始烘烤某些松耦合的OO设计。除了使用new操作符之外,还有更多制造对象的方法。你将了解到实例化这个活动不应该总是公开的进行,也会认识到初始化经常造成“耦合”问题。你不希望这样,对吧?读下去,你将了解工厂模式如何从复杂的依赖中帮你脱困。

当看到“new”,就会想到“具体”

是的,当使用“new”时,你的确是在实例化一个具体类,所以用的确实是实现,而不是接口。这是一个好问题,你已经知道了代码绑着具体类会导致代码更脆弱,更缺乏弹性。

//要使用接口让代码具有弹性,但是还得建立具体类的实例
Duck duck = new MallardDuck();

当有一群相关的具体类时,通常会写出这样的代码:

Duck duck;
//有一大堆不同的鸭子类,但是必须等到运行时,
//才知道该实例化哪个
if(picnic){
    duck = new MallardDuck();
}else if(hunting){
    duck = new DecoyDuck();
}else if(inBathTub){
    duck = new RubberDuck();
}

这里有一些要实例化的具体类,究竟实例化哪个类,要在运行时由一些条件来决定。

当看到这样的代码,一旦有变化或扩展,就必须重新打开这段代码进行检查和修改。通常这样修改的代码将造成部分系统更难维护和更新,而且也更容易犯错。

“new”有什么不对劲?

在技术上,new没有错,毕竟这是Java的基础部分。真正的犯人是我们的老朋友“改变”,以及它是如何影响new的使用的。

针对接口编程,可以隔离掉以后系统可能发生的一大堆改变。为什么呢?如果代码是针对接口而写,那么通过多态,它可以与任何新类实现该接口。但是,当代码使用大量的具体类时,等于是自找麻烦,因为一旦加入新的具体类,就必须改变代码。也就是说,你的代码并非“对修改关闭”。想用新的具体类型来扩展代码,必须重新打开它。

所以,该怎么办?当遇到这样的问题是,就应该回到OO设计原则去寻找线索。别忘了,我们第一个原则用来处理改变,并帮助我们“找出会变化的方面,把它们从不变部分分离出来”。

1.披萨店遇到的问题

识别变化的方面

假设你又一个披萨店,身为对象村内最先进的披萨店主人,你的代码可能这么写:

Pizza orderPizza(){
    //为了让系统有弹性,我们很希望这是一个抽象类或接口
    //但如果这样,这些类或接口就无法直接实例化
    Pizza pizza = new Pizza();

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

但是你需要更多披萨类型……

Pizza orderPizza(String type){
    //现在把披萨类型传入orderPizza()。
    Pizza pizza;

    //根据披萨的类型,我们实例化正确的具体类,然后将其赋值给pizza实例变量
    //请注意,这里的任何披萨都必须实现Pizza接口。
    if(type.equals("cheese")){
        pizza = new CheesePizza();
    }else if(type.equals("greek")){
        pizza = new GreekPizza();
    }else if(type.equals("pepperoni")){
        pizza = new PepperoniPizza();
    }
    
    //一旦我们有了一个披萨,需要做一些准备(就是擀揉面皮、加上佐料,例如芝士),
    //然后烘烤,切片,装盒
    //每个Pizza的子类型(CheesePizza、VeggiePizza等)都知道如何准备自己。
    pizza.prepare();
    pizza.bake();
    pizza.cut();
    pizza.box();
    return pizza;
}

但是压力来自于增加更多的披萨类型

你发现你所有的竞争者都已经在他们的菜单中加入了一些流行风味的披萨:Clam Pizza(蛤蛎披萨)、Veggie Pizza(素食披萨)。很明显,你必须要赶上他们,所以也要把这些风味加进你的菜单中。而最近Greek Pizza(希腊披萨)卖得不好,所以你决定将它从菜单中去掉:

Pizza orderPizza(String type){
    
    Pizza pizza;

    //这是变化的部分。随着时间过去,披萨菜单改变,这里就必须一改再改。
    //此代码“没有”对修改封闭。如果披萨店改变它所供应的披萨风味,就得进到这里来修改。
    if(type.equals("cheese")){
        pizza = new CheesePizza();
    }/*else if(type.equals("greek")){
        pizza = new GreekPizza();
    }*/else if(type.equals("pepperoni")){
        pizza = new PepperoniPizza();
    }else if(type.equals("clam")){
        pizza = new ClamPizza();
    }else if(type.equals("veggie")){
        pizza = new VeggiePizza();
    }
    
    //这里是我们不想改变的地方。
    //因为披萨的准备、烘烤、包装,
    //多年来都持续不变,所以这部分的
    //代码不会改变,只有发生这些动作的披萨会改变。
    pizza.prepare();
    pizza.bake();
    pizza.cut();
    pizza.box();
    return pizza;
}

很明显地,如果实例化“某些”具体类,将使orderPizza()出问题,而且也无法让orderPizza()对修改关闭;但是,现在我们已经知道哪些会改变,哪些不会改变,该是使用封装的时候了。

1.1 封装创建对象的代码

现在最好将创建对象移到orderPizza()之外,但怎么做呢?这个嘛,要把创建披萨的代码移到另一个对象中,由这个新对象专职创建披萨。

Pizza orderPizza(String type){
    Pizza pizza;

    //首先,把创建对象的方法从orderPizza()方法中抽离
    //然后把这部分的代码搬到另一个对象中,这个新对象只管如何创建披萨。
    //如果任何对象想要创建披萨,找它就对了。
    //这里该怎么写?
    ?????

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

我们称这个对象为“工厂”

工厂(factory)处理创建对象的细节。一旦有一个SimplePizzaFactory,orderPizza()就变成此对象的客户。当需要披萨时,就叫披萨工厂做一个。那些orderPizza()方法需要知道希腊披萨或者蛤蛎披萨的日子一去不复返了。现在orderPizza()方法只关心从工厂得到了一个披萨,而这个披萨实现了Pizza接口,所以它可以调用prepare()、bake()、cut()、box()来分别进行准备、烘烤、切片、装盒。

还有一些细节有待补充,比方说,原本在orderPizza()方法中的创建代码,现在该怎么写?现在就来为披萨店实现一个煎蛋的披萨工厂,来研究这个问题吧。

1.2 建立一个简单披萨工厂

先从工厂本身开始。我们要定义一个类,为所有披萨封装创建对象的代码。代码像这样……

/**
 * SimplePizzaFactory是我们的新类,
 * 它只做一件事情:帮它的客户创建披萨。
 */
public class SimplePizzaFactory {

    /**
     * 首先,在这个工厂内定义一个createPizza()方法。
     * 所有客户用这个方法来实例化新对象。
     */
    public Pizza createPizza(String type){
        Pizza pizza = null;

        //这是从orderPizza()方法中一过来的代码
        //这段代码没什么变动,和原本orderPizza()方法中的代码一样,
        //依然是以披萨的类型为参数
        if(type.equals("cheese")){
            pizza = new CheesePizza();
        }else if(type.equals("pepperoni")){
            pizza = new PepperoniPizza();
        }else if(type.equals("clam")){
            pizza = new ClamPizza();
        }else if(type.equals("veggie")){
            pizza = new VeggiePizza();
        }

        return pizza;
    }

}

问:这么做有什么好处?似乎只是把问题搬到另一个对象罢了,问题依然存在。

答:别忘了,SimplePizzaFactory可以有许多的客户。虽然目前只看到orderPizza()方法是它的客户,然而,可能还有PizzaShopMenu(披萨店菜单)类,会利用这个工厂来取得披萨的价钱和描述。可能还有一个HomeDelivery(宅急送)类,会以与PizzaShop类不同的方式来处理披萨。总而言之,SimplePizzaFactory可以有许多的客户。

所以,把创建披萨的代码包装进一个类,当以后实现改变时,只需修改这个类即可。

别忘了,我们也正要把具体实例化的过程,从客户的代码中删除!

问;我曾看过一个类似的设计方式,把工厂定义成一个静态的方法。这有何差别?

答:利用静态方法定义一个简单的工厂,这是很常见的技巧,常被称为静态工厂。为何使用静态方法?因为不需要使用创建对象的方法来实例化这个对象。但请记住,这也有缺点,不能通过继承来改变创建方法的行为。

1.3 重做PizzaStore类

是时候修改我们的客户代码了,我们所要做的是仰仗工厂来为我们创建披萨,要做这样的改变:

public class PizzaStore {

    //现在我们为PizzaStore加上一个对
    //SimplePizzaFactory的引用
    SimplePizzaFactory factory;

    //PizzaStore的构造器,需要一个工厂作为参数。
    public PizzaStore(SimplePizzaFactory factory) {
        this.factory = factory;
    }

    public Pizza orderPizza(String type){
        Pizza pizza;
        //而orderPizza()方法通过简单传入订单类型来使用工厂创建披萨
        //请注意,我们把new操作符替换成工厂对象的创建方法。
        //这里不再使用具体实例化
        pizza = factory.createPizza(type);


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

        return pizza;
    }

    //这里是其他方法
}

1.4 定义简单工厂

简单工厂其实不是一个设计模式,反而比较像是一种经常被使用的编程习惯。有些开发人员的确是把这个编程习惯误认为是“工厂模式”(Factory Pattern)。

不要因为简单工厂不是一个“真正的”设计模式,就忽略了它的用法。让我们来看看新的披萨店类图:

谢谢简单工厂来为我们暖身。接下来登场的是两个重量级的模式,它们都是工厂。但是别担心,未来还有更多的披萨!

※在提醒一次:在设计模式中,所谓的“实现一个接口”并“不一定”表示“写一个类,并利用implement关键词来实现某个Java接口”。“实现一个接口”泛指“实现某个超类型(可以是类或接口)的某个方法”。

1.5 加盟披萨店

对象村披萨店经营有成,击败了竞争者,现在大家都希望对象村披萨店能够在自家附近有加盟店。身为加盟公司经营者,你希望确保加盟店营运的质量,所以希望这些点都使用你那些经过时间考验的代码。

但是区域的差异呢?每家加盟店都可能想要提供不同风味的披萨(比方说纽约、芝加哥、加州),这受到了开店地点及该地区披萨美食家口味的影响。

我们已经有一个做法……

如果利用SimplePizzaFactory,写出三种不同的工厂,分别是NYPizzaFactory、ChicagoPizzaFactory、CaliforniaPizzaFactory,那么各地加盟店都有适合的工厂可以使用,这是一种做法。

让我们来看看会变成什么样子……

//这里创建的工厂,是制造纽约风味的披萨。
//然后建立一个披萨店,将纽约工厂的引用作为参数。
//……当我们制造披萨,会得到纽约风味的披萨。
NYPizzaFactory nyFactory = new NYPizzaFactory();
PizzaStore nyStore = new PizzaStore(nyFactory);
nyStore.orderPizza("Veggie");

//芝加哥披萨店也类似,先建立一个芝加哥风味工厂,并建立一个披萨店
//然后结合两者。制造出的披萨,就是芝加哥风味的披萨。
ChicagoPizzaFactory chicagoFactory = new ChicagoPizzaFactory ();
PizzaStore chicagoStore = new PizzaStore(chicagoFactory );
chicagoStore .orderPizza("Veggie");

但是你想要多一些质量控制……

某加盟披萨店厨师:我做披萨已经有好几年,所以想在披萨店的流程中,加入自己的“改良”。

                        ——一个好的加盟店,你“不需要”管他在披萨中放了什么东西。

在推广SimpleFactory时,你发现加盟店的确是采用你的工厂创建披萨,但是其他部分,却开始采用他们自创的流程:烘烤的做法有些差异、不要切片、使用其他厂商的盒子。

再想想这个问题,你真的希望能够建立一个框架,把加盟店和创建披萨捆绑在一起的同时又保持一定的弹性。那么该如何做才能够鱼与熊掌兼得呢?

给披萨店使用的框架

有个做法可让披萨制作活动局限于PizzaStore类,而同时又能让这些加盟店依然可以自动地制作该区域的风味。

所要做的事情,就是把createPizza()方法放回到PizzaStore中,不过要把它设置成“抽象方法”,然后为每个区域风味创建一个PizzaStore的子类。

首先,看看PizzaStore所做的改变:

/**
 * 现在PizzaStore是抽象的
 */
public abstract class PizzaStore {

    /**
     * 现在createPizza()方法从工厂对象中
     * 移回PizzaStore
     */
    public Pizza orderPizza(String type) {
        Pizza pizza;
        pizza = createPizza(type);

        //这些都没变
        pizza.prepare();
        pizza.bake();
        pizza.cut();
        pizza.box();

        return pizza;
    }

    /**
     * 现在把工厂对象移到这个方法中。
     * 在PizzaStore里,“工厂方法”现在是抽象的
     */
    abstract Pizza createPizza(String type);

}

现在已经有一个PizzaStore作为超类:让每个域类型(NYPizzaFactory、ChicagoPizzaFactory、CaliforniaPizzaFactory)都继承这个PizzaStore,每个子类各自决定如何制造披萨。让我们看看这要如何进行。

允许子类做决定

别忘了,PizzaStore已经有一个不错的订单系统,由orderPizza()方法负责处理订单,而你希望所有加盟店对于订单的处理都能够一致。

各个区域披萨店之间的差异在于他们制作披萨的风味(纽约披萨的饼薄,芝加哥披萨的饼厚等),我们现在要让createPizza()能够应对这些变化来负责创建正确种类的披萨。做法是让PizzaStore的各个子类负责定义自己的createPizza()方法。所以我们会得到一些PizzaStore具体的子类,每个子类都有自己的披萨变体,而仍然适合PizzaStore框架,并使用调试好的orderPizza()方法。

萌新:我不明白,毕竟PizzaStore的子类终究只是子类,如果才能做决定?在NYStylePizzaStore类中,并没有看到任何做决定逻辑的代码呀。

关于这个方面,要从PizzaStore的orderPizza()方法观点来看,此方法在抽象的PizzaStore内定义,但是只在子类中实现具体类型。所以此方法并不知道哪个子类将实际上只做披萨。

现在更进一步地,orderPizza()方法对Pizza对象做了许多事情(例如:准备、烘烤、切片、装盒),但由于Pizza对象是抽象的,orderPizza()并不知道哪些实际的具体类参与进来了。换句话说,这就是解耦(decouple)!

当orderPizza()调用createPizza()时,某个披萨店子类将负责创建披萨。做哪一种披萨呢?当然是由具体的披萨店来决定(例如:NYStylePizzaStore、ChicagoStylePizzaStore)。

那么,子类是实时做出这样的决定吗?不是,但从orderPizza()的角度来看,如果选择在NYStylePizzaStore订购披萨,就是由这个子类(NYStylePizzaStore)决定,严格来说,并非由这个子类实际做“决定”,而是由“顾客”决定到哪一家风味的披萨店才决定了披萨的风味。

让我们开一家披萨店吧!

开加盟店有它的好处,可以从PizzaStore免费取得所有的功能。区域点只需要继承PizzaStore,然后提供createPizza()方法实现自己的披萨风味即可。这里将为加盟店处理三个比较重要的披萨风味。

这是纽约风味,一旦将这个NYPizzaStore类编译成功,不妨尝试订购一两个披萨:

package com.zjwave.pattern.factory;


/**
 * createPizza()返回一个Pizza对象,
 * 由子类全权负责该实例化哪一个具体Pizza。
 *
 * NYPizzaStore扩展PizzaStore,所以拥有orderPizza()
 * 方法(以及其他的方法)
 *
 */
public class NYPizzaStore extends PizzaStore {

    /**
     * 我们必须实现createPizza()方法,因为在PizzaStore里它是抽象的。
     *
     * 请注意,超类的orderPizza()方法,并不知道正在创建的披萨是哪一种,
     * 它只知道这个披萨可以被准备、被烘烤、被切片、被装盒!
     */
    @Override
    protected Pizza createPizza(String item) {
        //这就是创建具体类的地方。对于每一种披萨类型都是创建纽约风味。
        if(item.equals("cheese")){
            return new NYStyleCheesePizza();
        }else if(item.equals("veggie")){
            return new NYStyleVeggiePizza();
        }else if(item.equals("clam")){
            return new NYStyleClamPizza();
        }else if(item.equals("pepperoni")){
            return new NYStylePepperoniPizza();
        }
        return null;
    }
}

这是芝加哥风味:

package com.zjwave.pattern.factory;


public class ChicagoPizzaStore extends PizzaStore {

    @Override
    protected Pizza createPizza(String item) {
        if(item.equals("cheese")){
            return new ChicagoStyleCheesePizza();
        }else if(item.equals("veggie")){
            return new ChicagoStyleVeggiePizza();
        }else if(item.equals("clam")){
            return new ChicagoStyleClamPizza();
        }else if(item.equals("pepperoni")){
            return new ChicagoStylePepperoniPizza();
        }
        return null;
    }
}

这是加州风味的披萨:

package com.zjwave.pattern.factory;


public class CAPizzaStore extends PizzaStore {
    
    @Override
    protected Pizza createPizza(String item) {
        if(item.equals("cheese")){
            return new CAStyleCheesePizza();
        }else if(item.equals("veggie")){
            return new CAStyleVeggiePizza();
        }else if(item.equals("clam")){
            return new CAStyleClamPizza();
        }else if(item.equals("pepperoni")){
            return new CAStylePepperoniPizza();
        }
        return null;
    }
}

声明一个工厂方法

原本是由一个对象负责所有具体类的实例化,现在通过对PizzaStore做一些小转变,变成由一群子类来负责实例化。让我们看的更仔细些:

package com.zjwave.pattern.factory;

public abstract class PizzaStore {

    /**
     * PizzaStore的子类在createPizza()方法中,处理对象的实例化。
     */
    public Pizza orderPizza(String type){
        Pizza pizza;

        pizza = createPizza(type);

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

        return pizza;
    }

    /**
     * 现在,实例化披萨的责任被移到一个”方法“中,此方法就如同是一个”工厂“
     */
    protected abstract Pizza createPizza(String item);

    //其他的方法
}

工厂方法用来处理对象的创建,并将这样的行为封装在子类中。这样,客户程序中关于这样的行为封装在子类中。这样,客户程序中关于超类的代码就和子类对象创建代码解耦了。

    /**
     * 1.工厂方法可能需要参数(也可能不需要)来指定所要的产品。
     * 2.工厂方法是抽象的,所以依赖子类来处理对象的创建。
     * 3.工厂方法必须返回一个产品。超类中定义的方法,通常使用到工厂方法的返回值。
     * 4.工厂方法将客户(也就是超类中的代码,例如orderPizza())和实际创建具体产品的代码分隔开来。
     */
    abstract Product factoryMethod(String type);

如何利用披萨工厂方法订购披萨

Ethan:我喜欢纽约风味的披萨……就是那种薄脆的饼,有着少许的芝士,以及很棒的酱料。

Joel:我喜欢芝加哥风味的深盘披萨,有着厚饼,和浓浓的芝士。

他们应该如何订购?

  1. 首先,Joel和Ethan需要取得披萨店的实例。Joel需要实例化一个ChicagoPizzaStore,而Ehan需要一个NYPizzaStore。
  2. 有了各自的PizzaStore,Joel和Ethan分别调用orderPizza()方法,并传入他们所喜爱的披萨类型(芝士、素食……)
  3. orderPizza()调用createPizza()创建披萨。其中NYPizzaStore实例化的是纽约风味的披萨,而ChicagoPizzaStore实例化的是芝加哥风味披萨。createPizza()会将创建好的披萨当做返回值。
  4. orderPizza()并不知道真正创建的是哪一种披萨,只知道这是一个披萨,能够被准备、被烘烤、被切片、被装盒,然后提供给Joel和Ethan。

看看如何根据订单生产这些披萨……

1.先看看Ehtan的订单:首先我们需要一个纽约披萨店:

//建立一个NYPizzaStore的实例
PizzaStore nyPizzaStore = new NYPizzaStore();

2.现在有了一个店,可以下订单了:

//调用nyPizzaStore实例的orderPizza()方法(这个方法被定义在PizzaStore中)。
nyPizzaStore.orderPizza("cheese");

3.orderPizza()方法于是调用createPizza()方法:

//别忘了,工厂方法createPizza()是在子类中实现的。
//在这个例子中,它会返回纽约芝士披萨。
Pizza pizza = createPizza("cheese");

4.最后,披萨必须经过下列的处理才算完成orderPizza():

//orderPizza()方法得到一个披萨
//但不知道它实例的具体类是什么
//但所制作出来的披萨一定定义了这些方法
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();

如果没有披萨可出售,我们的披萨店开的再多也不行。现在让我们来实现披萨:

package com.zjwave.pattern.factory;

import java.util.ArrayList;
import java.util.List;

public abstract class Pizza {

    //每个披萨都具有名称、面团类型、酱料类型、一套佐料。
    String name;
    String dough;
    String sauce;
    List<String> toppings = new ArrayList<>();

    //此抽象类提供了某些默认的基本做法,用来进行烘焙、切片、装盒。

    public void prepare(){
        //准备工作需要以特定的顺序进行,有一连串的步骤。
        System.out.println("Preparing " + name);
        System.out.println("Tossing dough...");
        System.out.println("Adding sauce...");
        System.out.println("Adding toppings...");
        for (String topping : toppings) {
            System.out.println("   " + topping);
        }
    }

    public void bake(){
        System.out.println("Bake for 25 minutes at 350");
    }

    public void cut(){
        System.out.println("Cutting the pizza into diagonal slices");
    }

    public void box(){
        System.out.println("Place pizza in official PizzaStore box");
    }

    public String getName() {
        return name;
    }
}

现在我们需要一些具体子类……来定义纽约和芝加哥风味的芝士披萨:

package com.zjwave.pattern.factory;



public class NYStyleCheesePizza extends Pizza {

    public NYStyleCheesePizza() {
        //纽约披萨有自己的大蒜番茄酱和薄饼
        name = "NY Style Sauce and Cheese Pizza";
        dough = "Thin Crust Dough";
        sauce = "Marinara Sauce";

        //上面覆盖的是意大利reggiano高级干酪
        toppings.add("Grated Reggiano Cheese");
    }
}
package com.zjwave.pattern.factory;

public class ChicagoStyleCheesePizza extends Pizza{

    public ChicagoStyleCheesePizza() {
        //芝加哥披萨使用小番茄作为酱料,并使用薄饼
        name = "Chicago Style Deep Dish Cheese Pizza";
        dough = "Extra Thick Crust Dough";
        sauce = "Plum Tomato Sauce";

        //芝加哥风味的深盘披萨使用许多mozzarella(意大利白干酪)
        toppings.add("Shredded Mozzarella Cheese");
    }

    @Override
    public void cut() {
        //这个芝加哥风味披萨覆盖了cut()方法,将披萨切成正方形
        System.out.println("Cutting the pizza into square slices");
    }
}

你已经等的够久了,来吃些披萨吧。

package com.zjwave.pattern.factory;

public class PizzaTestDrive {

    public static void main(String[] args) {
        //首先建立两个不同的店。
        NYPizzaStore nyStore = new NYPizzaStore();
        ChicagoPizzaStore chicagoStore = new ChicagoPizzaStore();

        //然后用一个店帮Ethan下订单
        Pizza pizza = nyStore.orderPizza("cheese");
        System.out.println("Ethan ordered a " + pizza.getName() + "\n");

        //这是Joel的订单
        pizza = chicagoStore.orderPizza("cheese");
        System.out.println("Joel ordered a " + pizza.getName() + "\n");
        //两个披萨都准备好了:佐料都加上了,烘烤完成了,切片装盒了。
        //超累从来不管细节。通过实例化正确的披萨类,子类会自行照料这一切。
    }
}

2. 认识工厂模式

所有工厂模式都用来封装对象的创建。工厂方法模式(Factory Method Pattern)通过让子类决定该创建的对象是什么,来达到将对象创建的过程封装的目的。让我们来看看这些类图,以了解有哪些组成元素:

创建者(Creator)类

产品类

2.1 另一个观点:平行的类层级

我们已经看到,讲一个orderPizza()方法和一个工厂方法联合起来,就可以成为一个框架。除此之外,工厂方法将生产知识封装进各个创建者,这样的做法,也可以被视为是一个框架。

让我们来看看这两个平行的类层级,并认清它们的关系:

2.2 定义工厂方法模式

下面是工厂方法模式的正式定义:

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

工厂方法模式能够封装具体类型的实例化。看看下面的类图,抽象的Creator提供了一个创建对象的方法的接口,也称为“工厂方法”。在抽象的Creator中,任何其他实现的方法,都可能使用到这个工厂方法所制造出来的产品,但只有子类真正实现这个工厂方法并创建产品。

如同在正式定义中所说的,常常听到其他开发人员说:工厂方法让子类决定要实例化的类是哪一个。希望不要理解错误,所谓的“决定”,并不是指模式允许子类本身在运行时做决定,而是指在编写创建者类时,不需要知道实际创建的产品是哪一个。选择使用哪个子类,自然就决定了实际创建的产品是什么。

问:当只有一个ConcreteCreator的时候,工厂方法模式有什么优点?

答:尽管只有一个具体创建者,工厂方法模式依然很有用,因为它帮助我们将产品的“实现”从“使用”中解耦。如果增加产品或者改变产品的实现,Creator并不会收到影响(因为Creator与任何ConcreteProduct之间都不是紧耦合)。

问:如果纽约和芝加哥的商店是利用简单工厂创建的,这样的说法是否正确?看起来倒是很想。

答:他们很类似,但用法不同。虽然每个具体商店的实现看起来都很想是SimplePizzaFactory,但是别忘了,这里的具体商店是扩展自一个类,此类有一个抽象的方法createPizza()。由每个商店自行负责createPizza()方法的行为。在简单工厂中,工厂是另一个由Pizzastore使用的对象。

问:工厂方法和创建者是否总是抽象的?

答:不,可以定义一个默认的工厂方法来产生某个具体的产品,这么一来,即使创建者没有任何子类,依然可以创建产品。

问:每个商店基于传入的类型制造出不同种类的披萨。是否所有的具体创建者都必须如此?能不能只创建一种披萨?

答:这里所采用的方式称为“参数化工厂方法”。它可以根据传入的参数创建不同的对象。然而,工厂经常只产生一种对象,不需要参数化。模式的这两种形式都是有效的。

问:利用字符串传入参数化的类型,似乎有点危险,万一把Clam(蛤蜊)英文拼错,成了Calm(平静),要求供应“CalmPizza”,怎么办?

答:说得很对,这样的情形会造成所谓的“运行时错误”。有几个其他更复杂的技巧可以避开这个麻烦,在编译时期就将参数上的错误跳出来。比方说,你可以创建代表参数类型的对象和使用静态常量或者Java所支持的enum。

问:对于简单工厂和工厂方法之间的差异,我依然感到困惑。他们看起来很类似,差别在于,在工厂方法中,返回披萨的类是子类。能解释一下吗?

答:子类的确看起来像简单工厂。简单工厂把全部的事情,在一个地方都处理完了,然而工厂方法却是创建一个框架,让子类决定要如何实现。比方说,在工厂方法中,orderPizza()方法提供了一般的框架,以便创建披萨,orderPizza()方法依赖工厂方法创建具体类,并制造出实际的披萨。可通过继承PizzaStore类,决定实际制造出的披萨是什么。简单工厂的做法,可以将对象的创建封装起来,但是简单工厂不具备工厂方法的弹性,因为简单工厂不能变更正在创建的产品。

大师与门徒:

大师:蚱蜢, 告诉我训练进行得如何了?

门徒:大师,我已经更进一步研究了“封装变化”。

大师:继续说……

门徒:我已经学习到,可以将创建对象的代码封装起来。实例化具体类的代码,很可能在以后经常需要变化。我学到一个称为“工厂”的技巧,可以封装实例化的行为。

大师:那么这些所谓的“工厂”究竟能带来什么好处?

门徒:有许多好处。将创建对象的代码集中在一个对象或方法中,可以避免代码中的重复,并且更方便以后的维护。这也意味着客户在实例化对象时,只会依赖于接口,而不是具体类。我在学习中发现,这可以帮助我针对接口编程,而不针对实现编程。这让代码更具有弹性,可以应对未来的扩展。

大师:很好,蚱蜢,你的OO直觉正在增强。今天对师父可有问题要问吗?

门徒:大师,我知道封装起创建对象的代码,就可以对抽象编码,将客户代码和真实的实现解耦。然而在我的工厂代码中,不可避免的,仍然必须使用具体类来实例化真正的对象。我这不是“蒙着眼睛骗自己”吗?

大师:蚱蜢呀!对象的创建是现实的,如果不创建任何对象,就无法创建任何Java程序。然而,利用这个现实的知识,可将这些创建对象的代码用栅栏围起来,就像你把素有的所有堆到眼前一样,一旦围起来后,就可以保护这些创建对象的代码。如果让创建对象的代码到处乱跑,那就无法收集到“羊毛”,你说是吧?

门徒:大师,我已经认识到真理。

大师:我知道你能够体会。现在请进一步调节对象的依赖。

2.3 一个很依赖的披萨店

假设你从未听说过OO工厂。下面是一个不使用工厂模式的披萨店版本。数一数,这个类所依赖的具体披萨对象有几种。如果又加了一种加州风味披萨到这个披萨店中,那么届时又会依赖几个对象?

package com.zjwave.pattern.factory;

public class DependentPizzaStore {


    public Pizza createPizza(String style, String type) {
        Pizza pizza = null;
        if (style.equals("NY")) {
            //处理所有纽约风味的披萨
            if (type.equals("cheese")) {
                pizza = new NYStyleCheesePizza();
            } else if (type.equals("veggie")) {
                pizza = new NYStyleVeggiePizza();
            } else if (type.equals("clam")) {
                pizza = new NYStyleClamPizza();
            } else if (type.equals("pepperoni")) {
                pizza = new NYStylePepperoniPizza();
            }
        } else if (style.equals("Chicago")) {
            //处理所有芝加哥风味的披萨
            if (type.equals("cheese")) {
                pizza = new ChicagoStyleCheesePizza();
            } else if (type.equals("veggie")) {
                pizza = new ChicagoStyleVeggiePizza();
            } else if (type.equals("clam")) {
                pizza = new ChicagoStyleClamPizza();
            } else if (type.equals("pepperoni")) {
                pizza = new ChicagoStylePepperoniPizza();
            }
        } else {
            System.out.println("Error: invalid type of pizza");
            return null;
        }
        pizza.prepare();
        pizza.bake();
        pizza.cut();
        pizza.box();
        return pizza;
    }

}

当你直接实例化一个对象时,就是在依赖它的具体类。请看看这个依赖性很高的披萨店例子,它由披萨店类来创建所有的披萨对象,而不是委托给工厂。

如果把这个版本的披萨店和它依赖的对象画城一张图,看起来是这样的:

2.4 依赖倒置原则

很清楚地,代码里减少对于具体类的依赖是件“好事”。事实上,有一个OO设计原则就正式阐明了这一点,这个原则甚至还有一个又响亮又正式的名称:“依赖倒置原则”(Dependency Inversion Principle):

设计原则:要依赖抽象,不要依赖具体类。

首先,这个原则听起来很像是“针对接口编程,不针对实现编程”,不是吗?的确很相似,然而这里更强调“抽象”。这个原则说明了:不能让高层组件依赖低层组件,而且,不管高层或低层组件,“两者”都应该依赖于抽象。(所谓“高层”组件,是由其他低层组件定义其行为的类。例如,PizzaStore是个高层组件,因为它的行为是由披萨定义的:PizzaStore创建所有不同的披萨对象,准备、烘焙、切片、装盒;而披萨本身属于低层组件。)

这到底是什么意思?

这个嘛,让我们再次看看披萨店的图。PizzaStore是“高层组件”,而披萨实现是“低层组件”,很清楚地,PizzaStore依赖这些具体披萨类。

现在,这个原则告诉我们,应该重写代码以便于我们依赖抽象类,而不依赖具体类。对于高层及低层模块都应该如此。

但是怎么做呢?我们来想想看怎样在“非常依赖披萨店”实现中,应用这个原则。

2.5 原则的应用

非常依赖披萨店的主要问题在于:它依赖每个披萨类型。因为它是在自己的orderPizza()方法中,实例化这些具体类型的。

虽然外面已经创建了一个抽象,也就是Pizza,但我们仍然在代码中,实际地创建了具体的Pizza,所以,这个抽象没什么影响力。

如何在orderPizza()方法中,将这些实例化对象的代码独立出来?我们都知道,工厂方法刚好能排上用场。

所以,应用工厂方法之后,类图看起来就像这样:

在应用工厂方法之后,你将注意到,高层组件(也就是PizzaStore)和低层组件(也就是这些披萨)都依赖了Pizza抽象。想要遵循依赖倒置原则,工厂方法并非是唯一的技巧,但却是最有威力的技巧之一。

2.6 依赖倒置原则,究竟倒置在哪里?

在依赖倒置中的倒置指的是和一般OO设计的思考方式完全相反。看看前面的图,你会注意到底层组件现在竟然依赖高层的抽象。同样地,高层组件现在也依赖相同的抽象。前面所绘制的依赖图是由上而下的,现在却倒置了,而且高层与低层模块现在都依赖这个抽象。

让我们好好地回顾一个设计过程来看看,究竟使用了这个原则之后,对设计的思考方式会被怎样的倒置……

倒置你的思考方式

好的,所以你需要实现一个披萨店,你第一件想到的事情是什么?

嗯!披萨店进行准备、烘烤、切片、装盒,所以我的店必须能制造许多不同风味的披萨,例如:芝士披萨,素食披萨,蛤蜊披萨……

没错!先从顶端开始,然后往下到具体的类。但是,正如你所看到的你不想让披萨店理会这些具体类,要不然披萨店将全都依赖这些具体类。现在,“倒置”你的想法……别从顶端开始,而是从披萨(Pizza)开始,然后想想看能抽象化些什么。

是的,芝士披萨,素食披萨和蛤蜊披萨都是披萨,所以它们应该共享一个Pizza接口。

对了,你想要抽象化一个Pizza。好,现在回头重新思考如何设计披萨店。

既然我已经有一个披萨抽象,就可以开始设计披萨店,而不用例会具体的披萨类了。

很接近了,但是要这么做,必须靠一个工厂来将这些具体类取出披萨店。一旦你这么做了,各种不同的具体披萨类型就只能依赖一个抽象,而披萨店也会依赖这个抽象。我们已经倒置了一个商店依赖具体类的设计,而且也倒置了你的思考方式。

几个指导方针帮助你遵循此原则……

下面的指导方针能帮你避免在OO设计中违反依赖倒置原则:

  • 变量不可以持有具体类的引用。(如果使用new,就会持有具体类的引用。你可以改用工厂来避开这样的做法。)
  • 不要让类派生自具体类。(如果派生自具体类,你就会依赖具体类。请派生自一个接口或抽象类。)
  • 不要覆盖基类中已实现的方法。(如果覆盖基类已实现的方法,那么你的基类就不是一个真正适合被继承的抽象。基类中已经实现的方法,应该由所有子类共享。)

萌新:但是,等等,要完全遵守这些指导方针似乎不太可能吧?如果遵守这些方针,我连一个简单程序都写不出来!

你说的没错!正如同我们的许多原则一样,应该尽量达到这个原则,而不是随时都要遵循这个原则。我们都很清楚,任何Java程序都有违反这些指导方针的地方!

但是,如果你深入体验这些方针,将这些方针内化成你思考的一部分,那么在设计是,你将指导何时有足够的理由违反这样的原则。比方说,如果有一个不像是会改变的类,那么在代码中直接实例化具体类也就没什么大碍。想想看,我们平常还不是在程序中不假思索地就实例化字符串对象吗?就没有违反这个原则?当然有!可以这么做吗、可以!为什么?因为字符串不可能改变。

另一方面,如果有个类可能改变,你可以采用一些好技巧(例如工厂方法)来封装改变。

3. 抽象工厂模式

披萨店的设计变得很棒:具有弹性的框架,而且遵循设计原则。

现在,对象村披萨店成功的关键在于新鲜、高质量的原料,而且通过导入新的框架,加盟店将遵循你的流程,但是有一些加盟店,使用低价原料来增加利润。你必须采取一些手段,以免长此以往回了对象村的品牌。

3.1 确保原料的一致

要如何确保每家加盟店使用高质量的原料?你打算建造一家生产原料的工厂,并将原料运送到各家加盟店。对于这个做法,现在还剩下了一个问题:加盟店坐落在不同的区域,纽约的红酱料和芝加哥的红酱料是不一样的。所以对于纽约和芝加哥,你准备了两组不同的原料。让我们看的更仔细些:

3.2 原料家族

纽约使用一组原料,而芝加哥使用另一组原料。对象村披萨是如此受欢迎,可能不久之后加州就有加盟店了,到时候有需要运送另一组区域的原料。接着呢?西雅图吗?

想要行得通,必须先清楚如何处理原料家族。

3.3 建造原料工厂

现在,我们要建造一个工厂来生产原料,这个工厂将负责创建原料家族中的每一种原料。也就是说,工厂将需要生产面团、酱料、芝士等。待会儿,你就会知道如何处理各个区域的差异了。

开始先为工厂定义一个接口,这个接口负责创建所有的原料:

/**
 * 在接口中,每个原料都有一个对应的方法创建该原料。
 * 如果每个工厂实例内都有某一种通用的“机制”需要实现,就可以把这个例子改写成抽象类……
 */
public interface PizzaIngredientFactory {

    /**
     * 这里有许多新类,每个原料都是一个类
     */
    Dough createDough();

    Suace createSauce();

    Cheese createCheese();

    Veggies[] createVeggies();

    Pepperoni createPepperoni();
    
    Clams createClam();
    
}

要做的事情是:

  1. 为每个区域建造一个工厂。你需要创建一个继承自PizzaIngredientFactory的子类来实现每一个创建方法。
  2. 实现一组原料类供工厂使用,例如ReggianoCheese、RedPeppers、ThickCrustDough。这些类可以在合适的区域间共享。
  3. 然后你仍然需要将这一些组织起来,将新的原料工厂整合进旧的PizzaStore代码中。

创建纽约原料工厂

好了,这是纽约原料工厂的实现。这工厂专精于大蒜番茄酱料、Reggiano干酪、新鲜蛤蜊……

/**
 * 对于原料家族内的每一种原料,我们都提供了纽约的版本。
 */
public class NYPizzaIngredientFactory implements PizzaIngredientFactory {


    @Override
    public Dough createDough() {
        return new ThinCrustDought();
    }

    @Override
    public Suace createSauce() {
        return new MarinaraSauce();
    }

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

    /**
     * 对于蔬菜,以一个蔬菜数组为返回值。
     * 在这里我们是直接把蔬菜写死。其实我们可以把它改写得更好一点,
     * 但这对于学习工厂模式并没有帮助,所以还是保持这个简单的做法就好了。
     */
    @Override
    public Veggies[] createVeggies() {
        return new Veggies[]{new Garlic(),new Onion(),new Mushroom(),new RedPepper()};
    }

    /**
     * 这是切片的意式香肠,纽约和芝加哥都会用到他。
     */
    @Override
    public Pepperoni createPepperoni() {
        return new SlicedPepperoni();
    }

    /**
     * 纽约靠海,所以有新鲜的蛤蜊。芝加哥就必须使用冷冻的蛤蜊。
     * @return
     */
    @Override
    public Clams createClam() {
        return new FreshClams();
    }
}

创建芝加哥原料工厂

public class ChicagoPizzaIngredientFactory implements PizzaIngredientFactory {


    @Override
    public Dough createDough() {
        return new ThickCrustDough();
    }

    @Override
    public Suace createSauce() {
        return new PlumTomatoSauce();
    }

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

    @Override
    public Veggies[] createVeggies() {
        return new Veggies[]{new Spinach(),new EggPlant(),new BlackOlives()};
    }

    @Override
    public Pepperoni createPepperoni() {
        return new SlicedPepperoni();
    }

    @Override
    public Clams createClam() {
        return new FrozenClams();
    }
}

3.4 重做披萨

工厂已经一切就绪,准备生产高质量原料了,现在我们只需要重做披萨,好让它们只是用工厂生产出来的原料。我们先从抽象的Pizza类开始:

public abstract class Pizza {


    String name;
    Dough dough;
    Sauce sauce;

    //每个披萨都持有一组在准备时会用到的原料
    Veggies veggies[];
    Cheese cheese;
    Pepperoni pepperoni;
    Clams clams;


    //现在把prepare()方法声明成抽象。
    //在这个方法中,我们需要收集披萨所需的原料,
    //而这些原料当然是来自原料工厂了。
    abstract void prepare();

    public void bake() {
        System.out.println("Bake for 25 minutes at 350");
    }

    public void cut() {
        System.out.println("Cutting the pizza into diagonal slices");
    }

    public void box() {
        System.out.println("Place pizza in official PizzaStore box");
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        //这里是打印披萨的代码
        return "Pizza{" +
                "name='" + name + '\'' +
                '}';
    }
}

现在已经有了一个抽象披萨,可以开始创建纽约和芝加哥风味的披萨了。从今以后,加盟店必须直接从工厂取得原料,那些偷工减料的日子宣告结束了!

我们曾经写过工厂方法的代码,有NYCheesePizza和ChicagoCheesePizza类。比较一下这两个类,唯一的差别在于使用区域性的原料,至于披萨的做法都一样(面团+酱料+芝士),其他的披萨(蔬菜、蛤蜊等)也是如此。它们都依循着相同的准备步骤,只是使用不同的原料。

所以,其实我们不需要设计两个不同的类来处理不同风味的披萨,让原料工厂处理这种区域差异就可以了。下面是CheesePizza:

public class CheesePizza extends Pizza {

    PizzaIngredientFactory ingredientFactory;

    /**
     * 要制作披萨,需要工厂提供原料。
     * 所以每个皮萨雷都需要从构造器参数中得到一个工厂,
     * 并把这个工厂存储在一个实例变量中。
     * @param ingredientFactory
     */
    public CheesePizza(PizzaIngredientFactory ingredientFactory) {
        this.ingredientFactory = ingredientFactory;
    }

    @Override
    void prepare() {
        System.out.println("Preparing " + name);
        dough = ingredientFactory.createDough();
        sauce = ingredientFactory.createSauce();
        cheese = ingredientFactory.createCheese();
        //prepare()方法一步一步地创建芝士披萨,每当需要原料时,就跟工厂要。
    }
}

Pizza的代码利用相关的工厂生产原料。所生产的原料依赖所使用的工厂,Pizza类根本不关心这些原料,它只知道如何只做披萨。现在,Pizza和区域原料之间被解耦,无论原料工厂是在落基山脉还是西北沿岸地区,Pizza类都可以轻易地复用,完全没有问题。

//1.把Pizza的实例变量设置为此披萨所使用的某种酱料。
//2.Pizza不在乎使用什么工厂,只要是原料工厂就行了。
//3.createSauce()方法会返回这个区域所使用的酱料。
//如果这是一个纽约原料工厂,我们将取得大蒜番茄酱料。
sauce = ingredientFactory.createSauce();

也来看看蛤蜊披萨:

public class ClamPizza extends Pizza {

    PizzaIngredientFactory ingredientFactory;

    /**
     * 蛤蜊披萨也需要原料工厂
     * @param ingredientFactory
     */
    public ClamPizza(PizzaIngredientFactory ingredientFactory) {
        this.ingredientFactory = ingredientFactory;
    }

    @Override
    void prepare() {
        System.out.println("Preparing " + name);
        dough = ingredientFactory.createDough();
        sauce = ingredientFactory.createSauce();
        cheese = ingredientFactory.createCheese();
        //要做出蛤蜊披萨,prepare()方法就必须从本地工厂中取得正确的原料。
        //如果是纽约工厂,就会使用新鲜的蛤蜊;如果是芝加哥工厂,就是冷冻的蛤蜊。
    }
}

我们几乎完工了,只需再到加盟店短暂巡视一下,确认他们使用了正确的披萨。也需要他们能和本地的原料工厂搭上线:

public class NYPizzaStore extends PizzaStore {

    @Override
    protected Pizza createPizza(String item) {
        Pizza pizza = null;
        //纽约店会用到纽约披萨原料工厂,
        //由该原料工厂负责生产所有纽约风味披萨所需的原料。
        PizzaIngredientFactory ingredientFactory = new NYPizzaIngredientFactory();
        if(item.equals("cheese")){
            //把工厂传递给每一个披萨,以便披萨能从工厂中取得原料。
            pizza = new NYStyleCheesePizza(ingredientFactory);
            pizza.setName("New York Style Cheese Pizza");
        }else if(item.equals("veggie")){
            pizza =  new NYStyleVeggiePizza(ingredientFactory);
            pizza.setName("New York Style Veggie Pizza");
        }else if(item.equals("clam")){
            pizza =  new NYStyleClamPizza(ingredientFactory);
            pizza.setName("New York Style Clam Pizza");
        }else if(item.equals("pepperoni")){
            pizza =  new NYStylePepperoniPizza(ingredientFactory);
            pizza.setName("New York Style Pepperoni Pizza");
        }
        //对于每一种披萨,我们实例化一个新的披萨,并传进该种披萨所需的工厂
        //以便披萨取得它的原料
        return pizza;
    }
    
}

我们做了些什么?

一连串的代码改变,我们到底做了些什么?

我们引入新类型的工厂,也就是所谓的抽象工厂,来创建披萨原料家族。

通过抽象工厂所提供的接口,可以创建产品的家族,利用这个接口书写代码,我们的代码将从实际工厂解耦,以便在不同上下文中实现各式各样的工厂,制造出各种不同的产品。例如:不同的区域、不同的操作系统、不同的外观及操作。

因为代码从实际的产品中解耦了,所以我们可以替换不同的工厂来取得不同的行为(例如:取得大蒜番茄酱料,而不是取得番茄酱料)。

3.5 订购更多披萨

Ethan和Joel对于对象村的披萨欲罢不能!其实他们不知道,现在所订购的披萨是利用新原料工厂的原料制作出来的。因此当他们订购披萨时:

Ethan:我还是喜欢纽约风味。

Joel:我依然选择芝加哥。

一开始订购的流程依然完全不变,让我们再来看看Ethan的订单:

1.首先我们需要一个纽约披萨店:

//创建一个NYPizzaStore的实例
PizzaStore nyPizzaStore = new NYPizzaStore();

2.现在已经有一个披萨店,可以接受订单:

//调用nyPizzaStore实例的orderPizza()方法
nyPizzaStore.orderPizza("cheese");

3.orderPizza()方法首先调用createPizza()方法:

Pizza pizza = createPizza("cheese");

接下来,就不一样了,因为我们现在使用了原料工厂

4.当createPizza()方法被调用时,也就开始涉及原料工厂了:

//选择原料工厂,接着在PizzaStore中实例化,
//然后将它传进每个披萨的构造器中。
Pizza pizza = new CheesePizza(nyIngredientFactory);

5.接下来需要准备披萨。一旦调用了prepare()方法,工厂将被要求准备原料:

//对Ethan的披萨来说,使用了纽约原料工厂,取得了纽约的原料。
void prepare() {
    //薄饼
    dough = ingredientFactory.createDough();
    //大蒜番茄酱料
    sauce = ingredientFactory.createSauce();
    //Reggiano干酪
    cheese = ingredientFactory.createCheese();
}

6.最后,我们得到了准备好的披萨,orderPizza()就会接着烘烤、切片、装盒。

3.6 定义抽象工厂模式

我们又在模式家族中新增了另一个工厂模式,这个模式可以创建产品的家族。看看这合格模式的正式定义:

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

抽象工厂允许客户使用抽象的接口来创建一组相关的产品,而不需要知道(或关心)实际产出的具体产品是什么。这样一来,客户就从具体的产品中被解耦。让我们看看类图来了解其中的关系:

这是一张相当复杂的类图;让我们从PizzaStore的观点来看一看它:

萌新:我注意到了,抽象工厂的每个方法实际上看起来都像是工厂方法(例如:createDough()、createSause()等)。每个方法都被声明成抽象,而子类的方法覆盖这些方法来创建某些对象。这不正是工厂方法吗?

工厂方法是不是潜伏在抽象工厂里面?

是的,抽象工厂的方法经常以工厂方法的方式实现,这很有道理,对吧?抽象工厂的任务是定义一个负责创建一组产品的接口。这个接口内的每个方法都负责创建一个具体产品,同时我们利用实现抽象工厂的子类来提供这些具体的做法。所以,在抽象工厂中利用工厂方法实现生产方法是相当自然的做法。

模式访谈

记者:哇!今天很难得,同时请到了两种模式。这可是头一回啊!

工厂方法:呵!我其实不希望人们把我和抽象工厂混为一谈。虽然我们都是工厂模式,但并不表示我们就应该被合在一起访问。

记者:别生气,我们之所以想要同时采访你们就是为了帮读者搞清楚你们之间谁是谁。你们的确有相似的地方,听说人们常常会把你们搞混了。

抽象工厂:这是真的,有些时候我被错认为是工厂方法。我知道工厂方法也有相同的困扰。我们两个在把应用程序从特定实现中解耦方面真的很有一套,只是做法不同而已。我能够理解为什么人们总是把我们搞混。

工厂方法:哎呀!这还是让我很不爽。毕竟,我使用的是类而你使用的是对象,根本就不是一回事啊。

记着;工厂方法,能请你多做一些解释吗?

工厂方法:当然。抽象工厂与我都是负责创建对象,这是我们的工作。但是我用的方法是继承……

抽象工厂:……而我是通过对象的组合。

工厂方法:对!所以这意味着,利用工厂方法创建对象,需要一个扩展类,并覆盖它的工厂方法。

记者:那这个工厂方法是做什么的呢?

工厂方法:当然是用来创建对象的了。其实整个工厂方法模式,只不过就是通过子类来创建对象。用这种做法,客户只需要知道他们所使用的抽象类型就可以了,而由子类来负责决定具体类型。所以,换句话说,我只负责将客户从具体类型中解耦。

抽象工厂:这一点我也做得到,只是我的做法不同。

记者:抽象工厂,请继续……你刚刚说了一些关于对象组合的事?

抽象工厂:我提供一个用来创建一个产品家族的抽象类型,这个类型的子类定义了产品被产生的方法。要想使用这个工厂,必须先实例化它,然然后将它传入一些针对抽象类型所写的代码中。所以,和工厂方法一样,我可以把客户从所使用的实际具体产品中解耦。

记者:噢!我了解了,所以你的另一个优点是可以把一群相关的产品集合起来。

抽象工厂:对。

记者:万一需要扩展这组相关产品(比方说新增一个产品),又该怎么办呢?难道这不需要改变接口吗?

抽象工厂:那倒是真的,如果加入新产品那就必须改变接口,我知道大家不喜欢这么做……

工厂方法:<窃笑>

抽象工厂:我说,工厂方法,你偷笑什么?

工厂方法:拜托,那可是很严重的!改变接口就意味着必须深入改变每个子类的接口!听起来可是很繁重的工作呀。

抽象工厂:是的,但是我需要一个大的接口,因为我可是被用来创建整个产品家族的。你只不过是创建一个产品,所以你根本不需要一个大的接口,你只需要一个方法就可以了。

记者:抽象工厂,我听说你经常使用工厂方法来实现你的具体工厂。

抽象工厂:是的,我承认这一点,我的具体工厂经常实现工厂方法来创建他们的产品。不过对我来说,这些具体工厂纯粹只是用来创建产品罢了……

工厂方法:……而对我来说,抽象创建者(creator)中所实现的代码通常会用到子类所创建的具体类型。

记者:听起来你们都有自己的一套。我相信人们喜欢有选择的余地,毕竟,工厂这么有用,大家希望在各种不同的情况下都可使用工厂。你们俩都能将对象的创建封装起来,使应用程序解耦,并降低其对特定实现的依赖。真的是很棒。不管是使用工厂方法还是抽象工厂,都可以给人们带来好处。访谈结束前,请两位各说几句话吧。

抽象工厂:谢谢。我是抽象工厂,当你需要创建产品家族和想让制造的相关产品集合起来时,你可以使用我。

工厂方法;而我是工厂方法,我可以把你的客户代码从需要实例化的具体类中解耦。或者如果你目前还不知道将来需要实例化哪些具体类时,也可以用我。我的使用方式很简单,只要把我继承成子类,并实现我的工厂方法就可以了。

3.7 比较工厂方法和抽象工厂

4.总结

在本文,我们多加了两个工具到你的工具箱中:工厂方法和抽象工厂。这两种模式都是将对象创建的过程封装起来,以便将代码从具体类解耦。

OO原则:

  • 多用组合,少用继承。
  • 针对接口编程,不针对实现编程。
  • 为交互对象之间的松耦合设计而努力。
  • 类应该对扩展开放,对修改关闭。
  • 依赖抽象,不要依赖具体类。

我们有了一个新原则,指导我们尽可能地让事情保持抽象。

OO模式:

  • 抽象工厂模式——提供一个接口,用于创建相关或依赖对象的家族,而不需要明确指定具体类。
  • 工厂方法模式——定义了一个创建对象的接口,但由子类决定要实例化的类是哪一个。工厂方法让类把实例化推迟到子类。

这些新的模式可以将对象的创建封装起来,以便于得到更松耦合,更有弹性的设计。

本文要点:

  • 所有的工厂都是用来封装对象的创建。
  • 简单工厂,虽然不是真正的设计模式,但仍不失为一个简单的方法,可以将客户程序从具体类解耦。
  • 工厂方法使用继承:把对象的创建委托给子类,子类实现工厂方法来创建对象。
  • 抽象工厂使用对象组合:对象的创建被实现在工厂接口所暴露出来的方法中。
  • 所有工厂模式都通过减少应用程序和具体类之间的依赖促进松耦合。
  • 工厂方法允许类将实例化延迟到子类进行。
  • 抽象工厂创建相关的对象家族,而不需要依赖他们的具体类。
  • 依赖倒置原则,指导我们避免依赖具体类型,而要尽量依赖抽象。
  • 工厂是很有威力的技巧,帮助我们针对抽象编程,而不要针对具体类编程。

转载请注明原文链接:ZJ-Wave

Top