Preface
创建型模式(
Creational Pattern
)对类的实例化过程进行了抽象, 能够将软件模块中对象的创建和对象的使用分离. 为了使软件的结构更加清晰, 外界对于这些对象只需要知道它们共同的接口, 而不清楚其具体的实现细节, 使整个系统的设计更加符合单一职责原则.创建型模式在创建什么(What), 由谁创建(Who), 何时创建(When)等方面都为软件设计者提供了尽可能大的灵活性. 创建型模式隐藏了类的实例的创建细节, 通过隐藏对象如何被创建和组合在一起达到使整个系统独立的目的.
Singleton Pattern
单例模式可以说简单, 也可以说不简单. . . 使用不当就是小学生了
这么说吧, 一个城市只能有一个市长, 每当需要他的时候他总会出现, 并且每次都是同一个人. 总不能一个城市有两个市长吧?
那么单例模式就是保证一个类只有一个实例化对象, 并提供一个全局访问入口.
本质就是控制实例的数量.
小学生式单例模式
现在来看一个最原始的单例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22package com.yangbingdong.singleton;
/**
* @author ybd
* @date 17-10-16
*
* 小学生式单例模式
*/
public class SimpleSingleton {
private static SimpleSingleton instance;
private SimpleSingleton() {}
public static SimpleSingleton getInstance() {
if (instance == null) {
instance = new SimpleSingleton();
}
return instance;
}
}
如果是刚入门的程序猿这可以得到101分(多一份骄傲)
但若是已出来工作的写出这样的代码. . . 那是找群殴. . .
单例模式中注重的是单字, 上面代码有可能造成多个实例, 来一段多线程测试代码: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
50package com.yangbingdong.singleton;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import static java.util.Collections.synchronizedSet;
import static java.util.concurrent.Executors.newCachedThreadPool;
/**
* @author ybd
* @date 17-10-18
*/
public class SingletonTest {
private static final int NUM = 1000;
public static void main(String[] args) throws Exception {
ExecutorService executorService = newCachedThreadPool();
try {
CyclicBarrier cyclicBarrier = new CyclicBarrier(NUM);
CountDownLatch countDownLatch = new CountDownLatch(NUM);
Set<String> set = synchronizedSet(new HashSet<String>());
for (int i = 0; i < NUM; i++) {
executorService.execute(() -> {
try {
/* 阻塞并等待所有线程加载完毕再同时run */
cyclicBarrier.await();
SimpleSingleton singleton = SimpleSingleton.getInstance();
set.add(singleton.toString());
} catch (Exception e) {
e.printStackTrace();
} finally {
/* 计数器用于阻塞主线程 */
countDownLatch.countDown();
}
});
}
/* 阻塞主线程, 等待所有线程跑完再执行下面 */
countDownLatch.await();
System.out.println("------并发情况下我们取到的实例------");
set.forEach(System.out::println);
} catch (Exception e) {
e.printStackTrace();
} finally {
executorService.shutdown();
}
}
}
执行结果:
1 | ------并发情况下我们取到的实例------ |
很明显的产生了多个实例, 三个线程同时通过了instance == null
条件.
饿汉式
1 | package com.yangbingdong.singleton; |
通过类加载保证了线程安全, 空间换时间.
懒汉式
1 | package com.yangbingdong.singleton; |
每一次获取实例都需要同步, 性能极差, 不可取.
双重检查加锁
1 | package com.yangbingdong.singleton; |
double check模式需要在给instance
加上volatile
关键字, 作用是当线程中的变量发生变化时, 会强制写回主存, 其他线程发现主存的变量地址发生改变, 也会强制读取主存的变量.
如果不加volatile
关键字, 则有可能出现这样的情况:
线程A、B同时进入并通过了第一个if (instance == null)
, 然后A获取了锁, A把instance
实例化并释放锁, B获取锁, 但此时B自己的内存里的instance
还是空(因为没有强制读取主存并不知道instance
已经被实例化了), 所以又实例化了一个对象. . .
Lazy initialization holder class模式
1 | package com.yangbingdong.singleton; |
在JVM进行类加载的时候会保证数据是同步的, 我们采用内部类实现: 在内部类里面去创建对象实例.
只要应用中不使用内部类 JVM 就不会去加载这个单例类, 也就不会创建单例对象, 从而实现延迟加载和线程安全.
枚举
1 | package com.yangbingdong.singleton; |
枚举天生就是一个单例, 并且是线程安全的, 自由序列化的, 这意味着反序列化之后它还是原来的那个单例.
而其他的单例模式需要定义readResolve()
方法, 反序列化的时候会调用此方法:
1 | private Object readResolve() { |
Java标准库中的单例模式
java.lang.Runtime#getRuntime()
就是一个典型的代表.1
2
3
4
5
6
7
8
9
10
11class Runtime {
private static Runtime currentRuntime = new Runtime();
public static Runtime getRuntime() {
return currentRuntime;
}
private Runtime() {}
//...
}
这个currentRuntime
就是在初始化就已经加载了的.
Simple Factory Pattern
简单工厂模式又被称为静态工厂方法模式, 由一个工厂类根据传入的参数, 动态决定应该创建哪一个产品类(这些产品类继承自一个父类或接口)的实例.
将“类实例化的操作”与“使用对象的操作”分开, 让使用者不用知道具体参数就可以实例化出所需要的“产品”类, 从而避免了在客户端代码中显式指定, 实现了解耦;
即使用者可直接消费产品而不需要知道其生产的细节
代码
定义人类接口:1
2
3
4
5
6public interface Human {
/**
* 是个人都会讲话
*/
void talk();
}
实现类(男人和女人):1
2
3
4
5
6
7
8
9
10
11
12
13public class Man implements Human {
public void talk() {
System.out.println("I'm man! \n");
}
}
public class Woman implements Human {
public void talk() {
System.out.println("I'm woman! \n");
}
}
工厂类: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
36package com.yangbingdong.simplefactory;
import static com.yangbingdong.simplefactory.HumanFactory.HumanEnum.MAN;
import static com.yangbingdong.simplefactory.HumanFactory.HumanEnum.WOMAN;
/**
* @author ybd
* @date 17-10-19.
*/
public class HumanFactory {
private HumanFactory() {}
/**
* 工厂获取实例静态方法
* @param humanEnum 根据传进来的枚举获取对应的实例
* @return 返回的实例
*/
public static Human getInstance(HumanEnum humanEnum) {
if (MAN.equals(humanEnum)) {
System.out.println("生产了男人");
return new Man();
}else if (WOMAN.equals(humanEnum)) {
System.out.println("生产了女人");
return new Woman();
}else {
System.out.println("什么都没有生产");
return null;
}
}
public enum HumanEnum {
MAN,WOMAN
}
}
现在来测试一下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23package com.yangbingdong.simplefactory;
import java.util.Optional;
import static com.yangbingdong.simplefactory.HumanFactory.HumanEnum.*;
/**
* @author ybd
* @date 17-10-19.
*/
public class SimpleFactoryTest {
public static void main(String[] args) {
invokeTalkIfNotNull(MAN);
invokeTalkIfNotNull(WOMAN);
invokeTalkIfNotNull(null);
}
private static void invokeTalkIfNotNull(HumanFactory.HumanEnum man) {
Optional.ofNullable(HumanFactory.getInstance(man)).ifPresent(Human::talk);
}
}
运行结果:1
2
3
4
5
6
7生产了男人
I'm man!
生产了女人
I'm woman!
什么都没有生产
特点
将创建实例的工作与使用实例的工作分开, 使用者不必关心类对象如何创建, 只需要传入工厂需要的参数即可, 但也有弊端: 工厂类集中了所有实例(产品)的创建逻辑, 一旦这个工厂不能正常工作, 整个系统都会受到影响, 违背“开放 - 关闭原则”, 一旦添加新产品就不得不修改工厂类的逻辑, 这样就会造成工厂逻辑过于复杂, 对于系统维护和扩展不够友好.
Java标准库中的简单工厂模式
1 | java.util.Calendar - getInstance() |
Factory Method
工厂方法模式, 又称工厂模式、多态工厂模式和虚拟构造器模式, 通过定义工厂父类负责定义创建对象的公共接口, 而子类则负责生成具体的对象. 就是一个工厂生产一个专一产品.
代码
人类接口与实现类与上面的一样
主要是把工厂抽象成了接口, 具体的人类由具体的工厂实现类创建.
工厂接口定义统一的创建人类的借口
1 | public interface HumanFactory { |
两个工厂实现类
1 | public class ManFactory implements HumanFactory { |
测试类:
1 | package com.yangbingdong.factorymethod; |
运行结果:
1 | 生产了男人 |
特点
工厂方法模式把具体产品的创建推迟到工厂类的子类(具体工厂)中, 此时工厂类不再负责所有产品的创建, 而只是给出具体工厂必须实现的接口, 这样工厂方法模式在添加新产品的时候就不修改工厂类逻辑而是添加新的工厂子类, 符合开放封闭原则, 克服了简单工厂模式中缺点. 工厂模式可以说是简单工厂模式的进一步抽象和拓展, 在保留了简单工厂的封装优点的同时, 让扩展变得简单, 让继承变得可行, 增加了多态性的体现.
同时缺点也很明显, 多一个产品就多一个工厂, 开销变大了, 不适用与创建多种产品.
Java中的工厂方法
查找了一下, 数据库链接驱动就是一个典型的工厂方法模式, Java定义链接数据库以及其他操作的接口, 数据库厂商必须实现这些接口, 比如Mysql、Oracle.
Abstract Factory
抽象工厂模式为创建一组对象提供了一种解决方案. 与工厂方法模式相比, 抽象工厂模式中的具体工厂不只是创建一种产品, 它负责创建一族产品. 比如AMD工厂负责生产AMD全家桶, Intel工厂负责生产Intel全家桶.
代码
首先定义CPU接口以及实现类
1 | public interface CPU { |
主板接口以及实现类
1 | public interface MainBoard { |
定义抽象工厂与实现类
1 | public interface AbstractFactory { |
测试类
1 | public class AbstractFactoryTest { |
运行结果
1 | 生产了AMD的CPU |
优缺点
优点:
分离接口和实现: 客户端使用抽象工厂来创建需要的对象, 而客户端根本就不知道具体的实现是谁, 客户端只是面向产品的接口编程而已. 也就是说, 客户端从具体的产品实现中解耦.
使切换产品族变得容易: 因为一个具体的工厂实现代表的是一个产品族, 比如上面例子的从Intel系列到AMD系列只需要切换一下具体工厂.
缺点:
不太容易扩展新的产品: 如果需要给整个产品族添加一个新的产品, 那么就需要修改抽象工厂, 这样就会导致修改所有的工厂实现类
Java标准类库中的抽象工厂模式
1 | package java.util; |
Builder
将一个复杂对象的构建与它的表示分离, 使得同样的构建过程可以创建不同的表示.
使用Lombok中的@Builder
可以很简单地实现Builder模式, @Accessors(chain = true)
也可以实现类似的模式.
代码
1 | public class Summoner { |
优缺点
优点:
- 封装性很好: 使用建造者模式可以有效的封装变化, 在使用建造者模式的场景中, 一般产品类和建造者类是比较稳定的, 因此, 将主要的业务逻辑封装在导演类中对整体而言可以取得比较好的稳定性.
- 扩展性很好: 建造者模式很容易进行扩展. 如果有新的需求, 通过实现一个新的建造者类就可以完成, 基本上不用修改之前已经测试通过的代码, 因此也就不会对原有功能引入风险.
- 可以有效控制细节风险: 由于具体的建造者是独立的, 因此可以对建造者过程逐步细化, 而不对其他的模块产生任何影响.
缺点:
- 建造者模式所创建的产品一般具有较多的共同点, 其组成部分相似, 如果产品之间的差异性很大, 则不适合使用建造者模式, 因此其使用范围受到一定的限制.
- 如果产品的内部变化复杂, 可能会导致需要定义很多具体建造者类来实现这种变化, 导致系统变得很庞大.
Java类库中的建造者模式
1 | StringBuilder strBuilder= new StringBuilder(); |
Prototype
原型模式是23GOF
模式的一种, 其特点就是通过克隆/拷贝的方式来, 节约创建成本和资源, 被拷贝`的对象模型就称之为原型.
JAVA中对原型模式提供了良好的支持, 我们只需要实现Cloneable
接口即可, 它的目的就是将对象标记为可被复制.
一般的应用场景是, 对象的创建非常复杂, 可以使用原型模式快捷的创建对象;或在运行过程中不知道对象的具体类型, 可使用原型模式创建一个相同类型的对象, 或者在运行过程中动态的获取到一个对象的状态.
优缺点
优点:
- 由于clone方法是由虚拟机直接复制内存块执行, 所以在速度上比使用new的方式创建对象要快.
- 可以基于原型, 快速的创建一个对象, 而无需知道创建的细节.
- 可以在运行时动态的获取对象的类型以及状态, 从而创建一个对象.
缺点:
- 需要实现
Cloneable
接口,clone
位于内部, 不易扩展, 容易违背开闭原则
(程序扩展,不应该修改原有代码). - 默认的
clone
只是浅克隆, 深度克隆需要额外编码比如: 统一实现Cloneable
接口, 或者序列化方式.
代码就不贴了, 实际开发中估计用不到, 但有一句话说得很对: 存在即合理.
原型模式一般伴随这工厂模式, 代码可参考: https://github.com/masteranthoneyd/design-pattern-java/tree/master/creational/src/com/yangbingdong/prototype