在编码一段时间之后,对框架、库又有了新的理解,于是打算重新学习下框架、库。

重回Servlet

让我回忆下我的Java之路,刚开始是学Java SE,在控制台里面输出看结果,当时觉得这个控制台还是比较单调的,不如动画桌面生动,本来打算学习下Swing,后来发现这玩意有点丑,后来发现Java的市场在WEB端,也就是浏览器这里,然后就开始学习JavaScript、CSS、HTML、Jquery。学完这三个,还是没有感应到Java的用处,我那个时候在想浏览器中输入框的值该怎么到Java里面呢? 答案就是Servlet,Servlet是浏览器和服务端的桥梁。像下面这样:

然后接到值之后,我们还是通常需要存储一下,也就是跟数据库打交道,也就是JDBC,后来发现JDBC有些步骤是重复的,不必每次和数据库打交道的时候都重复写一遍,也就是DBUtils。这似乎是一个完整的流程了,从页面接收数据,存储到数据库,然后页面发起请求再将数据库中的数据返回给数据库。但是这个过程并不完美,第一个就是Java和数据库的合作,每次发送SQL语句都是新建一个连接,这相当消耗资源,于是在这里我们又引入了数据库连接池。那个时候还不怎么回用maven,处理依赖,那个时候总是会去找许多jar包粘贴在lib目录下。
那个时候的一个数据处理链条大致是这样的:

然后很快我们就发现代码越写越多了,于是我们就是开始划分结构,也就是MVC,上面的图就被切成了下面这样:

view是视图层提供给用户,供用户操作,是程序的外壳。Controller根据用户输入的指令,选取对应的model,读取,然后进行相应的操作,返回给用户。
这三层是紧密联系在一起的,但又是互相独立的,每一层内部的变化不影响其他层。每一层都对外提供接口(Interface),供上面一层调用,这样一来,软件就可以实现模块化,修改外观和变更数据都不用修改其他层,大大方便了软件的维护和升级。
但是还不是很完善,现在来看我们的代码结构变得清晰了。
软件设计的一个目标就是写的代码越来越少,原生的JDBC还是有些繁琐,那么能否更进一步,更加简洁一些呢? 这是许多Java界的开发者提出的问题,答案就是ORM(Object Relational Mapping)框架, 比较有代表性的就是MyBatis、Hibernate。
这是在减少模型层的代码量,但是目前整合ORM框架、连接池还是编码,整合的优劣受制于编写代码人的水平。现在我们引入的框架之后,我们的引入的框架之间的关系就变成了就变成了下面这样:

看起来似乎结构很整齐的样子,但并不是我们想要的目标,因为通常一个WEB工程并不止会引入一个框架,我们会陆陆续续引入其他框架,比如定时任务框架,日志框架,监控框架,权限控制框架。像下面这样:

混乱而无序,也许会为每个框架准备一个工具类,封装常用的方法。但是这仍不是我们想要的目标,因为代码不是一成不变的,比如ORM框架,可能你在和数据库连接池整合的时候是一个版本,但是后续发现要升级,这个时候如果你是硬编码的话,你就得该代码,可能还需要思索一番 ,那么我们能不能不硬编码呢?统一管理这些框架之间的依赖关系,做到可配置化。

这也就是工厂模式,Spring的前身,当然Spring不只是工厂模式。

如何取对象 - IOC与DI绪论

在Java的世界里,我们获得对象的方式主要是new,对于简单的对象来说,这一点问题都没有。但是有些对象创建起来,比较复杂,我们希望能够隐藏这些细节。注意强调一遍,我们希望对于对象的使用者来说,隐藏这些细节。
这是框架采用工厂模式的一个主要目的,我们来看一下MyBatis中的工厂:

我们通过SqlSessionFactory对象很容易就拿到了SqlSession对象,但是SqlSession对象的就比较复杂,我们来看下源码:


这是工厂模式的一种典型的应用场景。
Java的世界是对象的世界,我们关注的问题很多时候都是在创建对象、使用对象上。假如你想用一个HashMap,大可不必用上设计模式,直接new就可以了。但是不是所有的对象都像HashMap那样,简单。在我对设计模式和Spring框架还不是很了解之前,没用Spring系的框架,在Service层调用Dao层的时候,还是通过new,每个Service的方法调用Dao,都是通过new,事实上在这个Service类,同样类的对象可以只用一个的,也就是单例模式。不懂什么叫单例模式的,可以参看我这篇博客:
今天我们来聊聊单例模式和枚举

那么这个时候我们的愿景是什么呢? 在面向对象的世界里,我们主要的关注的就是取(创建)对象和存对象,就像数据结构一样,我们主要关注的两点是如何存进去,如何取出来。创建的对象也有着不同的场景,上面我们提到的有些对象创建起来比较复杂,我们希望对对象的使用者屏蔽掉这类细节。这类场景在Java中是很常见的,比如线程池的创建:

虽然我们并不推荐直接使用这种方式创建线程池。那么还有一种情况就是扩展性设计,比如说刚开始我定义了一种对象,将该对象对应的操作方法放置到了一个接口中,这也就是面向对象的设计理念,做什么用接口,是什么用对象。但是随着发展,原来的对象已经无法满足我的要求了,但是这个对象附属于某一类库,原来的对象对应的现实实体还在使用,为了向前兼容,我只得又建立了一个类,之前的操作还是共同的。你可能觉得我说的有点抽象,这里举一个比较典型的例子,Java的POI库,用于操纵excel,使用频率是很高的,excel是有几种不同的版本的,Excel 2003文件后缀为.xls, Excel 2007及以上版本文件后缀为.xlsx。excel-2003的最大行数是65536行,这个行数有些时候是无法满足需求的,在2007版突破了65536行,最大为1048576行。POI也考虑到了这个,扩展性还是比较高的,将对excel的操作抽象出来放在workbook中,专属于某个版本的操作放在对应的excel类中,像下面这样:

HSSFWorkbook对应03版,Xssfworkbook对应07版,SXSSFWorkbook为了解决读取大型excel数据内存溢出而推出的。多数情况下,我们要使用的其实是workbook中的方法,没必要还要去看看当前读取的excel是哪个版本的,这也就是WorkbookFactory的初衷,你读文件需要文件对象,你将这个文件对象交给我,我来向你返回对应的Workbook对象。

这里抽象一点的描述就是,使用该对象不再通过new,而是向工厂传递标识符,由工厂来返回对应的对象。这也就是简单工厂模式。很多讲解简单工厂模式的博客或者视频(讲Spring框架入门的或者讲设计模式的)通常会从解耦合的角度出发,我在看视频或者看博客的时候,心里就会有一个问题,你解的是谁和谁的耦合,如果是从上面的介绍的WorkbookFactory来说的话,没用这种简单工厂模式,那么我认为就是希望使用WorkBook对象的编码者和具体的Excel版本的类耦合了起来,像下面这样:

public class ExcelDemo {
    public static void main(String[] args) throws IOException {
        XSSFWorkbook xssfWorkbook = new XSSFWorkbook("");
        HSSFWorkbook hssfWorkbook = new HSSFWorkbook();
        SXSSFWorkbook sxssfWorkbook = new SXSSFWorkbook();
    }
}

我在刚开始做Excel导入的时候,就是先去根据Excel版本去找对应的类,然后做读取数据工作。在写完之后我又想了想,这样不是强制限定了用户上传的Excel版本了吗? 假设用户上传的不是这个版本,我应该给个异常提示? 这样似乎有点不友好吧,03版本的Excel也有人再用啊。我想了想,把我的代码改成了下面这样:

 public void uploadExcel(MultipartFile file) throws IOException {
        Workbook workbook = null;
        if (file.getName().endsWith(".xls")){
             workbook = new HSSFWorkbook(file.getInputStream());
        }else if (file.getName().endsWith(".xlsx")){
             workbook = new XSSFWorkbook(file.getInputStream());
        }
        // 做真正的业务处理
    }

这样我的代码就和用户上传的excel版本解除了耦合,用户上传哪个版本我都能做导入,那个时候我还不知道工厂模式,只是下意识的完成需求的时候,想让上传的Excel版本不受限制。然后某天我偶然看到了WorkbookFactory这个类,这个类解除了WorkBook对象和具体Excel版本文件对象的耦合。
用代码来演示上面介绍的工厂模式就是:

public interface   AnimalBehavior {
    /**
     * 所有动物都会睡觉
     */
    void sleep();
    /**
     * 都要吃饭
     */
    void eat();
}
public class Cat implements AnimalBehavior {
    @Override
    public void sleep() {
        System.out.println("我是猫,喜欢睡觉");
    }

    @Override
    public void eat() {
        System.out.println("我是猫,喜欢吃鱼");
    }
}
public class Dog implements  AnimalBehavior {
    @Override
    public void sleep() {
        System.out.println("我是狗,睡觉喜欢在狗窝睡");
    }

    @Override
    public void eat() {
        System.out.println("我是狗,喜欢啃骨头");
    }
}
public class AnimalBehaviorFactory {

    public static AnimalBehavior create(String name){
        AnimalBehavior animalBehavior = null;
        if ("dog".equals(name)){
            animalBehavior = new Dog();
        }else if ("cat".equals(name)){
            animalBehavior = new Cat();
        }
        return  animalBehavior;
    }
}
public class Test {
    public static void main(String[] args) {
        AnimalBehavior dog = AnimalBehaviorFactory.create("dog");
        dog.eat();
        AnimalBehavior cat = AnimalBehaviorFactory.create("cat");
        cat.eat();
    }
}

我刚开始学习简单工厂模式的时候就是有人写了类似的例子,告诉我这叫解耦合,我当时心里的想法是,这是啥呀,完全看不出来这个设计模式好在哪里啊? 这就是设计模式? 这就是解耦合? 你倒是告诉我,谁跟谁解耦合了啊? 就这? 我学了这玩意之后,完全感受不到用武之地啊? 甚至我跟别人解释简单工厂模式是这? 我都感觉不好意思。
在写了一些代码之后,代码量之后,我可以给出答案,谁和谁解耦的答案,AnimalBehavior实例的调用者和具体的动物对象(Cat、Dog)解除了耦合,只需要将某些标识传递给工厂即可,工厂向你返回对应的AnimalBehavior实例,同时可以屏蔽对应对象创建的复杂细节。真实的使用场景就是POI的WorkBookFactory。简单工厂模式我们讲的差不多了,这里UML图我也不画了,因为我觉得有同学可能还不会看UML图(后面会专门出介绍UML的文章),另一方面我觉得我讲的已经足够清晰了。

在面向对象的世界里,我们始终关心的是如何取对象,简单点,再简单点。在取对象的路上我们面临的另一个比较复杂的场景就是,对象之间有复杂的依赖关系,比如我们的ORM框架MyBatis,依赖于连接池,而连接池又依赖于对应的数据库驱动,于是创建一个SqlSession对象就是一组对象的创建和注入,自己一个的去new吗? 这台麻烦了吧? 能不能统一管理呢? 因为哪天我可能更换连接池啊? 这就是Spring IOC思路,你将对象先放入IOC容器中,配置好他们之间的依赖关系,然后IOC容器帮你new就可以了,你就直接只关心取就可以了。

还有一种情况就是你知道怎么创建一个对象,但是无法把控创建的时机,你需要把"如何创建"的代码塞给"负责什么时候创建"的代码。后者在适当的实际,就调用对应的创建对象的函数。

除此之外,在一些语言比如C++的构造函数是不允许抛出异常的,因为这将产生一个没有被完整构造的对象,从而导致对应的内存没有正确释放。在Java中,虽然语言上支持在构造函数中抛异常,但是这是一种并不推荐的做法,由于这不是本篇的主题,这里不做讨论,具体的可以参考这篇博客:

但是业务上要求在创建对象的时候,抛出一个异常,我们该如何处理,我们也可以通过工厂模式,未必是上面的简单工厂模式,但这也是工厂模式思路的一种应用。

工厂模式的本质就是对获取对象过程的抽象,所以接下来介绍的工厂方法模式和抽象工厂模式会比较抽象,我们需要慢慢的体会这两种设计模式的好处,需要一定的代码量才能体会。

工厂方法模式

工厂方法模式 Factory Method,在工厂方法模式中,核心的工厂类不再负责所有的产品的创建,而是将具体创建工作交给子类去做。该核心类称为一个抽象工厂角色,仅仅负责给出具体工厂子类必须实现的接口,而不接触哪一个产品类应当被实例化这种细节。还是上面的例子,我们就可以转变成了下面:

// 定义顶层的核心工厂类
public interface AbstractAnimalFactory {
    AnimalBehavior  getBehavior();
}
//具体的子类负责对应的创建
public class CatFactory implements  AbstractAnimalFactory {
    @Override
    public AnimalBehavior getBehavior() {
        return new Cat();
    }
}
public class DogFactory implements AbstractAnimalFactory {
    @Override
    public AnimalBehavior getBehavior() {
        return new Dog();
    }
}
public class Test {
    public static void main(String[] args) {
        AbstractAnimalFactory abstractAnimalFactory = new DogFactory();
        AnimalBehavior dog = abstractAnimalFactory.getBehavior();
        dog.eat();
    }
}

好处就是,不用硬编码,假设在上面的工厂模式中,我们要添加个猪对象,然后上面的简单工厂就得改,需要再加个判断。
我们并不希望动老代码,即使你小心翼翼的去修改,也无法做到万无一失。那么对于这种工厂方法模式来说,我们只需要在加个猪厂就行了,复合设计原则,对修改关闭,对添加开放。
再加一个猪场:

public class Pig implements  AnimalBehavior {
    @Override
    public void sleep() {
        System.out.println("猪睡了");
    }

    @Override
    public void eat() {
        System.out.println("猪啥都吃");
    }
}
public class PigFactory implements  AbstractAnimalFactory {
    @Override
    public AnimalBehavior getBehavior() {
        return new Pig();
    }
}

抽象工厂模式

工厂模式的本质就是对获取对象过程的抽象,我们再强调一遍,但是不是一种抽象就可以应付所有创建对象的场景,因为站在不同的角度,你就会看见不同的场景。
从很多博客对抽象工厂模式的实现上来说,相对于工厂方法模式,顶层的核心工厂不再只是一个方法,顶层的核心工厂会出现若干个抽象方法,也就是顶层的核心工厂所能生产的对象是一族的产品,每个子工厂生产的产品也不止有一个,这样的例子在现实世界是常见的,比如小米不仅出品手机,还出品只能家居和笔记本电脑。不仅是小米,苹果也做智能家居和电脑。

从这个角度来看的话,工厂方法模式更像是抽象工厂模式的削弱版本。我们首先我们准备两类产品,第一个是手机接口,描述了生产手机的规范,第二个是PC接口,描述了生产PC的规范。你当然也可以用其他去举例,比如高端口罩和低端口罩,但最终表达的意思是一致的。

public interface PersonalComputer {
    //知道电脑
    void makePc();
}
public interface Phone {
   // 制造手机
   void makePhone();
}

然后下面是核心工厂:

public interface AbstractEmFactory {
    PersonalComputer createPersonalComputer();
    Phone createPhone();
}

然后是对应生产产品的工厂:

public class MiEmFactory implements AbstractEmFactory {
    @Override
    public PersonalComputer createPersonalComputer() {
        return new MiPC();
    }

    @Override
    public Phone createPhone() {
        return new MiPhone();
    }
}
public class OppoEmFactory implements AbstractEmFactory {
    @Override
    public PersonalComputer createPersonalComputer() {
        return new OppoPC();
    }

    @Override
    public Phone createPhone() {
        return new OppoPhone();
    }
}

测试类:

public class Test {
    public static void main(String[] args) {
        OppoEmFactory oppoEmFactory = new OppoEmFactory();
        PersonalComputer personalComputer = oppoEmFactory.createPersonalComputer();
        personalComputer.makePc();
        Phone oppoPhone = oppoEmFactory.createPhone();
        oppoPhone.makePhone();
    }
}

相对于工厂方法模式来说,抽象工厂方法模式的优点是不必没多一类产品,我就来个工厂去制造它,我们可以根据特点将他们归纳成一族,这样的话也减少了工厂子类,更容易维护。

总结一下

工厂模式并不是一个独立的设计模式,而是三种功能接近的设计模式的统称,这三种设计模式分别是简单工厂模式、工厂方法模式、抽象工厂模式。事实上在《设计模式之禅》这本书分了两章节讲工厂模式,并没有将我们上面讲的简单工厂模式单独拎出来讲,而是将我们上面提到的简单工厂模式算在工厂方法模式中了。但是往上大部分资料都是将工厂模式分为三种:

  • 简单/静态工厂模式
  • 工厂方法模式
  • 抽象工厂模式

在《Effective Java》中作者提倡使用静态工厂模式代替构造器,为什么这种提倡呢,这种提倡是建立在你有多个构造函数的前提下的,我们知道构造函数我们是没有办法改名的,我们能不能通过见名知义原则向调用该对象的人暴露更多的信息呢?
也就是告诉调用者,此时产生的是什么对象。这是静态工厂模式的另一种使用场景。

我们再来审视以下: 简单工厂模式、工厂方法模式、抽象工厂模式。共同的作用还是希望调用者能够更方便的拿到需要使用的对象,也就是解除耦合,同时也方便集中管理。我们希望的是使用对象的人尽可能简单的获取到想要使用的对象,而不是去直接尝试去寻找这个类,然后用构造函数去产生,在面向对象的世界,遍地都是对象,尽可能的归纳对象,收拢对象,这是工厂模式要解决的问题。

简单工厂模式根据调用者传递的标识符来向调用者返回对应的对象,解除了调用者与实际类的耦合,但并不符合开闭原则(对修改关闭,对添加开放)。假设我再要多一类,那么简单工厂模式就可能需要再多一个判断,优点是相对于工厂方法模式来说,我们就不需要知道对应的工厂了。但我们切记不要生搬硬套,只是单纯的使用某一类模式,要根据情况去组合使用。
工厂方法模式复合开闭原则,但是对于调用者来说要去尝试了解对应的工厂,才能产生对应的对象。那么随着产品的增多,这种只能生产一种产品的工厂很快就不能在满足我们的需要,因为每种产品都需要一个厂子,这么多的厂子对于维护的人来说也是一个负担, 我们根据产品的特点将他们划分为一族,抽象工厂生产的就是一族的产品。

没有完美的设计模式,没有哪种设计模式适应所有的状况,我们需要根据实际情况去选择对应的模式,但是也可以在实践中对对应的设计模式加以改造,以便达到最佳的解耦合效果。比如WorkbookFactory这个类也不是一开始就添加到了POI中,也是在4.0版本引入的。

不管你用什么语言,创建什么资源。当你开始为“创建”本身写代码的时候,就是在使用"工厂模式"了。

参考资料:


北冥有只鱼
147 声望35 粉丝