之前用6篇Blog篇幅的时间对创建型模式进行了学习,这里对创建型模式进行一个小结。之前的6篇Blog都贴到这里:
创建型模式文章列表
创建型模式主要解决对象的创建问题,封装复杂的创建过程,解耦对象的创建代码和使用代码
- 单例模式用来创建全局唯一的对象。
- 工厂模式用来创建不同但是相关类型的对象(继承同一父类或者接口的一组子类),由给定的参数来决定创建哪种类型的对象。
- 建造者模式是用来创建复杂对象,可以通过设置不同的可选参数,定制化地创建不同的对象。
- 原型模式针对创建成本比较大的对象,利用对已有对象进行复制的方式进行创建,以达到节省创建时间的目的
其中工厂模式又分为:简单工厂模式、工厂方法模式、抽象工厂模式。
创建型模式使用频次
依据是否常用来划分,我觉得单例模式、简单工厂模式、建造者模式比较常用。工厂方法模式、抽象工厂模式以及原型模式很少用到,至少我目前还没有在项目中接触过。
创建型模式重点回顾
下面回顾下创建型模式
1 单例模式
单例模式用来创建全局唯一的对象。一个类只允许创建一个对象(或者叫实例),那这个类就是一个单例类,这种设计模式就叫作单例模式。
单例有几种经典的实现方式,它们分别是:饿汉式、懒汉式、双重检测、静态内部类、枚举。尽管单例是一个很常用的设计模式,在实际的开发中,我们也确实经常用到它,但是,有些人认为单例是一种反模式(anti-pattern),并不推荐使用,主要的理由有以下几点:
- 单例对 OOP 特性的支持不友好;单例会隐藏类之间的依赖关系;单例对代码的扩展性不友好;
- 单例对代码的可测试性不友好;单例不支持有参数的构造函数;
那有什么替代单例的解决方案呢?如果要完全解决这些问题,我们可能要从根上寻找其他方式来实现全局唯一类。比如,通过工厂模式、IOC 容器来保证全局唯一性。
如果单例类并没有后续扩展的需求,并且不依赖外部系统,那设计成单例类就没有太大问题。对于一些全局类,我们在其他地方 new 的话,还要在类之间传来传去,不如直接做成单例类,使用起来简洁方便。
2 工厂模式
工厂模式用来创建不同但是相关类型的对象(继承同一父类或者接口的一组子类),由给定的参数来决定创建哪种类型的对象。实际上,如果创建对象的逻辑并不复杂,那我们直接通过 new 来创建对象就可以了,不需要使用工厂模式。当创建逻辑比较复杂,是一个大工程的时候,我们就考虑使用工厂模式,封装对象的创建过程,将对象的创建和使用相分离
工厂模式包括简单工厂、工厂方法、抽象工厂这 3 种细分模式。其中,简单工厂和工厂方法比较常用,抽象工厂的应用场景比较特殊,所以很少用到。当每个对象的创建逻辑都比较简单的时候,推荐使用简单工厂模式,将多个对象的创建逻辑放到一个工厂类中
当每个对象的创建逻辑都比较复杂的时候,为了避免设计一个过于庞大的工厂类,推荐使用工厂方法模式,将创建逻辑拆分得更细,每个对象的创建逻辑独立到各自的工厂类中。
当创建一个产品族的时候,推荐使用抽象工厂:
详细点说,工厂模式的作用有下面 4 个,这也是判断要不要使用工厂模式最本质的参考标准。
- 封装变化:创建逻辑有可能变化,封装成工厂类之后,创建逻辑的变更对调用者透明。
- 代码复用:创建代码抽离到独立的工厂类之后可以复用。
- 隔离复杂性:封装复杂的创建逻辑,调用者无需了解如何创建对象。
- 控制复杂度:将创建代码抽离出来,让原本的函数或类职责更单一,代码更简洁。
工厂模式有一个非常经典的应用场景:依赖注入框架,比如 Spring IOC、Google Guice,它用来集中创建、组装、管理对象,跟具体业务代码解耦,让程序员聚焦在业务代码的开发上。
3 建造者模式
建造者模式用来创建复杂对象,可以通过设置不同的可选参数,定制化地创建不同的对象。建造者模式的原理和实现比较简单,重点是掌握应用场景,避免过度使用。
如果一个类中有很多属性,为了避免构造函数的参数列表过长,影响代码的可读性和易用性,我们可以通过构造函数配合 set()
方法来解决。但是,如果存在下面情况中的任意一种,我们就要考虑使用建造者模式了。
- 我们把类的必填属性放到构造函数中,强制创建对象的时候就设置。如果必填的属性有很多,把这些必填属性都放到构造函数中设置,那构造函数就又会出现参数列表很长的问题。如果我们把必填属性通过 set() 方法设置,那校验这些必填属性是否已经填写的逻辑就无处安放了。
- 如果类的属性之间有一定的依赖关系或者约束条件,我们继续使用构造函数配合 set() 方法的设计思路,那这些依赖关系或约束条件的校验逻辑就无处安放了。
- 如果我们希望创建不可变对象,也就是说,对象在创建好之后,就不能再修改内部的属性值,要实现这个功能,我们就不能在类中暴露 set() 方法。构造函数配合 set() 方法来设置属性值的方式就不适用了。
建造者模式最重要的一点就是不要滥用,其实场景没有那么多
4 原型模式
如果对象的创建成本比较大,而同一个类的不同对象之间差别不大(大部分字段都相同),在这种情况下,我们可以利用对已有对象(原型)进行复制(或者叫拷贝)的方式,来创建新对象,以达到节省创建时间的目的。这种基于原型来创建对象的方式就叫作原型模式。
原型模式有两种实现方法,深拷贝和浅拷贝。浅拷贝只会复制对象中基本数据类型数据和引用对象的内存地址,不会递归地复制引用对象,以及引用对象的引用对象。而深拷贝得到的是一份完完全全独立的对象。所以,深拷贝比起浅拷贝来说,更加耗时,更加耗内存空间。
如果要拷贝的对象是不可变对象,浅拷贝共享不可变对象是没问题的,但对于可变对象来说,浅拷贝得到的对象和原始对象会共享部分数据,就有可能出现数据被修改的风险,也就变得复杂多了。操作非常耗时的情况下,我们比较推荐使用浅拷贝,否则,没有充分的理由,不要为了一点点的性能提升而使用浅拷贝
创建型模式易混概念
在创建型模式学习过程中遇到的一些易混概念或者使用场景不易区分概念,抽离出来辨析一下。
单例模式和静态方法
单例模式和静态方法在很多情况下能发挥相同的作用,但它们也有不同点:
- 单例模式某些实现方式(懒汉式单例)支持延迟加载,但静态方法不支持延迟加载。
- 静态方法是面向过程的,而非面向对象的编程思想,单例是一个对象,能够实现接口或者继承一个父类,但类的静态方法不行。静态方法只能使用静态成员变量,单例可以动态加载一些内容,比如属性有其他类的对象(组合)。
- 单例模式是利用唯一的实例保存系统的状态,提供的实例方法也是为了对这个唯一的实例进行操作,而静态方法多是一些工具方法,Math 类中的静态方法就是一个典型的例子,如果仅仅是想不自己创建类的实例就可以调用到某些方法来完成一定的操作,那完全没必要也不应该使用单例模式
- 从生命周期上来看,静态方法的类会在代码编译的时候就被加载,静态方法中产生的对象句柄,会随着静态方法执行完毕而释放掉,对象也会在不再有引用的时候消失,而且执行类中的静态方法时,不会实例化静态方法所在的类。如果用单例模式, 产生的那一个唯一的实例,会一直在内存中,不会被GC清除的(原因是静态的属性变量不会被GC清除),除非整个应用退出了JVM
其实只要明确一点:静态方法是类方法,类方法面向过程;而单例模式是唯一实例对象,面向对象;以上的几点区别都基于此。所以静态方法一般适用于做一些Util类,例如JsonUtil,StringUtil。而单例适合做唯一实例管理器-就像简单工厂模式中提到的单例+简单工厂模式、日志处理器、自增序号生成器。而且二者在一般情况下也没有非常清晰的使用界限。
简单工厂模式与工厂方法模式
当创建逻辑比较复杂,是一个大工程的时候,我们就考虑使用工厂模式,封装对象的创建过程,将对象的创建和使用相分离。何为创建逻辑比较复杂呢?
- 第一种情况:类似规则配置解析的例子,代码中存在 if-else 分支判断,动态地根据不同的类型创建不同的对象。针对这种情况,我们就考虑使用工厂模式,将这一大坨 if-else 创建对象的代码抽离出来,放到工厂类中。
- 当每个对象的创建逻辑都比较简单的时候,推荐使用简单工厂模式,将多个对象的创建逻辑放到一个工厂类中。
- 当每个对象的创建逻辑都比较复杂的时候,为了避免设计一个过于庞大的简单工厂类,推荐使用工厂方法模式,将创建逻辑拆分得更细,每个对象的创建逻辑独立到各自的工厂类中。注意这里只是为了避免简单工厂类的庞大,利用多态创建工厂类,当个工厂类避免了分支逻辑,但整体使用时并不能避免分支逻辑,工厂方法模式也会存在分支逻辑。
- 第二种情况,尽管我们不需要根据不同的类型创建不同的对象,但是,单个对象本身的创建过程比较复杂,比如前面提到的要组合其他类对象,做各种初始化操作。在这种情况下,我们也可以考虑使用工厂模式,将对象的创建过程封装到工厂类中。当然也可以考虑使用建造者模式处理
- 对于这种情况:因为单个对象本身的创建逻辑就比较复杂,所以建议使用工厂方法模式
除了刚刚提到的这几种情况之外,如果创建对象的逻辑并不复杂,那我们就直接通过 new 来创建对象就可以了,不需要使用工厂模式。
工厂模式和建造者模式
工厂模式和建造者模式都用于将对象的创建和使用分离,都用来构建对象,看起来很相似,我们来看下他们的区别:
- 工厂模式是用来创建不同但是相关类型的对象(继承同一父类或者接口的一组子类),由给定的参数来决定创建哪种类型的对象。建造者模式是用来创建一种类型的复杂对象,通过设置不同的可选参数,定制化地创建不同的对象。也就是工厂模式是用来创建几种相似类型的对象,关注一种父类(接口类)下不同子类的创建;而建造者模式则是创建一种类型的但内部数据有不同表现形式的对象,关注一种类型不同对象的创建过程。
披萨工厂生产不同的披萨:榴莲披萨、芝士披萨,这个就是工厂模式;厨师制作的时候又会依据客人的口味进行配料调整,例如芝士披萨:多芝士、少洋葱、大号芝士,这个就是建造者模式;
总结一下
创建型模式主要解决对象的创建问题,封装复杂的创建过程,解耦对象的创建代码和使用代码。这里我们最常用的其实是单例模式和工厂模式,其实枚举这种单例项目中随处可见,而工厂模式我们也常常使用,原型模式和建造者模式应用场景则没有那么多。