原文链接

开闭原则

(1)对于扩展是开放的(Open for extension)。这意味着模块的行为是可以扩展的。当应用的需求改变时,我们可以对模块进行扩展,使其具有满足那些改变的新行为。也就是说,我们可以改变模块的功能。

(2)对于修改是关闭的(Closed for modification)。对模块行为进行扩展时,不必改动模块的源代码或者二进制代码。模块的二进制可执行版本,无论是可链接的库、DLL或者.EXE文件,都无需改动。

好处

可复用性好。

我们可以在软件完成以后,仍然可以对软件进行扩展,加入新的功能,非常灵活。因此,这个软件系统就可以通过不断地增加新的组件,来满足不断变化的需求。

可维护性好。

由于对于已有的软件系统的组件,特别是它的抽象底层不去修改,因此,我们不用担心软件系统中原有组件的稳定性,这就使变化中的软件系统有一定的稳定性和延续性。

Build模式

用来处理构建对象时有大量可选参数的问题

Buidler模式, 是一种创建型的设计模式.
通常用来将一个复杂的对象的构造过程分离, 让使用者可以根据需要选择创建过程.
另外, 当这个复杂的对象的构造包含很多可选参数时, 那Builder模式可以说是不二之选了.

工厂方法模式

工厂方法模式

几个点:
1, 为何叫工厂方法, 是因为每个工厂有一个方法来创建产品.
2, 每个产品对应一个工厂实例来生产这个产品实例.
3, 因为产品和其对应的工厂都与其他产品分离, 我们可以很轻易的去增加新的产品和其对应的工厂, 而不改变原来的结构. (开闭原则, 实际上还蕴含了职责单一)

策略模式

定义一组算法, 并将每一个单独算法封装起来, 让它们可以相互替换.

策略模式

与简单工厂和工厂方法的区别:

  1. 首先简单工厂工厂方法创建型的模式, 而策略模式行为型的模式.
  2. 所谓创建型就是说用来生产对象的, 注重的生产(new)这个部分, 用创建型的模式来代替直接new一个实例, 更多是想将直接的实例依赖通过不同的方法转化接口依赖.
  3. 所谓行为型模式更多是描述一种行为, A使用B, 怎么使用的这个关系上.

装饰者模式

装饰者模式

1
2
3
Drink doubleIceHoneyCoke = new Ice(new Ice(new Honey(new Coke())));
System.out.println(doubleIceHoneyCoke.make());
//这是一杯可乐, 加一份蜂蜜, 加一份冰, 加一份冰

装饰者模式就是用来动态的给对象加上额外的职责.
Drink是被装饰的对象, Stuff作为装饰类, 可以动态地给被装饰对象添加特征, 职责.

有的同学可能会说, 我完全可以通过继承关系, 在子类中添加职责的方式给父类以扩展啊. 是的, 没错, 继承本就是为了扩展.

然而, 装饰者模式和子类继承扩展的最大区别在于:

装饰者模式强调的是动态的扩展, 而继承关系是静态的.

由于继承机制的静态性, 我们会为每个扩展职责创建一个子类, 例如IceCoke, DoubleIceCoke, SugarXDrink, IceSugarXDrink等等…会造成类爆炸.

另外, 这里引入一条新的面向对象编程原则:
组合优于继承, 大家自行体会下.

还有的同学说, 这种按需定制的方式貌似跟之前讲的Builder模式有点像啊, 那为什么不用Builder模式呢.

这里先说明下二者的本质差异:

Builder模式是一种创建型的设计模式. 旨在解决对象的差异化构建的问题.
装饰者模式是一种结构型的设计模式. 旨在处理对象和类的组合关系.

实际上在这个例子中, 我们是可以用Builder模式的, 但就像使用继承机制一样, 会有些问题.
首先, Builder模式是构建对象, 那么实际上要求我们是必须事先了解有哪些属性/职责供选择. 这样我们才可以在构建对象时选择不同的Build方式. 也就是说:

Builder模式的差异化构建可预见的, 而装饰者模式实际上提供了一种不可预见的扩展组合关系.

代理模式

代理模式

相比于之前的关系, 这个相对简单, 就两个角色, 小光和大龙, 都实现了Person接口. 关键点在于:

  • 大龙是直接和供应商打交道的, 但是实际的决策和行为(签单)是由小光来做的.
  • 也就是说大龙是小光的代理.

这就是我们所要说的代理模式:
为其他对象(小光)提供一个代理(大龙)以控制对这个对象的访问.

细心的同学可能有发现, 这个例子的模式貌似和前文装饰模式有点类似啊. 这里大龙也相当于给小光装饰上了新的职责(谈判negotiate):

1
2
3
public void negotiate(int price) {
System.out.println("不接受, 要求降价" + (price - 80));
}

那么代理模式相比与装饰模式有什么区别呢?

让我们再带上重点符来重温下二者:

  • 代理模式旨在为一个对象提供一个代理, 来控制对这个对象的访问.
  • 装饰模式旨在为一个对象动态添加职责, 增加这个对象的行为/属性.

二者虽然都会有代理类/装饰类实际调用被代理对象/被装饰对象的行为. 然而代理模式重在控制, 而装饰模式重在添加.

上面说到大龙是有控制权的, 也就是说, 这种代理实际上是一种控制代理, 也可以称之为保护代理.

代理模式除了这种控制访问/保护性的, 常常用到的场景还有:

  • 远程代理: 为一个在不同的地址空间的对象提供局部代表, 从而可以隐藏这个被代理对象存在于不同地址空间的事实. 这个代表有点类似于大使, 故而也可以称之为”大使模式”.
  • 智能引用代理: 在代理中对被代理对象的每个操作做些额外操作, 例如记录每次被代理对象被引用, 被调用的次数等. 有点像引用计数的感觉.

抽象工厂模式

抽象工厂模式

抽象工厂
提供一个创建一系列相关或互相依赖的对象的接口, 而无需指定它们的具体实现.

简单工厂,工厂方法,抽象工厂三者对比

对比

简单工厂
实际上我们可以理解为是一种编程习惯, 将类似对象的初始化放下一个地方, 便于管理.
它提供了一个工厂(表妹), 来根据不同的指令(drinkType)来生产不同的饮料产品(橙汁, 可乐, 酸梅汤).
相对简单, 适用于要创建类似(实现同一接口的)的产品, 且产品种类不多, 扩展可能性不大的情况. 当需要增加一中饮料时, 我们需要修改工厂(表妹)的实现, 增加drinkType的对应实现.

工厂方法
顾名思义, 有一个工厂, 工厂(饮料机)里有那么一个方法(定义了一个创建对象的接口makeDrink), 可以生产产品(Drink). 由实现了这个工厂方法的类来决定具体生产出什么产品(可以是可乐, 橙汁, 奶茶等).

相比于简单工厂, 工厂方法有良好的扩展性, 当我们需要增加一种饮料时, 不需要去修改工厂, 只需扩展一个新的工厂, 实现其工厂方法, 提供新的饮料即可.

这实际上就是典型的, 通过继承/实现, 来达成了对修改关闭, 对扩展开放的效果.

另外, 从简单工厂到工厂方法, 我们也可以理解为是一次Switch Statements的重构.

对于”Switch Statements的重构”, 有兴趣的同学可以参看<<重构–改善既有代码的设计>>一书的3.10节. 那是一本好书, 2010年的时候华为的一位技术经理推荐给我的, 感谢他.

另外, 并不是我们以后遇到Switch就要想着改造, 遇到简单工厂就想着用工厂方法…还需根据实际情况取用合适的.

抽象工厂
同样, 从名字中, 我们大致能了解, 抽象工厂描述的一个抽象的工厂, 其可以生产一系列的相关的或是互相依赖的产品.

抽象工厂和工厂方法有很多类似之处, 都是创建产品, 都是通过继承/实现, 来达成了对修改关闭, 对扩展开放的效果.

然而, 抽象工厂相较于工厂方法, 它的重点, 是它解决的是一个产品族(相关的, 或是互相依赖的产品们)的创建问题, 而非仅仅是一类产品.

以本故事来说, 工厂方法是用来创建一类产品, 通过他创建出来的都是饮料. 而抽象工厂是用来创建一系列产品, 包括店铺, 收银台, 餐具等, 这些产品是相关的, 都是一个分店所需要的.

打个比方, 如果我有一个轮胎工厂, 我生产的东西都是轮胎, 只是规格不同, 我就可以使用工厂方法; 如果我是一个汽车工厂, 我生产汽车, 它需要轮胎, 车架, 发动机… 那么我就应用使用抽象工厂.

单例模式

单例模式

保证一个类(HungryForm)仅有一个实例(sInstance), 并提供一个访问该实例的全局访问点(getInstance).
这就意味着单例通常有如下两个特点:

  1. 构造函数是私有的(避免别的地方创建它)
  2. 有一个static的方法来对外提供一个该单例的实例.
1
2
3
4
5
6
7
8
9
10
11
12
13
public class HungryForm extends Form {

// 提前创建好
private static HungryForm sInstance = new HungryForm();

// 私有化的构造, 避免别人直接创建表格
private HungryForm() {}

// 店长们通过这个接口来取表格
public static HungryForm getInstance() {
return sInstance;
}
}

同学们可能注意到了, 我们在这个单例模式中使用了Hungry这个词, 没错, 我们这里实现单例的方式使用的就是饿汉式.

饿汉式单例

饿汉式单例
顾名思义, 就是很饿, 不管三七二十一先创建了一个实例放着, 而不管最终用不用.

然而, 这个单例可能最终并不需要, 如果提前就创建好, 就会浪费内存空间了.
例如, 我们这个故事中, 年底假期中, 所有店子都歇业十天, 这十天就没有任何店长会去访问这个表格, 然而小光还是都每天都创建了, 这就造成了空间浪费(假设这个表格数据(对象实例)很大…)

懒汉式单例

那么怎么办呢?
我们可以使用懒汉式单例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class LazyForm extends Form {

private static LazyForm sInstance;

// 私有化的构造, 避免别人直接创建表格
private LazyForm() {}

// 店长们通过这个接口来取表格
public static LazyForm getInstance() {

// 在有店长访问该文件时才创建, 通过判断当前文件是否存在(sInstance == null)来避免重复创建
if (sInstance == null) {
sInstance = new LazyForm();
}
return sInstance;
}
}

懒汉式单例
“懒”, 也就是现在懒得创建, 等有用户要用的时候才创建.

线程安全的懒汉式单例

但是这样创建也会有问题啊, 因为他是通过sInstance == null判断当前是否已经存在表格文件的, 假设有两个店长同时调用getInstance来取文件, 同时走到sInstance == null判断这一步, 就会出问题了 — 有可能创建了两个文件(实例), 就达不到单例的目的了.

所以说这种懒汉式是线程不安全的, 在多线程环境下, 并不能做到单例.

那么, 该如何做, 既能懒加载, 又线程安全呢?
我们都知道Java中多线程环境往往会用到synchronized关键字, 通过他来做线程并发性控制.

synchronized方法控制对类成员变量的访问, 每个类实例对应一把锁, synchronized修饰的方法必须获得调用该方法的类实例的锁方能执行, 否则所属线程阻塞. 方法一旦执行, 就独占该锁. 直到从该方法返回时才将锁释放. 此后被阻塞的线程方能获得该锁, 重新进入可执行状态.

让我们来看下线程安全的懒汉式单例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class SynchronizedLazyForm extends Form {

private static SynchronizedLazyForm sInstance;

// 私有化的构造, 避免别人直接创建表格
private SynchronizedLazyForm() {}

// 店长们通过这个接口来取表格
// 注意, 这是一个synchronized方法
// 参考https://docs.oracle.com/javase/tutorial/essential/concurrency/syncmeth.html
public static synchronized SynchronizedLazyForm getInstance() {

// 在有店长访问该文件时才创建, 通过判断当前文件是否存在(sInstance == null)来避免重复创建
if (sInstance == null) {
sInstance = new SynchronizedLazyForm();
}
return sInstance;
}
}

线程安全的懒汉式单例
利用synchronized关键字来修饰对外提供该类唯一实例的接口(getInstance)来确保在一个线程调用该接口时能阻塞(block)另一个线程的调用, 从而达到多线程安全, 避免重复创建单例.

然而, synchronized有很大的性能开销. 而且在这里我们是修饰了getInstance方法, 意味着, 如果getInstance被很多线程频繁调用时, 每次都会做同步检查, 会导致程序性能下降.

实际上我们要的是单例, 当单例已经存在的时候, 我们是不需要用同步方法来控制的. 一如我们第一种单例的实现—饿汉模式单例, 我们一开始就创建好了单例, 就无需担心线程同步问题.

但是饿汉模式是提前创建, 那么我们怎么能做到延迟创建, 且线程安全, 且性能有所提升呢?

双重检查锁定DCL(Double-Checked Locking)单例

如上所言, 我们想要的是单例, 故而单例已经存在的情况下我们无需做同步检查, 如下实现:

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
public class DCLForm extends Form {

// 注意, 这里我们引入了volatile关键字
private volatile static DCLForm sInstance;

// 私有化的构造, 避免别人直接创建表格
private DCLForm() {}

// 店长们通过这个接口来取表格
public static DCLForm getInstance() {

// 第一次检查
if (sInstance == null) {
// 第一次调用getInstance时, sInstance为空, 进入此分支
// 使用synchronized block来确保多线程的安全
synchronized (DCLForm.class) {
// 第二次检查
if (sInstance == null) {
sInstance = new DCLForm();
}
}
}
return sInstance;
}
}
  1. 舍弃了同步方法
  2. 在getInstance时, 先检查单例是否已经存在, 如果存在了, 我们无需同步操作了, 任何线程过来直接取单例就行, 大大提升了性能.
  3. 若单例不存在(第一次调用时), 使用synchronized同步代码块, 来确保进入的只有一个线程, 在此再做一次单例存在与否的检查, 进而创建出单例.

这样就保证了:

  1. 在单例还没有创建时, 多个线程同时调用getInsance时, 保证只有一个线程能够执行sInstance = new DCLForm()创建单例.
  2. 在单例已经存在时, getInsance没有加锁, 直接访问, 访问创建好的单例, 从而达到性能提升.

注意
这里我们对sInstance使用的volatile关键字
具体原因和原理, 请参考这篇文章, 讲的很详细.

然而, 使用volatile关键字的双重检查方案需要JDK5及以上(因为从JDK5开始使用新的JSR-133内存模型规范,这个规范增强了volatile的语义).

那么我们还有什么更通用的方式能保证多线程单例创建, 以及懒加载方式呢?

静态内部类单例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class StaticInnerClassForm extends Form {

// 私有化的构造, 避免别人直接创建表格
private StaticInnerClassForm() {}

// 店长们通过这个接口来取表格
public static StaticInnerClassForm getInstance() {
return FormHolder.INSTANCE;
}

// 在静态内部类中实例化该单例
private static class FormHolder {
private static final StaticInnerClassForm INSTANCE = new StaticInnerClassForm();
}
}

这种方式, 通过JVM的类加载方式(虚拟机会保证一个类的初始化在多线程环境中被正确的加锁、同步), 来保证了多线程并发访问的正确性.

另外, 由于静态内部类的加载特性 — 在使用时才加载, 这种方式也达成了懒加载的目的.

显然, 这种方式是一种比较完美的单例模式. 当然, 它也有其弊端, 依赖特定编程语言, 适用于JAVA平台.

模板方法

模板方法

模板方法模式
定义一个操作中的算法骨架(热干面的制作流程), 而将某些步骤实现延迟到子类中. 使得子类可以根据实际情况不改变算法骨架(热干面的制作流程), 但是可以重新定义或改变该算法中的某些特定步骤(例如装碗).