(四)工厂方法模式详解(另附简单工厂的死亡之路)

               作者:zuoxiaolong8810(左潇龙),转载请注明出处,特别说明:本博文来自博主原博客,为保证新博客中博文的完整性,特复制到此留存,如需转载请注明新博客地址即可。

               文章开头首先非常感谢各位的支持,代理模式中提到了class文件(即字节码文件)的相关知识,有一位读者说想要看有关class文件的相关内容,我也意识到了这一点,所以以后如果有在讲解那个模式的过程当中用到了其它的技术,我会留一些篇幅去介绍这个技术,有关class文件的内容我也会看以后的模式当中有没有用到的地方顺便简单介绍一下,如果没有的话,我会在设计模式介绍完以后专门写一篇有关java字节码文件的相关内容。

               本章我们继续讨论新的设计模式,工厂方式模式,在这之前,LZ决定先给出引自其它地方的标准定义以及类图。

               定义:工厂方法(Factory Method)模式的意义是定义一个创建产品对象的工厂接口,将实际创建工作推迟到子类当中。核心工厂类不再负责产品的创建,这样核心类成为一个抽象工厂角色,仅负责具体工厂子类必须实现的接口,这样进一步抽象化的好处是使得工厂方法模式可以使系统在不修改具体工厂角色的情况下引进新的产品。

               可以看到工厂方法模式中定义了一个工厂接口,而具体的创建工作推迟到具体的工厂类,它是对简单工厂模式中的工厂类进一步抽象化,从而产生一个工厂类的抽象和实现体系,从而弥补简单工厂模式对修改开放的诟病。

               下面LZ给出工厂方法模式的类图,该类图和定义引自百度百科。


               可以看到,上面右半部分是产品抽象和实现体系,左半部分是工厂抽象和实现体系,其中工厂体系依赖于产品体系,每一个工厂负责创造一种产品,这就省去了简单工厂中的elseif判断,又客户端决定实例化一个特定的工厂去创建相应的产品。

               下面LZ简单的使用JAVA代码诠释上述标准的工厂方法模式的类图。

               首先是抽象产品接口。

public interface Light {

    public void turnOn();

    public void turnOff();
    
}

               下面是具体的产品。

public class BuldLight implements Light{

    public void turnOn() {
        System.out.println("BuldLight On");    
    }

    public void turnOff() {
        System.out.println("BuldLight Off");    
    }

}
public class TubeLight implements Light{

    public void turnOn() {
        System.out.println("TubeLight On");    
    }

    public void turnOff() {
        System.out.println("TubeLight Off");    
    }

}

               下面是抽象的工厂接口。

public interface Creator {

    public Light createLight();
}

               下面是创建指定产品的具体工厂。

public class BuldCreator implements Creator{

    public Light createLight() {
        return new BuldLight();
    }

}
public class TubeCreator implements Creator{

    public Light createLight() {
        return new TubeLight();
    }

}

              下面我们写个测试类去实验一下这个工厂方法模式的实例代码。

public class Client {

    public static void main(String[] args) {
        Creator creator = new BuldCreator();
        Light light = creator.createLight();
        light.turnOn();
        light.turnOff();
        
        creator = new TubeCreator();
        light = creator.createLight();
        light.turnOn();
        light.turnOff();
    }
}

               运行结果如下。



              可以看到,我们使用可以随意的在具体的工厂和产品之间切换,并且不需要修改任何代码,就可以让原来的程序正常运行,这也是工厂方法模式对扩展开放的表现,另外工厂方法模式弥补了简单工厂模式不满足开闭原则的诟病,当我们需要增加产品时,只需要增加相应的产品和工厂类,而不需要修改现有的代码。
              上面的示例可以比较清楚的展示各个类之间的关系,但是始终缺乏说服力,因为它完全没有什么实际意义,下面LZ就给出一些我们接触过的例子来说明工厂方法模式的好处。
               关于能够说明工厂方法模式的实例,LZ翻遍了所有能找到的源码,想寻找一个让各位读者既能学习到新的东西,又能对工厂方法理解更深的现有的优秀框架的设计。经过跋山涉水,LZ决定还是拿数据库连接来说事,我知道你想说,我去,又是数据库连接。LZ只想说,我们每天做的最多的就是增删改查好吗,其它的咱也不认识啊,囧。

               众所周知,为了统一各个数据库操作的标准,于是有了JDBC的API,它用于给我们这种被称作只会使用现成的东西的程序猿,提供一系列统一的,标准化的操作数据库的接口。其实JDBC的各个类或接口,就是我们操作数据库的过程中各个协助者的抽象,这样的设计是为了让我们对数据库的操作依赖于抽象,还记得我们在设计模式总纲中提到的一句话吗,用抽象构建框架,用细节扩展实现。

               JDBC API(即抽象的接口或类)就是整个数据库操作的框架,而各个数据库的驱动就是那些细节。而我们的操作依赖于JDBC API,而不是任何一个具体数据库的细节。

               JDBC是如何统一了数据库世界的呢?其实最主要的就是靠两个接口,就统一了世界。。。

               来看第一个接口Driver,附上源码。

package java.sql;

import java.sql.DriverPropertyInfo;
import java.sql.SQLException;

/**
 * The interface that every driver class must implement.
 */
public interface Driver {

    Connection connect(String url, java.util.Properties info)
        throws SQLException;

    boolean acceptsURL(String url) throws SQLException;

    DriverPropertyInfo[] getPropertyInfo(String url, java.util.Properties info)
             throws SQLException;

    int getMajorVersion();

    int getMinorVersion();

    boolean jdbcCompliant();
} 

               由于篇幅,LZ删掉了很多注释,只保留了这个类注释的第一句话,翻译过来是这是一个任何驱动类都必须实现的接口。多么霸气啊。也就是每个数据库厂商都必须实现这个接口来提供JDBC服务,即java数据库连接服务,来方便程序猿对数据库应用编程。

               我们先忽略掉下面的五个方法,第一个方法毫无疑问是这个接口中相对而讲最重要的方法了,即创造一个数据库连接,虽然方法名称是connect,但是我觉得这个方法完全可以改为createConnection。

               提到Connction,这个接口我们一定不陌生,它的源码也已经在代理模式一章出现过,这里我们再次让它出场,我依旧会删掉它的大部分方法,限于篇幅。

package java.sql;

import java.sql.PreparedStatement;
import java.sql.SQLException;

/**
 * <P>A connection (session) with a specific
 * database. SQL statements are executed and results are returned
 * within the context of a connection.
 * <P>
 */
public interface Connection  extends Wrapper {

    Statement createStatement() throws SQLException;

    PreparedStatement prepareStatement(String sql) throws SQLException;

}

               以上便是Connection接口,这里只留下了两个方法,这两个方法相信各位读者都非常熟悉,它们都是我们最经常用的方法之二。

               以上两个接口作为JDBC API的一部分,它们相当于告诉了数据库生产厂商两个要求。

               第一,数据库厂商要提供一个数据库驱动类,它的作用可以是可以创造数据库连接,而这个数据库连接向上转型为我们JDBC的Connection。

               第二,数据库厂商要提供一个数据库连接的实现类,这个实现类可以执行具体数据库的各个操作,比如帮我们执行SQL,返回执行结果,关闭连接等等。

               我们都知道mysql的驱动类位于com.mysql.jdbc.Driver,而mysql的connection实现类也在这个包中,名称是ConnectionImpl,而相应的oracle也有驱动类,位于oracle.jdbc.driver.OracleDriver,相应的oracle也有connection实现类,位于oracle.jdbc.OracleConnectionWrapper。一般每个数据库都会有一个Connection的扩展接口,这个接口的作用是提供使用者针对当前数据库特殊的操作。

               这里我们忽略掉这些中间接口以及抽象类,我给出上述六个类的UML图,如果各位以前知道工厂方法模式的话,各位看一下,它们的关系是否很熟悉。


               我们对比上面标准的工厂方法模式,就会发现它们的关系不正是工厂方法模式吗?

               工厂方法模式就是提供一个抽象的工厂,一个抽象的产品,在上述当中相当于Driver(数据库连接工厂)和Connection(抽象产品),实现的一方需要提供一个具体的工厂类(比如mysql驱动)和一个具体的产品(比如mysql数据库连接)。

               客户端调用时不依赖于具体工厂和产品(即到底是mysql驱动,mysql数据库连接还是oracle驱动,oracle连接,我们程序猿不需要管的,我们只管使用抽象的driver和connection,对吧?),而是依赖于抽象工厂和抽象产品完成工作。

               各位可以看到我在类图里面加入了一个DriverManager,这个类相信各位也不陌生,这是我们天天打交道的类,虽说因为hibernate和ibatis的封装,或许我们不能经常看到,但LZ相信它活在每个程序猿的心中。

               DriverMananger在这个设计当中扮演者一个管理者的角色,它帮我们管理数据库驱动,让我们不需要直接接触驱动接口,我们获取连接只需要和DriverManager打交道就可以,也就是说客户端依赖于DriverManager和Connection就可以完成工作,不再需要与Driver关联,所以上述说我们依赖于Driver和Connection,现在DriverManager帮我们管理Driver,那我们只需要依赖于DriverManager和Connection就可以了。

               LZ在类图中拉出了DriverManager的方法,其中的registerDriver方法正是我们注册数据库驱动的入口。来看看mysql的Driver中做了什么,oracle类似。

public class Driver extends NonRegisteringDriver
  implements java.sql.Driver
{
  public Driver()
    throws SQLException
  {
  }

  static
  {
    try
    {
      DriverManager.registerDriver(new Driver());
    } catch (SQLException E) {
      throw new RuntimeException("Can't register driver!");
    }
  }
}

                 可以看到,在类构造方法中,加入了registerDriver这个方法,所以当我们使用class.forName加载驱动的时候,将会把mysql驱动注册到DriverManager,这时DriverManager中就会持有Mysql驱动所必要的信息,我们就可以使用DriverManager来获得具体的mysql连接了,当然,你要提供url,用户名和密码。

                 原来我们都是活在温室里的花朵,都被这些设计者细心呵护着,生怕我们知道一点底层的东西。记得LZ当初第一次看到Class.forName时,还觉得真是个神奇的东西,没想到只是这些设计者给我们的糖外衣。

                 工厂方法模式的好处和适用的场景都相对比较好理解。

                 好处就是,从类关系上来说,它可以让客户端与具体的工厂与产品解耦,从业务角度来说,它让客户端与具体的产品解耦

                 适用的场景就是我们需要一个产品帮我们完成一项任务,但是这个产品有可能有很多品牌(像这里的mysql,oracle),为了保持我们对产品操作的一致性,我们就可能要用到工厂方法模式。

                 工厂方法模式也有它所不足的地方,可能你会说,这多好啊,我们操纵数据库不再需要关心具体是哪个数据库。是的,你很爽啊,那是因为这些产品的实现都不用你写啊,都是数据库厂商给你写的。

                 假设产品数量巨多,而且需要我们亲手去逐个实现的时候,工厂方法模式就会增加系统的复杂性,到处都是工厂类和产品类,而且这里所说的工厂类和产品类只是概念上的,真正的产品可能不是一两个类就能搞定,否则mysql和oracle的驱动包为啥要那么多类,而不是就一个Driver和一个Connection。

                 当然这也不是绝对,比如我们经常使用的HashSet和ArrayList,也是使用的工厂方法模式,各位看下他们的类图就看出来了。


                各位可能会说,不对啊,这和我们刚才理解的不太一样啊,按照刚才的说法,我们不是应该直接使用iterable和iterator吗?这样多牛X,我们不依赖于具体产品了。对于这个LZ表示三条黑线垂下,sun或者说oracle为了集合框架给你提供了这么多具备各个特性的集合,你只用iterator和iterable,估计当初参与设计集合框架的人都要气的去shi了。。

                上述这便是工厂方法模式另外一种用法了,刚才因为我们不关心真正的产品是什么,所以我们直接使用抽象接口操作。但是我们使用iterable和iterator的时候,我们是关心真正产品的特性的,所以为了使用产品的特性,我们就需要使用产品特有的接口了,比如特殊的SortedSet可排序,比如ArrayList可以有重复元素,可以根据索引获取元素等等。当然你依然是可以使用iterable和iterator的,但是不管你用什么,在这种场景下,产品是你自己选的,一句话,你随便。。。

                两种使用方式一种是对使用者透明的,一种是不透明的,一种是使用者对具体的产品不关心,这种情况下,一般产品提供的功能是类似的。一种是使用者非常了解产品的特性,并想使用产品的特性,这种情况下,一般产品只提供最基本的一致的功能,但每个产品都会有自己独特的一面。

               但是LZ个人觉得真正做项目的过程当中很少用到工厂方法模式,这个模式更多的是帮助我们理解现有的开源项目,就像现在,你是不是对JDBC的大体框架有了一定认识了呢,如果你不知道这个模式,可能看源码会觉得一头雾水呢。

 

               另外,文章最后插播一段内容,如果各位看过上一章(简单工厂模式)的话,一定还记得那个恶心的elseif结构,这是简单工厂的诟病,它对扩展开放,对修改也开放。

               简单工厂模式在项目规模相对较小或者说具体的产品类相对不多的情况下(针对上章的描述,特指的servlet数量不多的情况下),其实这种设计还是可以接受的,因为少量的elseif可以换来我们开发上的便利。

               所以LZ建议各位永远不要忘记,规则只是用来指导你的,不是用来限制你的,只要设计合理,你的设计就是规则

               不过针对简单工厂模式,你可以认为它给我们提供了一个思路,就是我们其实可以省掉那些让人痛恨的xml配置,对于我们后续的优化有着一定指导意义。

               就像上一章中的处理方式,很明显存在着隐患,那就是在servlet数量急剧上升的时候,工厂类就会变得非常臃肿和复杂,变得难以维护和阅读。本章LZ给各位读者介绍一种优化方式,可以采取一项JDK当中在1.5版本引入的技术,即注解,去消除那些elseif的逻辑判断。

               我们可以参考struts2的做法,即每一个Servlet我们都可以采用注解去设置它的名称,或者叫url,然后我们让我们的简单工厂依据这个去实例化我们的servlet。

               根据以上方案,我们需要按照以下步骤让我们的简单工厂彻底死翘翘。

               1.需要声明一个注解,它可以用来给servlet标识它的名称。

               2.需要声明一个注解的处理器,用来处理我们的注解,主要作用是通过一个CLASS文件,去获得它的注解信息。

               3.基于性能,我们需要将servlet与名称的映射与应用的生命周期绑定,并且这份映射在整个应用当中有且仅有一份,且不可更改。

               4.让我们用于分派请求的过滤器,使用映射信息将客户请求对应到相应的servlet去处理,并且将分派逻辑移回过滤器,从而彻底删除简单工厂,即ServletFactory。

               特别说一下,这四步当中,其中第三步是可选的,但也是必须的,因为如果不做这种处理,那么你就等着你的项目N长时间打开一个网页吧。

               以上是简单工厂给我们的启示,具体如何实现这样一个基于注解的请求分配的架构,LZ不再给各位一一演示,因为这已经只剩下一个堆积代码的过程,具体的实现方案已经有了,如果各位读者有兴趣,可以私底下尝试一下这种方式。

               好了,工厂方法模式就给各位分享到这吧,感谢各位的欣赏。

               下期预告,能不能取消这个预告。。。

                

                              

               

               

posted @ 2013-08-16 21:23 左潇龙 阅读(...) 评论(...) 编辑 收藏