创建型设计模式
创建型设计模式提供了一种在创建对象的同时隐藏创建逻辑的方式,而不是使用 new 运算符直接实例化对象。这使得程序在判断针对某个给定实例需要创建哪些对象时更加灵活。
它分为以下几个设计模式:
工厂模式
简单工厂模式
简单工厂模式(Simple Factory Pattern):定义一个工厂类,它可以根据参数的不同返回不同类的实例,被创建的实例通常都具有共同的父类。因为在简单工厂模式中用于创建实例的方法是静态(static)方法,因此简单工厂模式又被称为静态工厂方法(Static Factory Method)模式,它属于类创建型模式。
实现流程
首先将需要创建的各种不同对象(例如各种不同的Component对象)的相关代码封装到不同的类中,这些类称为具体产品类,而将它们公共的代码进行抽象和提取后封装在一个抽象产品类中,每一个具体产品类都是抽象产品类的子类;然后提供一个工厂类用于创建各种产品,在工厂类中提供一个创建产品的工厂方法,该方法可以根据所传入的参数不同创建不同的具体产品对象;客户端只需调用工厂类的工厂方法并传入相应的参数即可得到一个产品对象。
要点
当你需要什么,只需要传入一个正确的参数,就可以获取你所需要的对象,而无须知道其创建细节。
简单工厂包含的几个角色:
- Factory(工厂角色):工厂角色即工厂类,它是简单工厂模式的核心,负责实现创建所有产品实例的内部逻辑;工厂类可以被外界直接调用,创建所需的产品对象;在工厂类中提供了静态的工厂方法factoryMethod(),它的返回类型为抽象产品类型Product。
- Product(抽象产品角色):它是工厂类所创建的所有对象的父类,封装了各种产品对象的公有方法,它的引入将提高系统的灵活性,使得在工厂类中只需定义一个通用的工厂方法,因为所有创建的具体产品对象都是其子类对象。
- ConcreteProduct(具体产品角色):它是简单工厂模式的创建目标,所有被创建的对象都充当这个角色的某个具体类的实例。每一个具体产品角色都继承了抽象产品角色,需要实现在抽象产品中声明的抽象方法。
在简单工厂模式中,客户端通过工厂类来创建一个产品类的实例,而无须直接使用new关键字来创建对象,它是工厂模式家族中最简单的一员。举例
以组件的创建为例,假设我们需要创建Button和CheckBox两种类型的组件,代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58public interface Component {
void show();
}
public class Button implements Component {
public void show() {
System.out.println("显示了一个按钮!");
}
}
public class Checkbox implements Component {
public void show() {
System.out.println("显示了一个复选框!");
}
}
/**
* 这里提供了两种获取方式,其中方式一不符合开闭原则,每次加新类型的组件都需要修改,方式2则不用(也可换成读取配置类的方式)
*/
public class ComponentFactory {
/**
* 获取组件方式 1
*
* @param type
* @return
*/
public static Component getComponent(String type) {
if ("button".equalsIgnoreCase(type)) {
return new Button();
} else if ("checkbox".equalsIgnoreCase(type)) {
return new Checkbox();
}
return null;
}
/**
* 获取组件方式 2
*
* @param clazz
* @return
*/
public static Component getComponent(Class<? extends Component> clazz) {
try {
return clazz.newInstance();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return null;
}
}
方法调用:
1 | // 调用 |
输出:
1 | 显示了一个按钮! |
上面提供了两种获取组件的方法,其中方法一在每次新增Component组件时都需要修改工厂里的getComponent代码,不符合开闭原则,我们可以用方法二或者读取配置文件的方式来一定程度上的规避这个问题。
总结
简单工厂模式提供了专门的工厂类用于创建对象,将对象的创建和对象的使用分离开,它作为一种最简单的工厂模式在软件开发中得到了较为广泛的应用。
主要优点
工厂类包含必要的判断逻辑,可以决定在什么时候创建哪一个产品类的实例,客户端可以免除直接创建产品对象的职责,而仅仅“消费”产品,简单工厂模式实现了对象创建和使用的分离。
客户端无须知道所创建的具体产品类的类名,只需要知道具体产品类所对应的参数即可,对于一些复杂的类名,通过简单工厂模式可以在一定程度减少使用者的记忆量。
通过引入配置文件,可以在不修改任何客户端代码的情况下更换和增加新的具体产品类,在一定程度上提高了系统的灵活性。
主要缺点
由于工厂类集中了所有产品的创建逻辑,职责过重,一旦不能正常工作,整个系统都要受到影响。
使用简单工厂模式势必会增加系统中类的个数(引入了新的工厂类),增加了系统的复杂度和理解难度。
系统扩展困难,一旦添加新产品就不得不修改工厂逻辑,在产品类型较多时,有可能造成工厂逻辑过于复杂,不利于系统的扩展和维护。
简单工厂模式由于使用了静态工厂方法,造成工厂角色无法形成基于继承的等级结构。
适用场景
工厂类负责创建的对象比较少,由于创建的对象较少,不会造成工厂方法中的业务逻辑太过复杂。
客户端只知道传入工厂类的参数,对于如何创建对象并不关心时。
工厂模式
简单工厂模式虽然简单,但存在一个很严重的问题。当系统中需要引入新产品时,由于静态工厂方法通过所传入参数的不同来创建不同的产品,这必定要修改工厂类的源代码,将违背“开闭原则”,如何实现增加新产品而不影响已有代码?
并且在简单工厂模式中,所有的产品都由同一个工厂创建,工厂类职责较重,业务逻辑较为复杂,具体产品与工厂类之间的耦合度高,严重影响了系统的灵活性和扩展性,而工厂方法模式则可以很好地解决这些问题。
工厂方法模式(Factory Method Pattern):定义一个用于创建对象的接口,让子类决定将哪一个类实例化。工厂方法模式让一个类的实例化延迟到其子类。工厂方法模式又简称为工厂模式(Factory Pattern),又可称作虚拟构造器模式(Virtual Constructor Pattern)或多态工厂模式(Polymorphic Factory Pattern)。工厂方法模式是一种类创建型模式。
工厂方法模式包含的几个角色
● Product(抽象产品):它是定义产品的接口,是工厂方法模式所创建对象的超类型,也就是产品对象的公共父类。
● ConcreteProduct(具体产品):它实现了抽象产品接口,某种类型的具体产品由专门的具体工厂创建,具体工厂和具体产品之间一一对应。
● Factory(抽象工厂):在抽象工厂类中,声明了工厂方法(Factory Method),用于返回一个产品。抽象工厂是工厂方法模式的核心,所有创建对象的工厂类都必须实现该接口。
● ConcreteFactory(具体工厂):它是抽象工厂类的子类,实现了抽象工厂中定义的工厂方法,并可由客户端调用,返回一个具体产品类的实例。
与简单工厂模式相比,工厂方法模式最重要的区别是引入了抽象工厂角色,抽象工厂可以是接口,也可以是抽象类或者具体类。
举例
既然提到了工厂方法模式能解决上述简单工厂模式的问题,那么下面就用工厂方法模式来重构一下上面的代码。
1 | public interface Component { |
方法调用:
1 | public static void main(String[] args) { |
输出:
1 | 显示了一个按钮! |
总结
工厂方法模式是简单工厂模式的延伸,它继承了简单工厂模式的优点,同时还弥补了简单工厂模式的不足。工厂方法模式是使用频率最高的设计模式之一,是很多开源框架和API类库的核心模式。
主要优点
在工厂方法模式中,工厂方法用来创建客户所需要的产品,同时还向客户隐藏了哪种具体产品类将被实例化这一细节,用户只需要关心所需产品对应的工厂,无须关心创建细节,甚至无须知道具体产品类的类名。
基于工厂角色和产品角色的多态性设计是工厂方法模式的关键。它能够让工厂可以自主确定创建何种产品对象,而如何创建这个对象的细节则完全封装在具体工厂内部。工厂方法模式之所以又被称为多态工厂模式,就正是因为所有的具体工厂类都具有同一抽象父类。
使用工厂方法模式的另一个优点是在系统中加入新产品时,无须修改抽象工厂和抽象产品提供的接口,无须修改客户端,也无须修改其他的具体工厂和具体产品,而只要添加一个具体工厂和具体产品就可以了,这样,系统的可扩展性也就变得非常好,完全符合“开闭原则”。
主要缺点
在添加新产品时,需要编写新的具体产品类,而且还要提供与之对应的具体工厂类,系统中类的个数将成对增加,在一定程度上增加了系统的复杂度,有更多的类需要编译和运行,会给系统带来一些额外的开销。
由于考虑到系统的可扩展性,需要引入抽象层,在客户端代码中均使用抽象层进行定义,增加了系统的抽象性和理解难度,且在实现时可能需要用到DOM、反射等技术,增加了系统的实现难度。
适用场景
- 客户端不知道它所需要的对象的类。在工厂方法模式中,客户端不需要知道具体产品类的类名,只需要知道所对应的工厂即可,具体的产品对象由具体工厂类创建,可将具体工厂类的类名存储在配置文件或数据库中。
- 抽象工厂类通过其子类来指定创建哪个对象。在工厂方法模式中,对于抽象工厂类只需要提供一个创建产品的接口,而由其子类来确定具体要创建的对象,利用面向对象的多态性和里氏代换原则,在程序运行时,子类对象将覆盖父类对象,从而使得系统更容易扩展。
用一张图来表示就是下图的样子
抽象工厂模式
工厂方法模式通过引入工厂等级结构,解决了简单工厂模式中工厂类职责太重的问题,但由于工厂方法模式中的每个工厂只生产一类产品,可能会导致系统中存在大量的工厂类,势必会增加系统的开销。此时,我们可以考虑将一些相关的产品组成一个“产品族”,由同一个工厂来统一生产,这就是我们本文将要学习的抽象工厂模式的基本思想。
一般在有产品族的时候才会使用抽象工厂模式。这是围绕一个超级工厂创建其他工厂的设计模式,该超级工厂又称为其他工厂的工厂。
就拿refactoringguru.cn上家具模拟器的例子来说,我们买家具,肯定会有茶几和沙发等。如果我们还是使用工厂模式来设计,那就是这样的:将沙发和茶几进行抽象,然后沙发由SofaFactory 生产,茶几由CoffeeTableFactory 生产,且风格有现代(Modern) 、 维多利亚(Victorian)两种,设计如下图:
那么我们调用买家具功能的时候,就是这样的:
1 | // 得到现代风的沙发 |
那么当现代风和维多利亚风家具的风格不搭配时,就会有发生下图的情况,这样我想买家肯定会觉得很不合适。这里就牵涉到上文所说的产品族的概念了,所谓产品族,也就是组成某个产品的一系列附件的集合,比如华为系列和小米系列就是两个产品族。
当涉及到这种产品族的问题的时候,就需要抽象工厂模式来支持了。我们不再定义沙发工厂、茶几工厂、桌子工厂等等,而是直接定义家具工厂,每个家具工厂负责生产自己的所有的家具,这样能保证肯定不存在一套家具风格不搭的问题了。
这个时候,对于客户端来说,不再需要单独挑选沙发工厂、茶几工厂、桌子工厂等,直接选择一家家具工厂,家具工厂会负责生产所有的家具,而且能保证肯定风格是搭配的。代码如下:
1 | public static void main(String[] args) { |
当然,抽象工厂的问题也是显而易见的,比如我们要加个桌子,就需要修改所有的工厂,给所有的工厂都加上制造桌子的方法。这有点违反了对修改关闭,对扩展开放这个设计原则。
单例模式
单例模式(Singleton Pattern)是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。单例模式是创建型模式。单例模式在现实生活中应用也非常广泛。 例如,公司 CEO、部门经理在一个公司只能存在一个等。在 J2EE 标准中,ServletContext、 ServletContextConfig 等;在 Spring 框架应用中 ApplicationContext;数据库的连接池也都是单例形式。单例模式可分为以下几种:
饿汉式单例
饿汉式单例是在类加载的时候就立即初始化,并且创建单例对象。绝对线程安全,在线 程还没出现以前就是实例化了,不可能存在访问安全问题。
- 优点: 没有加任何的锁、执行效率比较高,在用户体验上来说,比懒汉式更好。
- 缺点: 类加载的时候就初始化,不管用与不用都占着空间,浪费了内存。
饿汉式写法简单,试用与单例对象少的情况,Spring 中 IOC 容器 ApplicationContext 本身就是典型的饿汉式单例。
- 写法一
1 | public class HungrySingleton { |
- 写法二
1 | public class HungryStaticSingleton { |
懒汉式单例
懒汉式单例在被外部类调用的时候内部类才会加载
- 懒汉式单例简单实现
缺点:不加synchronized可能会出现线程安全问题,但是加上synchronized关键字以后,在线程数量比较多情况下,如果 CPU 分配压力上升,会导致大批量线程出现阻塞,从而导致程序运行性能大幅下降。
1 | public class SimpleLazySingleton { |
- 双重校验锁实现饿汉式单例
比上一个方法稍微好点,但是因为加了锁,性能仍有问题。
1 | public class DoubleCheckLazySingleton { |
- 静态内部类实现饿汉式单例
这种形式兼顾饿汉式的内存浪费,也兼顾 synchronized 性能问题,
1 | public class InnerClassLazySingleton { |
这种形式兼顾饿汉式的内存浪费,也兼顾 synchronized 性能问题。内部类一定是要在方 法调用之前初始化,巧妙地避免了线程安全问题。
单例被破坏
反射破坏单例
上述所有的单例模式的构造方法除了加上 private 以外,没有做任何处 理。如果我们使用反射来调用其构造方法,然后,再调用 getInstance()方法,应该就会两个不同的实例。
- 测试
1 | public class InnerClassSingletonTest { |
- 运行结果
1 | com.design.pattern.singleton.lazy.InnerClassLazySingleton@776ec8df |
此时你会发现两次对象并不是同一个,也就是单例被破坏了,此时,为了避免这种情况,我们需要在构造方法中做一下限制,仍以静态内部类演示,代码如下:
1 | public class InnerClassLazySingleton { |
再次运行测试类,则出现以下异常:
序列化破坏单例
当我们将一个单例对象创建好,有时候需要将对象序列化然后写入到磁盘,下次使用时 再从磁盘中读取到对象,反序列化转化为内存对象。反序列化后的对象会重新分配内存, 即重新创建。那如果序列化的目标的对象为单例对象,就违背了单例模式的初衷,相当于破坏了单例。来看一段演示:
1 | public class SerializableSingleton implements Serializable { |
- 测试代码
1 | public class SerializableSingletonTest { |
- 运行结果
1 | com.design.pattern.singleton.seriable.SerializableSingleton@306a30c7 |
运行结果中,可以看出,反序列化后的对象和手动创建的对象是不一致的,实例化了两 次,违背了单例的设计初衷。那么,我们如何保证序列化的情况下也能够实现单例?其实很简单,只需要增加**readResolve()**方法即可。修改后的代码
1 | public class SerializableSingleton implements Serializable { |
再次运行测试类,得出如下结果:
1 | com.design.pattern.singleton.seriable.SerializableSingleton@6d311334 |
此时我们发现反序列化后的对象和手动创建的对象是同一个对象(源码分析待补充)。虽然,增加readResolve()方法返回实例,解决了单例被破坏的问题。但是,我们通过分析源码以及调试,我们可以看到实际上实例化了两次,只不过新创建的对象没有被返回而已。那如果,创建对象的动作发生频率增大,就意味着内存分配开销也就随之增大,难道真的就没办法从根本上解决问题吗?下面我们来注册式单例也许能帮助到你。
注册式单例
注册式单例又称为登记式单例,就是将每一个实例都登记到某一个地方,使用唯一的标 识获取实例。注册式单例有两种写法:一种为容器缓存,一种为枚举登记。
- 枚举式单例
1 | public enum EnumSingleton { |
此时用反序列化方式再进行测试
1 | public class EnumSingletonTest { |
- 运行结果
1 | java.lang.Object@b81eda8 |
通过结果可以发现枚举式单例能完美避开反序列化带来的问题。
我们再用反射来进行测试:
1 | public class EnumSingletonTest { |
则会得到如下异常:
报的是 java.lang.NoSuchMethodException 异常,意思是没找到无参的构造方法。打开JDK中的Enum类,我们只看到了一个这样的构造方法:
1 | protected Enum(String name, int ordinal) { |
那我们再来做一个这样的测试:
1 | public static void main(String[] args) { |
运行结果如下:
这时错误已经非常明显了,告诉我们 Cannot reflectively create enum objects,不能 用反射来创建枚举类型。我们进入Constructor中的neInstance()方法:
1 |
|
在 newInstance()方法中做了强制性的判断,如果修饰符是 Modifier.ENUM 枚举类型, 直接抛出异常。
- 容器缓存式单例
容器式写法适用于创建实例非常多的情况,便于管理。但是,是非线程安全的
- 代码
1 | public class ContainerSingleton { |
ThreadLocal 线程单例
ThreadLocal 不能保证其创建的对象是全局唯一,但是能保证在单个线程中是唯一的,天生的线程安全。
1 | /** |
- 简单测试代码
1 | public class ThreadLocalSingletonTest { |
- 运行结果
1 | com.design.pattern.singleton.threadlocal.ThreadLocalSingleton@24d46ca6 |
通过运行结果可以发现,在主线程 main中无论调用多少次,获取到的实例都是同一个,都在两个子线程中也都分别获取到了不同的实例。那么 ThreadLocal 是如果实现这样的效果的呢?我们知道上面的单例模式为了达到线程安全的目的,给方法上锁,以时间换空间。ThreadLocal 将所有的对象全部放在 ThreadLocalMap 中,为每个线程都提供一个对象,实际上是以 空间换时间来实现线程间隔离的。
建造者模式
建造者模式(Builder Pattern)使用多个简单的对象一步一步构建成一个复杂的对象。它的意图就是将一个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示。生活中这样的例子很多,如游戏中的不同角色,其性别、个性、能力、脸型、体型、服装、发型等特性都有所差异;还有汽车中的方向盘、发动机、车架、轮胎等部件也多种多样。
建造者模式一步一步创建一个复杂的对象,它允许用户只通过指定复杂对象的类型和内容就可以构建它们,用户不需要知道内部的具体构建细节。
建造者模式包含的几个角色
- Builder(抽象建造者):它为创建一个产品Product对象的各个部件指定抽象接口,在该接口中一般声明两类方法,一类方法是buildPartX(),它们用于创建复杂对象的各个部件;另一类方法是getResult(),它们用于返回复杂对象。Builder既可以是抽象类,也可以是接口。
- ConcreteBuilder(具体建造者):它实现了Builder接口,实现各个部件的具体构造和装配方法,定义并明确它所创建的复杂对象,也可以提供一个方法返回创建好的复杂产品对象。
- Product(产品角色):它是被构建的复杂对象,包含多个组成部件,具体建造者创建该产品的内部表示并定义它的装配过程。
- Director(指挥者):指挥者又称为导演类,它负责安排复杂对象的建造次序,指挥者与抽象建造者之间存在关联关系,可以在其construct()建造方法中调用建造者对象的部件构造与装配方法,完成复杂对象的建造。客户端一般只需要与指挥者进行交互,在客户端确定具体建造者的类型,并实例化具体建造者对象(也可以通过配置文件和反射机制),然后通过指挥者类的构造函数或者Setter方法将该对象传入指挥者类中。
原型模式
使用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。原型模式是一种对象创建型模式。
原型模式包含的几个角色
Prototype(抽象原型类):它是声明克隆方法的接口,是所有具体原型类的公共父类,可以是抽象类也可以是接口,甚至还可以是具体实现类。
ConcretePrototype(具体原型类):它实现在抽象原型类中声明的克隆方法,在克隆方法中返回自己的一个克隆对象。
Client(客户类):让一个原型对象克隆自身从而创建一个新的对象,在客户类中只需要直接实例化或通过工厂方法等方式创建一个原型对象,再通过调用该对象的克隆方法即可得到多个相同的对象。由于客户类针对抽象原型类Prototype编程,因此用户可以根据需要选择具体原型类,系统具有较好的可扩展性,增加或更换具体原型类都很方便。