设计模式详解 简单工厂模式

1.接口基础回顾

设计模式六大原则,其中依赖倒转原则,强调我们应该面向接口编程,那什么是接口?接口的作用? 接口如何使用?

1.1 接口回顾

1.接口的概念

接口是一种特殊的抽象类,跟一般的抽象类相比,接口里面的所有方法都是抽象方法,接口里面的所有属性都是常量。也就是说,接口里面是只有方法定义而不会有任何方法实现。当然最新的jdk接口里面已经可以有实现了, 当然那不是本章讨论的重点.

2.接口的作用

通常用接口来定义实现类的外观,也就是实现类的行为定义,用来约束实现类的行为。接口就相当于一份契约,根据外部应用需要的功能,约定了实现类应该要实现的功能,但是具体的实现类除了实现接口约定的功能外,还可以根据需要实现一些其它的功能,这是允许的,也就是说实现类的功能包含但不仅限于接口约束的功能

3.接口思想

根据接口的作用和用途,浓缩下来,接口的思想就是“封装隔离”。
通常提到封装是指对数据的封装,但是这里的封装是指“对被隔离体的行为的封装”,或者是“对被隔离体的职责的封装”;而隔离指的是外部调用和内部实现,外部调用只能通过接口进行调用,而外部调用是不知道内部具体实现的,也就是说外部调用和内部实现是被接口隔离开的。

4.接口的好处

由于外部调用和内部实现被接口隔离开了,那么只要接口不变,内部实现的变化就不会影响到外部应用,从而使得系统更灵活,具有更好的扩展性和可维护性,这也就是所谓“接口是系统可插拔性的保证”这句话的意思。

5.接口和抽象类的选择
既然接口是一种特殊的抽象类,那么在开发中,何时选用接口,何时选用抽象类呢?对于它们的选择,在开发中是一个很重要的问题,特别总结两句话给大家:优先选用接口。在如下情况应选择抽象类:既要定义子类的行为,又要为子类提供公共的功能。

1.2 面向接口编程

在Java 程序设计里面,非常讲究层的划分和模块的划分。通常按照三层来划分Java程序,分别是表现层、逻辑层、数据层,它们之间都要通过接口来通讯。

在每一个层里面,又有很多个小模块,一个小模块对外也应该是一个整体,那么一个模块对外也应该提供接口,其它地方需要使用到这个模块的功能,都应该通过此接口来进行调用。这也就是常说的“接口是被其隔离部分的外观”。基本的三层结构如图所示:

三层结构示意图

在一个层内部的各个模块交互也要通过接口,如图所示:


层内调用示意图

上面频频提到“组件”,那么什么是组件呢?先简单的名词解释一下:

组件从设计上讲,组件就是能完成一定功能的封装体,小到一个类,大到一个系统,都可以称为组件,因为一个小系统放到更大的系统里面去,也就当个组件而已。事实上,从设计的角度看,系统、子系统、模块、组件等说的其实是同一回事情,都是完成一定功能的封装体,只不过功能多少不同而已。

继续刚才的思路,大家会发现,不管是一层还是一个模块或者一个组件,都是一个被接口隔离的整体,那么下面我们就不去区分它们,统一认为都是接口隔离体即可,如图所示:

接口隔离体示意图

1.3 不用设计模式是如何使用接口的

假设有一个接口叫Api,然后有一个实现类Impl实现了它,在客户端怎么用这个接口呢?

接口和实现同客户端的关系

通常都是在客户端创建一个Impl的实例,把它赋值给一个Api接口类型的变量,然后客户端就可以通过这个变量来操作接口的功能了

接口及实现:

/**
 * 某个接口(通用的、抽象的、非具体的功能) 
 */
public interface Api {

    /**
     * 接口中的某个sayHello的功能
     */
    public void sayHello();
}

/**
 * 某个接口实现
 */
public class ApiImpl implements Api {

    @Override
    public void sayHello() {
        System.out.println("我是接口实现!");
    }
}

客户端怎么使用接口, 按照Java的知识,接口不能直接使用,需要使用接口的实现类,示例代码如下:

/** 
 *  客户端:测试使用Api接口 
 */
public class Client {

    public static void main(String[] args) {
        Api impl = new ApiImpl();
        impl.sayHello();
    }
}

1.4 存在问题

你会发现在客户端调用的时候,客户端不但知道了接口,同时还知道了具体的实现就是ApiImpl。而接口的思想是“封装隔离”,而Impl这个实现类,应该是被接口Api封装并与客户端隔离开的,也就是说,客户端根本就不应该知道具体的实现类是Impl

我们要解决的问题就变成了:

只知道接口而不知实现,该怎么获得这个接口的实例?

就像现在的Client,它知道要使用Api接口,但是不知由谁实现,也不知道如何实现,从而得不到接口对象,就无法使用接口,该怎么办呢?

2.解决方案

用来解决上述问题的一个合理的解决方案就是简单工厂,那么什么是简单工厂呢?,

1.概念

提供一个创建实例的功能,而无需关心具体的实现。被创建实例的类型可以是接口,抽象类,和普通类。

简单工厂模式是类的创建模式,又叫做静态工厂方法(Static Factory Method)模式。简单工厂模式是由一个工厂对象决定创建出哪一种产品类的实例。

2.1 应用简单工厂来解决的思路

分析上面的问题,虽然不能让模块外部知道模块内的具体实现,但是模块内部是可以知道实现类的,而且创建接口是需要具体实现类的。那么干脆在模块内部新建一个类,在这个类里面来创建接口,然后把创建好的接口返回给客户端,这样外部应用就只需要根据这个类来获取相应的接口对象,然后就可以操作接口定义的方法了。把这样的对象称为简单工厂,就叫Factory吧。
这样一来,客户端就可以通过这个Factory来获取需要的接口对象,然后调用接口的方法来实现需要的功能,而且客户端也不用再关心具体实现了。

2.2 简单工厂结构

简单工厂

Api:定义客户所需要的功能接口

ApiImpl:具体实现Api的实现类,可能会有多个

Factory:工厂,选择合适的实现类来创建Api接口对象

Client:客户端,通过Factory去获取Api接口对象,然后面向Api接口编程

2.3 通过简单工厂重写示意

Api 和 ApiImpl的定义同上一样, 下面看看工厂类, 通常是静态方法:

/**
 * 简单工厂类
 */
public class Factory {

    /** 
     * 具体的创造Api对象的方法
     * @param type 示意,从外部传入的选择条件 
     * @return 创造好的Api对象 
     */
    
    public static Api createApi(int type) {
        switch (type) {
        case 1:
            return new ApiImpl();
        case 2:
            return new ApiImpl2();
        default:
            break;
        }
        return null;
    }
}

再来看看客户端的示意,示例代码如下:

/** 
 *  客户端:测试使用Api接口 
 */
public class Client {

    public static void main(String[] args) {
        // 通常工厂类的方法是静态的, 此处传参数改变获取到的具体实现
        Api impl = Factory.createApi(2);
        impl.sayHello();
    }
}

就如同上面的示例,客户端通过简单工厂创建了一个实现接口的对象,然后面向接口编程,从客户端来看,它根本就不知道具体的实现是什么,也不知道是如何实现的,它只知道通过工厂获得了一个接口对象,然后就能通过这个接口来获取想要的功能。

事实上,简单工厂能帮助我们真正开始面向接口编程,像以前的做法,其实只是用到了接口的多态那部分的功能,最重要的“封装隔离性”并没有体现出来。

3.模式讲解

3.1 疑问

上面示例中的简单工厂看起来不就是把客户端里面的“new ApiImpl()”移动到简单工厂里面吗?不还是一样通过new一个实现类来得到接口吗?把“new ApiImpl()”这句话放到客户端和放到简单工厂里面有什么不同吗?

理解这个问题的重点就在于理解简单工厂所处的位置。

根据前面的学习,我们知道接口是用来封装隔离具体的实现的,目标就是不要让客户端知道封装体内部的具体实现简单工厂的位置是位于封装体内的,也就是简单工厂是跟接口和具体的实现在一起的,算是封装体内部的一个类,所以简单工厂知道具体的实现类是没有关系的

如图所示, 对于客户端, 虚线内部是它看不到的部分:


不可见内部

图中虚线框,就好比是一个组件的包装边界,表示接口、实现类和工厂类组合成了一个组件,在这个封装体里面,只有接口和工厂是对外的,也就是让外部知道并使用的,所以故意漏了一些在虚线框外,而具体的实现类是不对外的,被完全包含在虚线框内。

对于客户端而言,只是知道了接口Api和简单工厂Factory,通过Factory就可以获得Api了,这样就达到了让Client在不知道具体实现类的情况下获取接口Api。所以看似简单的把“new ApiImpl()”这句话从客户端里面移动到了简单工厂里面,其实是有了质的变化的。

3.2 认识简单工厂

  • 简单工厂的功能

工厂嘛,就是用来造东西的。在Java里面,通常情况下是用来造接口的,但是也可以造抽象类,比如本文下面有一个计算器的例子,甚至是一个具体的类实例。一定要注意,虽然前面的示例是利用简单工厂来创建的接口,但是也是可以用简单工厂来创建抽象类或者是普通类的实例的

  • 静态工厂

使用简单工厂的时候,通常不用创建简单工厂类的类实例,没有创建实例的必要。因此可以把简单工厂类实现成一个工具类,直接使用静态方法就可以了,也就是说简单工厂的方法通常都是静态的,所以也被称为静态工厂。如果要防止客户端无谓的创造简单工厂实例,还可以把简单工厂的构造方法私有化了。

  • 万能工厂

一个简单工厂可以包含很多用来构造东西的方法,这些方法可以创造不同的接口、抽象类或者是类实例,一个简单工厂理论上可以构造任何东西,所以又称之为“万能工厂”。虽然上面的实例中,在简单工厂里面只有一个方法,但事实上,是可以有很多这样创建方法的,这点要注意。

  • 简单工厂创建对象的范围
    虽然从理论上讲,简单工厂什么都能造,但对于简单工厂可创建对象的范围,通常不要太大,建议控制在一个独立的组件级别或者一个模块级别,也就是一个组件或模块一个简单工厂。否则这个简单工厂类会职责不明,有点大杂烩的感觉。

3.3 简单工厂中方法的写法

虽然说简单工厂的方法多是用来造接口的,但是仔细分析就会发现,真正能实现功能的是具体的实现类,这些实现类是已经做好的,并不是真的靠简单工厂来创造出来的,简单工厂的方法无外乎就是:实现了选择一个合适的实现类来使用

所以简单工厂方法的内部主要实现的功能是“选择合适的实现类”来创建实例对象。既然要实现选择,那么就需要选择的条件或者是选择的参数,选择条件或者是参数的来源通常又有几种:

来源于客户端,由Client来传入参数

来源于配置文件,从配置文件获取用于判断的值

来源于程序运行期的某个值,比如从缓存中获取某个运行期的值

由Client来传入参数的简单工厂:

/**
 * 简单工厂类
 */
public class Factory {

    /** 
     * 具体的创造Api对象的方法
     * @param type 示意,从外部传入的选择条件 
     * @return 创造好的Api对象 
     */
    
    public static Api createApi(int type) {
        switch (type) {
        case 1:
            return new ApiImpl();
        case 2:
            return new ApiImpl2();
        default:
            break;
        }
        return null;
    }
}

要注意这种方法有一个缺点

由于是从客户端在调用工厂的时候,传入选择的参数,这就说明客户端必须知道每个参数的含义,也需要理解每个参数对应的功能处理。这就要求必须在一定程度上,向客户暴露一定的内部实现细节

3.4 可配置的简单工厂

在现在的实现中,再新增加一种实现,会怎样呢?

那就需要修改工厂类,才能把新的实现添加到现有系统中。
每次新增加一个实现类都来修改工厂类的实现,肯定不是一个好的实现方式。那么现在希望新增加了实现类过后不修改工厂类,该怎么办呢?

一个解决的方法就是使用配置文件,当有了新的实现类过后,只要在配置文件里面配置上新的实现类就好了,在简单工厂的方法里面可以使用反射,当然也可以使用IoC/DI(控制反转/依赖注入,这个不在这里讨论)来实现。

看看如何使用反射加上配置文件,来实现添加新的实现类过后,无须修改代码,就能把这个新的实现类加入应用中。

配置文件用最简单的properties文件,实际开发中多是xml配置。定义一个名称为“FactoryTest.properties”的配置文件,放置到Factory同一个包下面,内容如下:

ImplClass=cn.javass.dp.simplefactory.example5.Impl

如果新添加了实现类,修改这里的配置就可以了,就不需要修改程序了。

/**
* 工厂类,用来创造Api对象
*/ 
public class Factory { 
    /**
    * 具体的创造Api的方法,根据配置文件的参数来创建接口
    * @return 创造好的Api对象
    */ 
    public static Api createApi(){ 
        //直接读取配置文件来获取需要创建实例的类 
        //至于如何读取Properties,还有如何反射这里就不解释了 
        Properties p = new Properties(); 
        InputStream in = null; 
        try { 
            in = Factory.class.getResourceAsStream("FactoryTest.properties"); 
            p.load(in); 
        } catch (IOException e) { 
            System.out.println("装载工厂配置文件出错了,具体的堆栈信息如下:"); 
            e.printStackTrace(); 
        }finally{ 
            try { 
                in.close(); 
            } catch (IOException e) { 
                e.printStackTrace(); 
            } 
        } 
        //用反射去创建,那些例外处理等完善的工作这里就不做了 
        Api api = null; 
        try { 
            api = (Api)Class.forName(p.getProperty("ImplClass")).newInstance(); 
        } catch (InstantiationException e) { 
            e.printStackTrace(); 
        } catch (IllegalAccessException e) { 
            e.printStackTrace(); 
        } catch (ClassNotFoundException e) { 
            e.printStackTrace(); 
        } 
        return api; 
    } 
}

此时的客户端就变得很简单了,不再需要传入参数,代码示例如下:

/** 
 *  客户端:测试使用Api接口 
 */
public class Client {

    public static void main(String[] args) {
        Api impl = Factory.createApi();
        impl.sayHello();
    }
}

3.5 简单工厂的优缺点

帮助封装:简单工厂虽然很简单,但是非常友好的帮助我们实现了组件的封装,然后让组件外部能真正面向接口编程。

解耦:通过简单工厂,实现了客户端和具体实现类的解耦。如同上面的例子,客户端根本就不知道具体是由谁来实现,也不知道具体是如何实现的,客户端只是通过工厂获取它需要的接口对象。

可能增加客户端的复杂度:如果通过客户端的参数来选择具体的实现类,那么就必须让客户端能理解各个参数所代表的具体功能和含义,这会增加客户端使用的难度,也部分暴露了内部实现,这种情况可以选用可配置的方式来实现。

不方便扩展子工厂:私有化简单工厂的构造方法,使用静态方法来创建接口,也就不能通过写简单工厂类的子类来改变创建接口的方法的行为了。不过,通常情况下是不需要为简单工厂创建子类的。

3.6 思考简单工厂

  • 简单工厂的本质 : 选择实现

注意简单工厂的重点在选择,实现是已经做好了的。就算实现再简单,也要由具体的实现类来实现,而不是在简单工厂里面来实现。简单工厂的目的在于为客户端来选择相应的实现,从而使得客户端和实现之间解耦,这样一来,具体实现发生了变化,就不用变动客户端了,这个变化会被简单工厂吸收和屏蔽掉

实现简单工厂的难点就在于“如何选择”实现,前面讲到了几种传递参数的方法,那都是静态的参数,还可以实现成为动态的参数。比如:在运行期间,由工厂去读取某个内存的值,或者是去读取数据库中的值,然后根据这个值来选择具体的实现等等。

  • 何时选用简单工厂

建议在如下情况中,选用简单工厂:

  1. 如果想要完全封装隔离具体实现,让外部只能通过接口来操作封装体,那么可以选用简单工厂,让客户端通过工厂来获取相应的接口,而无需关心具体实现;
  1. 如果想要把对外创建对象的职责集中管理和控制,可以选用简单工厂,一个简单工厂可以创建很多的、不相关的对象,可以把对外创建对象的职责集中到一个简单工厂来,从而实现集中管理和控制。

3.7 相关模式

  • 简单工厂和抽象工厂模式

简单工厂是用来选择实现的,可以选择任意接口的实现,一个简单工厂可以有多个用于选择并创建对象的方法,多个方法创建的对象可以有关系也可以没有关系。

抽象工厂模式是用来选择产品簇的实现的,也就是说一般抽象工厂里面有多个用于选择并创建对象的方法,但是这些方法所创建的对象之间通常是有关系的,这些被创建的对象通常是构成一个产品簇所需要的部件对象。

所以从某种意义上来说,简单工厂和抽象工厂是类似的,如果抽象工厂退化成为只有一个实现,不分层次,那么就相当于简单工厂了。

  • 简单工厂和工厂方法模式

简单工厂和工厂方法模式也是非常类似的。工厂方法的本质也是用来选择实现的,跟简单工厂的区别在于工厂方法是把选择具体实现的功能延迟到子类去实现。如果把工厂方法中选择的实现放到父类直接实现,那就等同于简单工厂。

  • 简单工厂和能创建对象实例的模式

简单工厂的本质是选择实现,所以它可以跟其它任何能够具体的创建对象实例的模式配合使用,比如:单例模式、原型模式、生成器模式等等。


4.最简单的例子:计算器的小程序(利用抽象类)

4.1 求两个数之和

public class App {
    public static void main(String[] args) {
    
        Scanner sc = new Scanner(System.in);
        System.out.println("请输入数字A后按回车键");
        double numberA = sc.nextDouble();
        System.out.println("请输入数字B后按回车键");
        double numberB = sc.nextDouble();
        System.out.println("计算结果: " + (numberA + numberB));
        sc.close();
    }
}

现在需求变更, 求两数之差

改代码? 把加号改成减号?是结果是有了, 再改变需求呢?乘法除法, 再改..
鉴于需求不断改变, 封装一个计算方法在这个App类中, 便于复用.

改良后代码:

public class App {
    public static void main(String[] args) {
    
        Scanner sc = new Scanner(System.in);
        System.out.println("请输入数字A后按回车键");
        double numberA = sc.nextDouble();
        System.out.println("请输入数字B后按回车键");
        double numberB = sc.nextDouble();
        System.out.println("请输入符号后按回车键");
        String sign = sc.next();
        System.out.println("计算结果: " + operating(numberA,numberB,sign));
        sc.close();
    }
    // 计算方法
    public static double operating(double numberA, double numberB, String sign) {
        if ("+".equals(opreate)) {
            return numberA + numberB;
        } else if ("-".equals(opreate)) {
            return numberA - numberB;
        } else if ("*".equals(opreate)) {
            return numberA * numberB;
        } else if ("/".equals(opreate)) {
            return numberA / numberB;
        }
        return 0;
    }
}

记得当初学习编程的时候,自己练习过这种计算小程序,也是写出这种代码,当时觉得自己牛逼的可以了,还会封装呢,哈哈..

4.2 把业务和界面分开

思考:如果现在不从控制台输入数字进行计算了, 改为从网页输入数字了,那这个app类也用不上了,难道复制一套计算的方法么? 所以实际上, 应该先把输入数字的界面和计算的业务首先分开

例如这样:

public class Operate {

    public static double operating(double a, double b, String sign) {
        if ("+".equals(sign)) {
            return numberA + numberB;
        } else if ("-".equals(sign)) {
            return numberA - numberB;
        } else if ("*".equals(sign)) {
            return numberA * numberB;
        } else if ("/".equals(sign)) {
            return numberA / numberB;
        }
        return 0;
    }
}

//App类调用: 
Operate.operating(numberA,numberB,sign);

这样的话不论你什么方式计算, 只要在相应的类中调用Operate.operating(numberA,numberB,sign);这段代码就可以了.

4.3 新的问题,两数相除得验证除数是否为0啊

最差解决方案: 在除法的if判断里加层逻辑判断么?判断b的值是否为0? 可以解决, 但是不是最好的方案. 但违背软件设计的开闭原则

如果此时再来个需求, 计算两个数的差再求和, 在Operate类中增加switch的分支么? 可以解决, 不过同上道理, 代码会越来越臃肿, 并且违背开闭原则

其实就这个模拟计算的程序而言, 怎么写可能都难以看出区别, 但是对于真正项目来说, 代码的优美, 对于程序的稳定和维护实际意义要大的多.

那么针对上述几个无聊的需求来说, 首先“除数为0”是只针对除法对象的需求, 利用面向对象的思想, "加法"对象会说, 你"0"不"0"的关老子鸟事? 对, 不关其他符号对象的鸟事!自己解决自己的事嘛!

说白了, 就是分离加 减 乘 除 他们, 让他们作为对象存在, 不管自己的算法有多复杂, 或者逻辑有多复杂, 都跟其他符号没关系, 减少代码维护的风险和难度.

另外, 大话设计模式这本书, 说的需求例子也很好, 我不细致描述了, 意思差不多, 有书的朋友可以参考.

所以我们需要分离运算符

将 加 减 乘 除 分离开, 四个对象均继承自运算Operate对象

Operate抽象类 具有计两个数的方法, 具体实现由 他们自己完成

public abstract class Operate {
    public abstract double operating(double a, double b);
}

加法, 除法的代码,其他算法略, 一个意思都

public class OperateAdd extends Operate {
    public double operating(double a, double b) {
        return a + b;
    }
}
public class OperateDiv extends Operate {
    public double operating(double a, double b) {
        if (b == 0) {
            throw 异常("除数为0....");
        }
        double result = a / b;
        if (result == 0) {
            控制台提示("结果为0")....
        }
        return result;
    }
}

思考:现在问题是什么? 我在调用时怎么知道客户端需要的是哪个运算?

这就跟开篇1.4章节讲的问题是一样的了, 如下:

只知道Operate抽象类,而不知道具体实现,该怎么获得这个子类的实例?

解决方案

这时就需要 "简单工厂模式", 用一个单独的类, 来做这个创造实例的过程.

那么工厂要想知道我们需要哪个运算类, 客户端需要告诉它符号是什么对吧?

因为面向对象的思想, 甚至符号我们也可以当成对象, 在里面写一些判断输入的符号是否合法之类的代码

符号Sign对象代码:

public class SignObj {
    private String sign;

    public String getSign() {
        return sign;
    }

    public void setSign(String sign) {
        if (sign == null || sign == '') {
            抛异常...("输入符号非法")
        }
        this.sign = sign;
    }
}

简单工厂的创建实例方法的代码:

public static Operate creteOperate(SignObj obj) {
    if (obj == null || obj.getSign() == null) {
        return null;
    }
    Operate opet = null;
    switch (obj.getSign()) {
    case "+":
        opet = new OperateAdd();
        break;
    case "-":
        ...
        break;
    case "*":
        ...
        break;
    case "/":
        opet = new OperateDiv();
        break;  
    default:
        break;
    }
    return opet;
}

最后app调用:

// 改良后
SignObj signObj = new SignObj();
signObj.setSign("/");
Operate opet = OperateFactory.creteOperate(signObj);
System.out.println("计算结果: " + opet.getResult(numberA, numberB));

附上UML类图(图画的不全, 只画了加法和减法, 除法和乘法和前面两者一样的,没画)


image

5.实际web应用的例子

各位基本上都使用过或者将来要使用的一个例子来说明简单工厂模式,我们去模拟一个简单的struts2的功能。

本例会制作一个简单的WEB项目,其中会忽略掉很多细节,目的是为了突出我们的简单工厂模式。

众所周知,我们平时开发web项目大部分是以spring作为平台,来集成各个组件,比如集成struts2来完成业务层与表现层的逻辑,集成hibernate或者ibatis来完成持久层的逻辑。

struts2在这个过程当中提供了分离数据持久层,业务逻辑层以及表现层的责任,有了Struts2,我们不再需要servlet,而是可以将一个普通的Action类作为处理业务逻辑的单元,然后将表现层交给特定的视图去处理,比如JSP,template等等。

我们来尝试着写一个非常非常简单的web项目,来看看在最原始的时候,也就是没有spring,struts2等等这些个开源框架的时候,我们都是怎么过的。

5.1 web项目的类

假设这是一个小型的WEB项目,我们通常里面会有这些类:


//数据源连接池,用来生产数据库连接。
class DataSource{}

//我们一般会有这样一个数据访问的基类,这个类要依赖于数据源
class BaseDao{}
    
//一般会有一系列这样的DAO去继承BaseDao,这一系列的DAO类便是数据持久层
class UserDao extends BaseDao{}
class PersonDao extends BaseDao{}
class EmployeeDao extends BaseDao{}
    
//我们还会有一系列这样的servlet,他们通常依赖于各个Dao类,这一系列servlet便是我们的业务层
class LoginServlet extends HttpServlet{}
class LoginOutServlet extends HttpServlet{}
class RegisterServlet extends HttpServlet{}
    
//我们通常还会有HTML页面或者JSP页面,但是这个本次不在考虑范围内,这便是表示层。

以上是我们小型WEB项目大体的结构,可以看到三个Servlet没有写具体的实现到底如何,但是不难猜测,三个Servlet的功能分别是进行登录,注销,以及注册新用户的功能。

我们的servlet一般都是继承自HttpServlet,因为我们在web.xml配置servlet时,所写入的Class需要实现Servlet接口,而我们通常采用的传输协议都是HTTP,所以HttpServlet就是我们最好的选择了,它帮我们完成了基本的实现。

5.2 存在的问题

但是这样我们有很多限制,比如我们一个Servlet一般只能负责一个单一的业务逻辑,因为我们所有的业务逻辑通常情况下都集中在doPost这样一个方法当中,可以想象下随着业务的增加,我们的Servlet数量会高速增加,这样不仅项目的类会继续增加,最最恶心的是,我们每添加一个Servlet就要在web.xml里面写一个servlet配置。

但是如果我们让一个Servlet负责多种业务逻辑的话,那我们需要在doPost方法中加入很多if判断,去判断当前的操作。

比如我们将上述三个servlet合一的话,你会在doPost出现以下代码。

protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //我们加入一个操作的参数,来让servlet做出不同的业务处理
        String operation = req.getParameter("operation");
        if (operation.equals("login")) {
            System.out.println("login");
        }else if (operation.equals("register")) {
            System.out.println("register");
        }else if (operation.equals("loginout")) {
            System.out.println("loginout");
        }else {
            throw new RuntimeException("invalid operation");
        }
    }

或者再好一点,会这么写

protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //我们加入一个操作的参数,来让servlet做出不同的业务处理
        String operation = req.getParameter("operation");
        if (operation.equals("login")) {
            login();
        }else if (operation.equals("register")) {
            register();
        }else if (operation.equals("loginout")) {
            loginout();
        }else {
            throw new RuntimeException("invalid operation");
        }
    }
    
    private void login(){
        System.out.println("login");
    }
    
    private void register(){
        System.out.println("register");
    }
    
    private void loginout(){
        System.out.println("loginout");
    }

虽然我们已经将各个单一的业务逻辑拆分成方法,但这依然是违背单一原则的,因为我们的servlet应该只是处理业务逻辑,而不应该还要负责与业务逻辑不相关的处理方法定位这样的责任,这个责任应该交给请求方,原本在三个servlet分别处理登陆,注销和注册的时候,其实就是这样的,作为请求方,只要是请求LoginServlet,就说明请求的人是要登陆,处理这个请求的servlet不需要再出现有关判断请求操作的代码。

所以我们需要想办法把判断的业务逻辑交给请求方去处理,回想下struts2的做法,我们来模拟一个分配请求的过滤器,它的任务就是根据用户的请求去产生响应的servlet处理请求

我们用这个过滤器来消除servlet在web.xml的配置,帮我们加快开发的速度,我们写出如下filter。

5.3 增加过滤器

/用来分派请求的filter
public class DispatcherFilter implements Filter{
    
    private static final String URL_SEPARATOR = "/";
    
    private static final String SERVLET_PREFIX = "servlet/";
    
    private String servletName;
    
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,FilterChain filterChain) throws IOException, ServletException {
        parseRequestURI((HttpServletRequest) servletRequest);
        //这里为了体现我们本节的重点,我们采用一个工厂来帮我们制造Action
        if (servletName != null) {
            //这里使用的正是简单工厂模式,创造出一个servlet,然后我们将请求转交给servlet处理
            Servlet servlet = ServletFactory.createServlet(servletName);
            servlet.service(servletRequest, servletResponse);
        }else {
            filterChain.doFilter(servletRequest, servletResponse);
        }
    }
    
    //负责解析请求的URI,我们约定请求的格式必须是/contextPath/servlet/servletName
    private void parseRequestURI(HttpServletRequest httpServletRequest){
        String validURI = httpServletRequest.getRequestURI().replaceFirst(httpServletRequest.getContextPath() + URL_SEPARATOR, "");
        if (validURI.startsWith(SERVLET_PREFIX)) {
            servletName = validURI.split(URL_SEPARATOR)[1];
        }
    }

}

这个filter需要在web.xml中加入以下配置

<filter>
  <filter-name>dispatcherFilter</filter-name>
  <filter-class>com.web.filter.DispatcherFilter</filter-class>
</filter>
<filter-mapping>
  <filter-name>dispatcherFilter</filter-name>
  <url-pattern>/servlet/*</url-pattern>
</filter-mapping>

下面给出我们的主角,我们的servlet工厂,它就相当于上面的Factory。

public class ServletFactory {

    private ServletFactory(){}
    //一个servlet工厂,专门用来生产各个servlet,而我们生产的依据就是传入的servletName,
    //这个serlvetName由我们在filter截获,传给servlet工厂。
    public static Servlet createServlet(String servletName){
        if (servletName.equals("login")) {
            return new LoginServlet();
        }else if (servletName.equals("register")) {
            return new RegisterServlet();
        }else if (servletName.equals("loginout")) {
            return new LoginoutServlet();
        }else {
            throw new ServletException("unknown servlet");
        }
    }
}

看到这里,是不是有点感觉了呢,我们一步一步去消除servlet的XML配置的过程,其实就是在慢慢的写出一个简单工厂模式,只是在这之中,抽象的产品接口是现成的,也就是Servlet接口。

虽说这些个elseif并不是好代码的征兆,不过这个简单工厂最起码帮我们解决了恶心的xml配置,说起来也算功不可没。

简单工厂是设计模式当中相对比较简单的模式,它甚至都没资格进入GOF的二十三种设计模式,所以可见它多么卑微了,但就是这么卑微的一个设计模式,也能真正的帮我们解决实际当中的问题,虽说这种解决一般只能针对规模较小的项目。


参考文档


推荐阅读更多精彩内容