天天看点

工厂模式factory pattern

 烘烤oo的精华

 我们已经学了3个章节了,还没回答关于new的问题,我们不应该针对实现编程,但是当我们每次使用new时,不正是在针对实现编程吗?

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

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

 Duck duck=new MallardDuck();

要使用接口让代码具有弹性         ,new 具体类 但是还是得建立具体类的实例。

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

Duck duck;

if(picnic){

     duck=new MallardDuck();

}else if(hunting){

     duck=new DecoyDuck();

}else if(inBathTub){

     duck=new RubberDuck();

}

有一大推不同的鸭子类,但是必须等到运行时,才知道实例化哪一个。

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

 但是,总是要创建对象吧!而java只提供了一个new关键字创建对象,不是吗?

 new有什么不对劲?

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

  针对接口编程,可以隔离掉以后系统可能发生的一大堆改变,为什么呢?如果代码是针对接口编程,那么通过多态,他可以与任何新类实现该接口,但是,当代码中使用大量的具体类时,等于是自找麻烦,因为一旦加入新的具体类,就必须修改代码。

也就是说,你的代码并非”对修改关闭“,想用新的具体类型来扩展代码,就必须打开它。

 所以,当遇到这样的问题时,就应该回到oo设计原则去寻找线索,别忘了,我们的第一个原则用来处理改变,并帮助我们”找出会变化的方面,把他们从不变的部分分离出来".

认识变化的方面

 假设你有一个pizza店,

 Pizza orderPizza(){

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

      pizza.prepare();

      pizza.bake();

      pizza.cut();

      pizza.box();

       return pizza;

但是你需要更多的pizza类型,所以你增加一些代码,来“决定合适的pizza类型”,然后在制造pizza。

Pizza orderPizza(String type){ 现在把pizza类型传入

      Pizza pizza;

       if(type.equals("cheess" )){

            pizza= new CheessPizza(); 注意这里的具体pizza类型都必须实现Pizza接口

      } else if (type.equals("greek")){

            pizza= new GreekPizza();

      }

      . . .

      pizza.prepare(); 每个子类都知道如何准备自己

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

  我们想要增加一些新类型的pizza和删除一些旧类型的pizza,就必须修改以上代码。

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

封装创建对象的代码

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

pizza orderPizza(String type){

      原先的创建对象的代码已经从该方法中抽离,

      这里该怎么写呢?

把原先的创建对象的代码移到新对象中,如果任何对象想要创建pizza,找这个新对象就对了。

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

 工厂(factory)处理创建对象的细节。一旦有了SimplePizzaFactory,orderPizza()就变成了此对象的客户。当需要pizza时,就叫pizza工厂做一个。那些orderpizza()需要知道pizza类型的日子一去不复返了。现在orderPizza()只关心从工厂得到了一个pizza,而这个pizza实现了Pizza接口,所以他可以调用prepare(),bake()等。

 还有一些细节,比方说,原先在orderPizza()方法中创建代码,现在怎么写? 现在我们来实现一个简单的pizza factory。

 先从工厂本身开始,我们要定义一个类,为所有的pizza创建对象的代码,代码向这样:

public class SimplePizzaFactory //这是一个新类,他只做一件事:帮他的客户创建 pizza

{

       public Pizza createPizza(String type) { //在这个工厂中内定了这个方法,所以客户用这个方法来实例化新对象

             Pizza pizza = null;

             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(pizza店菜单)类,会利用这个工厂来取得pizza的价钱和描述。可能还有一个HomeDelivery(宅急送)类,会以与PizzaShop类不同的方式来处理Pizza。总而言之,这个类可以有很多的客户。

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

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

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

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

重做PizzaStore类

 是修改客户代码的时候了,我们要做的是仰仗工厂来为我们创建pizza。

public class PizzaStore {

      SimplePizzaFactory factory;// 为PizzaStore加上SimplePizzaFactory的引用

       public PizzaStore(SimplePizzaFactory factory)

      {

             this.factory =factory;

       public Pizza orderPizza(String type)

             Pizza pizza;

            pizza= factory.createPizza(type);

            pizza.prepare();

            pizza.bake();

            pizza.cut();

            pizza.box();

       //这里是其他方法

定义简单工厂

  简单工厂其实不是一个设计模式,反而更像一种编程习惯,但由于经常被使用,所以我们给他一个”Head First Pattern 荣誉奖“,有些开发人员的确是把这个编程习惯误认为是”工厂模式“,当你下次和另一个开发人员无话可说的时候,这应该是打破沉默的一个不错的话题。

 不要因为简单工厂不是一个”真正的“模式,就忽略的它的用法,让我们来看看新的Pizza类图:、

工厂模式factory pattern

加盟披萨店

 对象村披萨店经营有成,很多人都想加盟。身为加盟公司的经营者,希望确保加盟店营运的质量,所以希望这些店都是用你那些经过时间考验的代码。

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

 我们按照原先的思路做,利用SimplePizzaFactory,写出三种不同的工厂,那么各地加盟店都有合适的工厂可以使用。这是一种做法。

工厂模式factory pattern

 但是,你想要更多控制。。。。

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

  在想想这个问题,你真的希望能够建立一个框架,把加盟店和创建pizza绑在一起的同时又保持一定的弹性。

 在我们稍早的SimplePizzaFactory代码之前,制作pizza的代码绑在PizzaStore里,但这么做却没有弹性。那么,该如何做才能鱼与熊掌兼得呢?

给披萨店使用的框架

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

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

 首先,让我们来看看PizzaStore所做的改变。

在PizzaStore里,工厂方法现在是抽象的。

 现在已经有了一个PizzaStore作为超类,让每个域类型(NYPizzaStore,ChicagePizzaStore,CaliForniaPizzaStore)都继承这个PizzaStore,每个子类各自决定如何制造Pizza,让我们看看这要如何进行。

允许子类做决定

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

各个区域pizz之间的差异在于他们制作pizza的风味,我们现在要让createPizza()能够应对这些变化来负责创建正确种类的pizza。做法是让PizzaStore的各个子类负责定义自己的createPizza方法,所以我们会得到一些PizzaStore具体的子类。

工厂模式factory pattern

 我不明白,毕竟PizzaS的子类终究只是子类,如何能做决定?

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

 PizzaStore

createPizza()

orderPizza();

现在,更进一步地,orderPizza()方法对Pizza对象做了许多事情,(如bake,cut等),但由于Pizza对象是抽象的,orderPizza并不知道哪些实际的具体类参与进来了。换句话说:就是解耦decouple。

工厂模式factory pattern

让我们开一家Pizza  Store吧

  开加盟店有他的好处,可以从PizzaStore免费获得所有的功能,区域点只需要继承PizzaStore,然后提供createPizza()方法实现自己的Pizza风味即可。

这是纽约风味:

其他的2个类型的PizzaStore类似。

 声明一个工厂方法

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

工厂模式factory pattern

各个类的代码:

注意Pizza类代码,我们特意用了abstract,虽然里面没有abstract方法,我们不想让他实例化。

测试类:

认识工厂方法模式

 所有工厂模式都用了封装对象的创建,工厂方法模式通过让子类决定该创建的对象是什么,来达到将对象创建的过程封装的目的。让我们来看看这些类图:

工厂模式factory pattern

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

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

 让我们看看这两个平行的类层级,

工厂模式factory pattern

定义工厂方法模式

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

工厂模式factory pattern

上面的图值得仔细看看。

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

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

一个很依赖的披萨店

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

看看对象依赖

 当你直接实例化一个对象是,就是在依赖他的具体类。

我们把这个版本的披萨店和他依赖的对象画成一张图,应该是这样:

工厂模式factory pattern

  很清楚地,代码里减少对于具体类的依赖是件“好事”,事实上,有一个oo设计原则就正式阐明了这一点;这个

原则叫做:依赖倒置原则(dependency inversion principle)

通则如下:

  要依赖抽象,不要依赖具体类。

 首先,这个原则听起来很像是“针对接口编程,不针对实现编程”,不是吗?的确很像是,然而这里更强调“抽象”。

这个原则说明了:不能让高层组件依赖低层组件,而且,不管高层或低层组件,“两者”都应该依赖于抽象。

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

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

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

原则的应用

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

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

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

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

工厂模式factory pattern

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

究竟倒置在哪里?

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

几个指导方针帮助你遵循依赖倒置原则

1.变量不可以持有具体类的引用。 

      (如果使用new,就会持有具体类的引用,你可以改用工厂方法来避开这样的做法。)

2.不要让类派生自具体类。

   (如果派生自具体类,你就会依赖具体类,请派生自一个抽象(接口或抽象类)。

3不要覆盖基类中已实现的方法。

  (如果覆盖基类中以实现的方法,那么你的基类就不是一个真正适合被继承的抽象。基类中已实现的方法,应该由所有的子类共享。)

 但是,等等,要完全遵守这些规则,那么我连一个简单的程序都写不出来!

 你说的没错。正如同我们的许多原则一样,应该尽量达到这个原则,而不是随时都要遵循这个原则。

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

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

继续阅读