4.工厂模式

1.绪

1.1引入

本章将准备好烘烤某些松耦合的OO设计。

除了使用new操作符之外,还有更多制造对象的方法。将了解到实例化这个活动不应该总是公开的进行,也会认识到初始化经常造成“耦合”问题。本章的工厂模式将带你了解如何从复杂的依赖中帮你脱困。

1.2 关于制造对象

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

当使用“new”时,就的确是在实例化一个具体类,因此用的确实是实现,而不是接口。因为代码绑着具体类,会导致代码更脆弱,更缺乏弹性。

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

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

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

但是,总要创建对象,而Java只提供一个new关键词 创建对象,除此之外,还能有什么?

“new”有什么不对劲?

在技术上,new没有错,在针对接口编程中,可以隔离掉以后系统可能发生的一大堆改变。如果代码是针对接口而写,那么通过多态,它可以与任何新类实现该接口。但是,当代码使用大量的具体类时,等于自找麻烦,因为一旦加入新的具体类,就必须改变代码。(改变就是参考上面的if ...else if...判断链)也就是说,你的代码并非“对修改关闭”。想用新的具体类型来扩展代码,则必须重新打开它。

而有个设计原则是“对扩展开放,对修改关闭”。

问题:如何将实例化具体类的代码从应用中抽离,或者封装起来,使它们不会干扰应用的其他部分?

1.3 识别变化的方面

假设有一个比萨店,则对于比萨的代码可能这么写:

 为了让系统有弹性,我们很希望这是一个抽象类或接口。但如果这样,这些类或接口就无法直接实例化

当需要更多的比萨类型时,则必须增加一些代码,来“决定”适合的比萨类型,然后在“制造”这个比萨:

即便如此,但是还有增加更多的比萨类型

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

  如果实例化“某些”具体类,将使orderPizza()出现问题,而且也无法让orderPizza()对修改关闭。

<注:设计原则是 对扩展打开,对修改关闭。就像 装饰者模式,只需要用装饰类去装饰组件,而不需要修改组件,就能实现相应的功能,这就符合设计原则,而在此处,如果采用上面的代码方法,当出现问题时,只能去修改代码,也就是:无法对修改关闭>

1.4 封装创建对象的代码

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

  

 将创建对象的代码从orderPizza()方法中抽离,然后把这个部分的代码搬到另一个对象中,这个新对象只管如何创建比萨。如果任何对象想要创建比萨,则直接找他就可以了

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

工厂(factory)处理创建对象的细节。一旦有了SimplePizzaFactory,orderPizza()就变成此对象的客户。当需要比萨时,就叫比萨工厂做一个。那些orderPizza()方法只关心从工厂得到了一个比萨,而这个比萨实现了Pizza接口,所以它可以调用prepare()、bake()、cut()、box()来分别进行准备、烘烤、切片和装盒。《注:此处就是,因为具体的比萨都实现了Pizza接口,那么对比萨进行prepare()等操作的时候,会根据实际的类型来进行相应的操作》

1.5建立一个简单的比萨工厂

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

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

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

因此,将创建比萨的代码包装进一个类,当以后实现改变时,只需要修改这个类即可

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

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

1.6 重做PizzaStore类

   来通过工厂来创建比萨orderPizza()是负责处理订单,即客户点了之后,比萨店根据制作流程对工厂生产的比萨进行处理之后,最后给顾客的比萨则最终的代码是:

 我们知道对象组合可以在运行时动态改变行为,因为我们可以更换不同的实现

2.定义简单工厂

2.1 简单工厂

简单工厂不是一个设计模式,反而比较像是一种编程习惯。

来看看新的比萨店类图:

 一定要去理解,简单工厂这样写法,该如何具体来用。因为具体的比萨都实现了Pizza接口,那么对比萨进行prepare()等操作的时候,会根据实际的类型来进行相应的操作。

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

 2.2 加盟比萨店产生的问题

场景需求:为了提高竞争力,则每家加盟店都想要提供不同风味的比萨(比如说纽约、芝加哥和加州)。

 如果利用SimplePizzaFactory,写出三种不同的工厂,则分别是NYPizzaFactory、ChicagoPizzaFactory和CaliforniaPizzaFactory,那么各个加盟店都有合适的工厂可以使用,这是一种做法,但根据这么做的结果是:

在推广SimplePizzaFactory时,则发现加盟店的确是采用的指定工厂创建比萨,但是其他部分,却开始采用他们自创的流程,比如烘烤的做法存在差异、不要切片或者使用其他厂商的盒子。这会导致对质量控制不会太多。 

针对这个问题,则就会希望建立一个框架,把加盟店和创建比萨捆绑在一起的同时,又保持一定的弹性

2.3 给比萨店使用的框架

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

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

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

现在已经有一个PizzaStore作为超类,让每个域类型(NYPizzaSotre、ChicagoPizzaStore、CaliforniaPizzaStore)都继承这个PizzaStore,每个子类各自决定如何制造比萨。

2.4 运行子类做决定

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

各个区域比萨店之间的差异在于他们制作比萨的风味(纽约比萨的饼薄,芝加哥比萨的饼厚等),我们现在要让createPizza()能够应对这些变化来负责创建正确种类的比萨。

做法:让PizzaSotre的各个子类负责定义自己的createPizza()方法,然后进行统一的准备,烘烤。切片和包装,则这样就能确保比萨风味的独特性和其他的统一。

因此,我们会得到一些PizzaSotre具体的子类,每个子类都有自己的比萨辩题,并且仍然是和PizzaStore框架,并使用调试好的orderPizza()方法。

 每个子类都会覆盖createPizza()方法,同时使用PizzaStore定义的orderPizza()方法,甚至可以把orderPizza()方法声明称final,以防止被子类覆盖。

2.5 子类如何做决定

关于这个问题,要从PizzaSotre的orderPizza()方法观点来看,此方法在抽象的PizzaStore内定义,但是只在子类中实现具体的类型。

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

 当orderPizza()调用createPizza()时,某个比萨店子类将负责创建比萨。所做的比萨类型由具体的比萨店来决定(例如:NYStylePizzaStore,ChicagoStylePizzaStore)。并且从orderPizza()的角度来看,如果选择在NYStylePizzaStore订购比萨,就是由这个子类(NYStylePizzaStore)决定。严格来说,并非由这个子类实际做“决定”,而是由“顾客”决定到哪一家风味的比萨店才决定了比萨的风味

2.6 来开一家比萨店

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

这是纽约风味:

 一旦将这个NYPizzaStore类编译成功,就可以成功订购这个比萨。同理,将芝加哥风味以及加州风味的比萨店建造完成。

2.7 声明一个工厂方法

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

 2.8 如何利用比萨工厂方法来订购比萨

问题引入:Joel需要从芝加哥比萨店订购比萨,而Ethan需要从纽约比萨店订购比萨。

如何订购:

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

 2.9 如何根据订单来生产比萨

2.10 创建Pizza类

现在来实现Pizza本身:

记得雷神的一句话,线程操作对象!因此,对象本身应该具备被线程所操作的所有方法,因此prepare(),bake(),cut(),box()等方法应该就放在Pizza类里面

public abstract class Pizza {

    //每个比萨都具有名称,面团类型,酱料类型和一套作料
    String name;
    String dough;
    String sauce;
    ArrayList toppings=new ArrayList();

    void prepare(){
        System.out.println("Preparing "+name);
        System.out.println("Tossing dough...");
        System.out.println("Adding sauce...");
        System.out.println("Adding toppings:");
        for (int i=0;i<toppings.size();i++){
            System.out.print(" "+toppings.get(i));
        }
    }
    
    void bake(){
        System.out.println("Bake for 25 minutes at 350");
    }
    
    void cut(){
        System.out.println("Cutting the pizza into diagonal slices");
    }
    
    void box(){
        System.out.println("Place pizza in official PizzaStore box");
    }
    
    public String getName(){
        return name;
    }

}

然后去定义一些具体的子类,定义纽约和芝加哥风味的芝士比萨。

public class NYStyleCheesePizza extends Pizza {

    public NYStyleCheesePizza() {
        name="NY Style Sauce and Cheese Pizza";
        dough="Thin Crust Dough";
        sauce="Marinara Sauce";

        toppings.add("Grated Reggiano Cheese");
    }
}


public class ChicagoStyleCheesePizza extends Pizza {

    public ChicagoStyleCheesePizza() {
        name="Chicago Style Deep Dish Cheese Pizza";
        dough="Extra Thick Crust Dough";
        sauce="Plum Tomato Sauce";

        toppings.add("Shredded Mozzarella Cheese");
    }

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

2.11 创建Pizza工厂类

先创建抽象的Pizza商店,然后在创建加盟商店用于生产并销售相应的Pizza

public abstract class PizzaStore {

    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 type);

}


public class ChicagoStore extends PizzaStore {
    @Override
    protected Pizza createPizza(String type) {
        if(type.equals("cheese")){
            return new ChicagoStyleCheesePizza();
        }else{
            return null;
        }
    }
}

public class NYPizzaStore extends PizzaStore{
    @Override
    protected Pizza createPizza(String type) {
        if(type.equals("cheese")){
            return new NYStyleCheesePizza();
        }else{
            return null;
        }
    }
}

做一些比萨来进行测试:

public class PizzaTestDrive {

    public static void main(String[] args) {

        PizzaStore nyStore=new NYPizzaStore();
        PizzaStore chicagoStore=new ChicagoStore();

        Pizza pizza=nyStore.orderPizza("cheese");
        System.out.println("Ethan ordered a "+pizza.getName());

        pizza=chicagoStore.orderPizza("cheese");
        System.out.println("Joel ordered a "+pizza.getName());
    }
}

测试代码中的 NYPizzaStore与ChicagoStore就是生产Pizza的工厂,由客户确定要生产什么样的比萨,然后交给工厂生产

最终输出结果为:

3.认识工厂方法模式

所有工厂模式都用来封装对象的创建。工厂方法模式(Factory Method Pattern)通过让子类决定该创建的对象是什么,来达到将对象创建的过程封装的目的。

3.1 创造者类和产品类

这些类图的组成元素为:

1.创造者(Creator)类

PizzaStore:是抽象创造者类。这定义了一个抽象的工厂方法,让子类实现此方法制造产品。

                创造者通常会包含依赖于抽象产品的代码,而这些抽象产品由子类制造,创造者不需要

                真的知道在制造哪种具体产品。

NYPizzaStore/ChicagoPizzaStore:其中的createPizza()方法是工厂方法,用来制造产品。因为每

                个加盟店都有自己的PizzaStore子类,所以可以利用实现createPizza()创建自己的风味

                的比萨。

2.产品类

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

 从上可知,将一个orderPizza()方法和一个工厂方法联合起来,就可以成为一个框架。除此之外,工厂方法将生产知识封装进各个创建者,这样也可以被视为一个框架

来看看这两个平行的类层级:

这两个类层级为什么是平行的,是因为他们都有抽象类,而抽象类都有许多具体的子类,每个子类都有自己特定的实现。

工厂方法就是封装这种知识的关键所在。

4.定义工厂方法模式

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

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

工厂方法让子类决定要实例化的类是哪一个。所谓的“决定”,指在编写创建者类时,不需要知道实际创建的产品是哪一个。选择了使用哪个子类,自然就决定实际创建的产品是什么

将这个抽象的UML图对应到上面具体的代码中去,就是:

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

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

通过上面,已经学到了“封装变化”。“封装变化”就是将创建对象的代码封装起来,因为实例化具体类的代码很可能在以后经常需要变化,可以通过“工厂”的技巧,进行封装实例化的行为。

所谓的“工厂”技巧带来的好处:将创建对象的代码集中在一个对象或者方法中,可以避免代码中的重复,也更方便以后的维护,这表明客户在实例化对象时,只会依赖于接口,而不是具体类。这可以有助于针对接口编程,而不针对实现编程,使代码更具有弹性,可以应对未来的扩展。

5.依赖倒置原则

5.1 对象依赖问题

封装起对创建对象的代码,就可以对抽象编码,将客户代码和真实的实现进行解耦。但在ConcreteCreator的代码中,还是需要使用具体类来实例化真正的对象,这么做的原因是什么?

因为对象的创建是现实的,如果不创建任何对象,就无法创建任何Java程序。利用这个显示的知识,可以将这些创建对象的代码统一起来,类似于用栅栏围起来,把羊毛堆到眼前,从而保护这些创建对象的代码,如果不将这些代码统一起来,那么就无法收集到“羊毛”了。

看一个很依赖的比萨店:

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

 对象依赖的定义当直接实例化一个对象时,就是在依赖它的具体类。这个比萨店对象,就是个依赖性很高的例子,因为对于比萨具体实现的任何改变都会影响PizzaStore,比如新增加一个比萨具体实现类,那么就需要改变PizzaStore代码。

 5.2 依赖倒置原则

在代码里减少对于具体类的依赖是件“好事”。“依赖倒置原则”就是正式阐明了这个观点:

依赖倒置原则:要依赖抽象,不要依赖具体类

这个原则听起来很像是“针对接口编程,不针对实现编程”,但是此处更强调抽象。这个原则说明:不能让高层组件依赖底层组件,并且不管高层或底层组件,“两者”都依赖于抽象

关于“高层组件” :是由其他低层组件定义其行为的类。例如,PizzaStore是个高层组件,因为它的行为是由比萨定义的,PizzaStore创建所有不同的比萨对象,准备、烘烤、切片、装盒;而比萨本身属于低层组件。

在回顾5.1中比萨店的图可知,PizzaStore是“高层组件”,而比萨实现是“低层组件”,因此,PizzaStroe依赖这些具体比萨类。

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

5.3 原则的应用

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

即便已经穿件了一个抽象对象Pizza,但是仍然在代码中实际创建了具体的Pizza,因此,这个抽象没什么影响力。

如何在orderPizza()方法中,将这些实例化对象的代码独立出来?工厂方法刚好能派上用场。应用工厂方法之后,类图看起来就像这样:

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

5.4 依赖倒置原则-倒置思考方式

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

为了更好地理解,那么需要 倒置我的思考方式

Q1:如果需要实现一个比萨店,第一件想到的事情是什么?

A1:比萨店进行准备、烘烤、装盒,比萨店还需要制作许多不同风味的比萨,例如:芝士比萨、素食比萨、蛤蜊比萨。。。

Q2:芝士比萨、素食比萨、蛤蜊比萨都是比萨,因此他们应该共享一个 Pizza接口。

A2:先从顶端开始,然后往下到具体类。但不想让比萨店理会这些具体类,否则比萨店将全都依赖这些具体类。因此,需要“倒置”想法,别从顶端开始,而是从比萨Pizza开始,然后想想看能抽象化些什么。

Q3:既然已经有一个比萨抽象就可以开始设计比萨店,因此不用理会具体的比萨类了。

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

指导方针可以帮助避免在OO设计中违反依赖倒置原则

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

在设计中,完全遵守这些方阵是不太可能的,但就像其他原则一样,应该尽量达到这个原则,而不是随时都要遵循这个原则。但是,如果深入体验这些方针,将这些方阵内化成写代码思考的一部分,那么在设计时,我们将指导何时有足够的理由违反这样的原则。比如,如果有一个不像是会改变的类,那么在代码中直接实例化具体类也就没什么大碍了。另一方面,如果有个类可能改变,那么就可以采用一些好技巧(例如工厂方法)来封装改变

6.抽象工厂模式

6.1 场景引入--原料家族

比萨店的设计通过引入简单工厂模式,其具有弹性的框架,并且遵循设计原则。

现在,比萨店成功的关键在于新鲜、高质量的原料,而且通过导入新的框架,加盟店将遵循流程,但有些加盟店使用低价原料来增加利润。为了保证品牌的形象,必须采用一些手段。

确保原料一致

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

问题引入:纽约使用一组原料,而芝加哥使用另一组原料。比萨店是如此受欢迎,可能不久之后加州就有加盟店了,到时候又需要运送另一组区域的原料。那接着呢,如果其他城市还有呢,比如西雅图?

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

因为所有比萨都是使用相同的组件制造而成的,但是每个区域对于这些组件却有不同的实现

每个家族都包含了一种面团、一种酱料、一种芝士,以及一种海鲜佐料(还有一些没写出来的原料,例如蔬菜与香料)的类型。芝加哥、纽约和加州的原料家族组成如下:

  •  芝加哥:FrozenClams、PlumTomatoSauce、ThickCrustDough、MozzarellcCheese.
  • 纽约:FreshClams、MarinaraSauce、ThinCrustDough、ReggianoCheese.
  • 加州:Calamarl、BrushchettaSauce、VeryThinCrust、GoatCheese.

整体来说,这三个区域组成了原料家族,每个区域实现了一个完整的原料家族。

6.2 建造原料工厂

现在,我们要建造一个工厂来生产原料;这个工厂将负责创建原料家族中的每一种原料,也就是说,工厂将需要生产面团、酱料、芝士等。

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

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

    public Sauce createSauce();

    public Cheese createCheese();

    public Veggies[] createVeggies();

    public Peppernoi createPepperoni();

    public Clams createClam();

}

 在接口中,每个原料都有一个对应的方法创建该原料。

如果每个工厂实例内都有某一种通用的“机制”需要实现,就可以把这个例子改写成抽象类。

要做的事情

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

6.3 创建纽约原料工厂

纽约原料工厂专精于生产:FreshClams、MarinaraSauce、ThinCrustDough、ReggianoCheese.

代码为:

public class NYPizzaIngredientFactory implements PizzaIngedientFctory {
    @Override
    public Dough createDough() {

        return new ThinCrustDough();
    }

    @Override
    public Sauce createSauce() {

        return new MarinaraSauce();
    }

    @Override
    public Cheese createCheese() {

        return new ReggianoCheese();
    }

    @Override
    public Veggies[] createVeggies() {
        //对于蔬菜,以一个蔬菜数组作为返回值,在这里直接将蔬菜写死
        Veggies veggies[]={new Garlic(),new Onion(),new Mushroom(),new RedPepper()};
        return veggies;
    }

    @Override
    public Pepperoni createPepperoni() {

        return new SlicedPepperoni();
    }

    @Override
    public Clams createClam() {

        return new FreshClams();
    }
}

类似的,芝加哥原材料工厂代码为:

public class ChicagoPizzaIngredientFactory implements PizzaIngedientFctory {

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

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

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

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

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

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

6.4 重做比萨

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

public abstract class Pizza {

    String name;
    Dough dough;
    Sauce sauce;
    Veggies veggies[];
    Cheese cheese;
    Pepperoni pepperoni;
    Clams clams;

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

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

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

    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 null;
    }
}

在抽象Pizza类中,将prepare()方法声明成抽象方法在这个方法中,需要收集比萨所需的原料,而这些原料是来自原料工厂的

有了抽象Pizza之后,开始创建纽约和芝加哥风味的比萨了。从此,加盟店必须直接从供厂区的原料,回顾之前写过的工厂方法代码,有NYCheesePizza和ChigoCheesePizza类,这两个类唯一的区别在于使用区域性的原料,而比萨的做法都一样(面团+酱料+芝士),其他的比萨(蔬菜、蛤蜊等)也是如此。他们都依循着相同的准备步骤,只是使用不同的原料。

因此,不需要设计两个不同的类来处理不同风味的比萨,只需要让原料工厂处理这种区域差异即可。下面是CheesePizza:

public class CheesePizza extends Pizza{

    PizzaIngedientFctory ingedientFctory;

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

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

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

 同时,也来看看蛤蜊比萨:

public class ClamPizza extends Pizza {
    
    PizzaIngedientFctory ingedientFctory;

    public ClamPizza(PizzaIngedientFctory ingedientFctory) {
        this.ingedientFctory = ingedientFctory;
    }

    @Override
    void prepare() {
        System.out.println("Preparing "+name);
        dough=ingedientFctory.createDough();
        sauce=ingedientFctory.createSauce();
        cheese=ingedientFctory.createCheese();
        clams=ingedientFctory.createClam();
    }
}

然后再回到比萨店,看NYPizzaStore的代码。纽约店会用到纽约比萨原料工厂,由该原料工厂负责生产所有纽约风味比萨所需的原料。

public class NYPizzaStore extends PizzaStore {

    @Override
    protected Pizza createPizza(String item) {
        Pizza pizza = null;
        PizzaIngedientFctory ingredientFactory =
                new NYPizzaIngredientFactory();

        if (item.equals("cheese")) {

            pizza = new CheesePizza(ingredientFactory);
            pizza.setName("New York Style Cheese Pizza");

        } else if (item.equals("veggie")) {

            pizza = new VeggiePizza(ingredientFactory);
            pizza.setName("New York Style Veggie Pizza");

        } else if (item.equals("clam")) {

            pizza = new ClamPizza(ingredientFactory);
            pizza.setName("New York Style Clam Pizza");

        } else if (item.equals("pepperoni")) {

            pizza = new PepperoniPizza(ingredientFactory);
            pizza.setName("New York Style Pepperoni Pizza");

        }
        return pizza;
    }
}

6.5 代码改变的作用

一连串的代码改变,引入了新类型的工厂,即抽象工厂,来创建比萨原料家族。

引入新类型的工厂,即抽象工厂,来创建比萨原料家族。

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

因为代码从实际的产品中解耦了,因此我们可以替换不同的工厂来取得不同的行为。

 6.6 给Ethan和Joel更多的比萨

现在所订购的比萨全是利用新原料工厂的原料制作出来的。Ethan喜欢纽约风味的,则他的订购流程为:

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

 6.7 定义抽象工厂模式

抽象工厂模式可以创建产品的家族。

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

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

 在电子书的第193页,有直接用PizzaStore的观点来展示的。

抽象工厂的任务是定义一个负责创建一组产品的接口。这个接口内的每个方法都负责创建一个具体产品,同时我们利用实现抽象工厂的子类来提供这些具体的做法。

7.工厂方法和抽象工厂的比较

工厂方法和抽象工厂都是负责创建对象,工厂方法用的是继承,而抽象工厂是通过对象的组合。

工厂方法:利用工厂方法创建对象,需要扩展一个类,并覆盖它的工厂方法。整个工厂方法模式,是通过子类来创建对象。用这种做法,客户只需要知道他们所使用的抽象类型就可以了,由子类来负责决定具体类型。工厂方法负责将客户代码从需要实例化的具体类中解耦

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

当需要扩展这组相关产品(比如说,新增一个产品)就需要改变接口,改变接口就意味着必须深入改变每个子类的接口,这是很繁重的工作。抽象工厂需要一个大的接口,因为这是被用来创建整个产品家族的。

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

见电子书P196

 抽象工程模式:

 看UML图无法直接推断的话,就看代码即可。

8.要点总结

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

截止到本章节,学到的OO原则:

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

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值