提交 e0014c93 编写于 作者: H hollis.zhl

增加7种设计模式相关知识

上级 d3a45f5c
......@@ -314,6 +314,7 @@
* [冬令时和夏令时](/basics/java-basic/StandardTime-vs-daylightSavingTime.md)
* [时间戳](/basics/java-basic/timestamp.md)
* Java中时间API
* [格林威治时间](/basics/java-basic/GMT.md)
......@@ -370,7 +371,7 @@
* lambda表达式
* Stream API
* [Stream API](/basics/java-basic/stream.md)
* 时间API
......@@ -753,19 +754,19 @@
* 创建型设计模式
* 单例模式
* [单例模式](/advance/design-patterns/singleton-pattern.md)
* 抽象工厂模式
* [抽象工厂模式](/advance/design-patterns/abstract-factory-pattern.md)
* 建造者模式
* [建造者模式](/advance/design-patterns/builder-pattern.md)
* 工厂模式
* [工厂模式](/advance/design-patterns/factory-method-pattern.md)
* 原型模式
* 结构型设计模式
* 适配器模式
* [适配器模式](/advance/design-patterns/adapter-pattern.md)
* 桥接模式
......@@ -785,7 +786,7 @@
* 命令模式
* 迭代器模式
* [迭代器模式](/advance/design-patterns/iterator-pattern.md)
* 观察者模式
......@@ -797,7 +798,7 @@
* 状态模式
* 策略模式
* [策略模式](/advance/design-patterns/strategy-pattern.md)
* 责任链模式
......
## 概念
抽象工厂模式(Abstract Factory Pattern):提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。抽象工厂模式又称为Kit模式,属于对象创建型模式。
抽象工厂模式提供了一种方式,可以将同一产品族的单独的工厂封装起来。在正常使用中,客户端程序需要创建抽象工厂的具体实现,然后使用抽象工厂作为接口来创建这一主题的具体对象。客户端程序不需要知道(或关心)它从这些内部的工厂方法中获得对象的具体类型,因为客户端程序仅使用这些对象的通用接口。抽象工厂模式将一组对象的实现细节与他们的一般使用分离开来。
### 产品族
来认识下什么是产品族: 位于不同产品等级结构中,功能相关的产品组成的家族。如下面的例子,就有两个产品族:跑车族和商务车族。
[<img src="http://www.hollischuang.com/wp-content/uploads/2016/04/QQ20160419-0.png" alt="QQ20160419-0" width="637" height="408" class="alignnone size-full wp-image-1421" />][5]
## 用途
抽象工厂模式和工厂方法模式一样,都符合开放-封闭原则。但是不同的是,工厂方法模式在增加一个具体产品的时候,都要增加对应的工厂。但是抽象工厂模式只有在新增一个类型的具体产品时才需要新增工厂。也就是说,工厂方法模式的一个工厂只能创建一个具体产品。而抽象工厂模式的一个工厂可以创建属于一类类型的多种具体产品。工厂创建产品的个数介于简单工厂模式和工厂方法模式之间。
在以下情况下可以使用抽象工厂模式:
> 一个系统不应当依赖于产品类实例如何被创建、组合和表达的细节,这对于所有类型的工厂模式都是重要的。
>
> 系统中有多于一个的产品族,而每次只使用其中某一产品族。
>
> 属于同一个产品族的产品将在一起使用,这一约束必须在系统的设计中体现出来。
>
> 系统提供一个产品类的库,所有的产品以同样的接口出现,从而使客户端不依赖于具体实现。
## 实现方式
抽象工厂模式包含如下角色:
> AbstractFactory(抽象工厂):用于声明生成抽象产品的方法
>
> ConcreteFactory(具体工厂):实现了抽象工厂声明的生成抽象产品的方法,生成一组具体产品,这些产品构成了一个产品族,每一个产品都位于某个产品等级结构中;
>
> AbstractProduct(抽象产品):为每种产品声明接口,在抽象产品中定义了产品的抽象业务方法;
>
> Product(具体产品):定义具体工厂生产的具体产品对象,实现抽象产品接口中定义的业务方法。
本文的例子采用一个汽车代工厂造汽车的例子。假设我们是一家汽车代工厂商,我们负责给奔驰和特斯拉两家公司制造车子。我们简单的把奔驰车理解为需要加油的车,特斯拉为需要充电的车。其中奔驰车中包含跑车和商务车两种,特斯拉同样也包含奔驰车和商务车。
[<img src="http://www.hollischuang.com/wp-content/uploads/2016/04/QQ20160419-1.png" alt="QQ20160419-1" width="657" height="554" class="alignnone size-full wp-image-1422" />][6]
以上场景,我们就可以把跑车和商务车分别对待,对于跑车有单独的工厂创建,商务车也有单独的工厂。这样,以后无论是再帮任何其他厂商造车,只要是跑车或者商务车我们都不需要再引入工厂。同样,如果我们要增加一种其他类型的车,比如越野车,我们也不需要对跑车或者商务车的任何东西做修改。
下面是抽象产品,奔驰车和特斯拉车:
public interface BenzCar {
//加汽油
public void gasUp();
}
public interface TeslaCar {
//充电
public void charge();
}
下面是具体产品,奔驰跑车、奔驰商务车、特斯拉跑车、特斯拉商务车:
public class BenzSportCar implements BenzCar {
public void gasUp() {
System.out.println("给我的奔驰跑车加最好的汽油");
}
}
public class BenzBusinessCar implements BenzCar{
public void gasUp() {
System.out.println("给我的奔驰商务车加一般的汽油");
}
}
public class TeslaSportCar implements TeslaCar {
public void charge() {
System.out.println("给我特斯拉跑车冲满电");
}
}
public class TeslaBusinessCar implements TeslaCar {
public void charge() {
System.out.println("不用给我特斯拉商务车冲满电");
}
}
下面是抽象工厂:
public interface CarFactory {
public BenzCar getBenzCar();
public TeslaCar getTeslaCar();
}
下面是具体工厂:
public class SportCarFactory implements CarFactory {
public BenzCar getBenzCar() {
return new BenzSportCar();
}
public TeslaCar getTeslaCar() {
return new TeslaSportCar();
}
}
public class BusinessCarFactory implements CarFactory {
public BenzCar getBenzCar() {
return new BenzBusinessCar();
}
public TeslaCar getTeslaCar() {
return new TeslaBusinessCar();
}
}
## “开闭原则”的倾斜性
“开闭原则”要求系统对扩展开放,对修改封闭,通过扩展达到增强其功能的目的。对于涉及到多个产品族与多个产品等级结构的系统,其功能增强包括两方面:
> 增加产品族:对于增加新的产品族,工厂方法模式很好的支持了“开闭原则”,对于新增加的产品族,只需要对应增加一个新的具体工厂即可,对已有代码无须做任何修改。
>
> 增加新的产品等级结构:对于增加新的产品等级结构,需要修改所有的工厂角色,包括抽象工厂类,在所有的工厂类中都需要增加生产新产品的方法,不能很好地支持“开闭原则”。
抽象工厂模式的这种性质称为“开闭原则”的倾斜性,抽象工厂模式以一种倾斜的方式支持增加新的产品,它为新产品族的增加提供方便,但不能为新的产品等级结构的增加提供这样的方便。
## 三种工厂模式之间的关系
当抽象工厂模式中每一个具体工厂类只创建一个产品对象,也就是只存在一个产品等级结构时,抽象工厂模式退化成工厂方法模式;
抽象工厂模式与工厂方法模式最大的区别在于,工厂方法模式针对的是一个产品等级结构,而抽象工厂模式则需要面对多个产品等级结构。
当工厂方法模式中抽象工厂与具体工厂合并,提供一个统一的工厂来创建产品对象,并将创建对象的工厂方法设计为静态方法时,工厂方法模式退化成简单工厂模式。
## 总结
抽象工厂模式提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。抽象工厂模式又称为Kit模式,属于对象创建型模式。
抽象工厂模式是所有形式的工厂模式中最为抽象和最具一般性的一种形态。
抽象工厂模式的主要优点是隔离了具体类的生成,使得客户并不需要知道什么被创建,而且每次可以通过具体工厂类创建一个产品族中的多个对象,增加或者替换产品族比较方便,增加新的具体工厂和产品族很方便;主要缺点在于增加新的产品等级结构很复杂,需要修改抽象工厂和所有的具体工厂类,对“开闭原则”的支持呈现倾斜性。
文中所有代码见[GitHub][7]
## 参考资料
[大话设计模式][8]
[深入浅出设计模式][9]
[抽象工厂模式(Factory Method Pattern)][10]
[1]: http://www.hollischuang.com/archives/category/%E7%BB%BC%E5%90%88%E5%BA%94%E7%94%A8/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F
[2]: http://www.hollischuang.com/archives/1401
[3]: http://www.hollischuang.com/archives/1408
[4]: http://www.hollischuang.com/archives/1391
[5]: http://www.hollischuang.com/wp-content/uploads/2016/04/QQ20160419-0.png
[6]: http://www.hollischuang.com/wp-content/uploads/2016/04/QQ20160419-1.png
[7]: https://github.com/hollischuang/DesignPattern
[8]: http://s.click.taobao.com/t?e=m=2&s=R5B/xd29JVMcQipKwQzePOeEDrYVVa64K7Vc7tFgwiHjf2vlNIV67jN2wQzI0ZBVHBMajAjK1gBpS4hLH/P02ckKYNRBWOBBey11vvWwHXSniyi5vWXIZkKWZZq7zWpCC8X3k5aQlui0qVGgqDL2o8YMXU3NNCg/&pvid=10_42.120.73.203_224_1460382841310
[9]: http://s.click.taobao.com/t?e=m%3D2%26s%3DObpq8Qxse2EcQipKwQzePOeEDrYVVa64K7Vc7tFgwiHjf2vlNIV67utJaEGcptl2kfkm8XrrgBtpS4hLH%2FP02ckKYNRBWOBBey11vvWwHXTpkOAWGyim%2Bw2PNKvM2u52N5aP5%2Bgx7zgh4LxdBQDQSXEqY%2Bakgpmw&pvid=10_121.0.29.199_322_1460465025379
[10]: http://design-patterns.readthedocs.org/zh_CN/latest/creational_patterns/abstract_factory.html#id14
\ No newline at end of file
## 概念
GOF是这样给适配器模式(`Adapter`)定义的:将一个类的接口转化成用户需要的另外一个接口。Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
GOF中将适配器模式分为类适配器模式和对象适配器模式。区别仅在于适配器角色对于被适配角色的适配是通过继承还是组合来实现的。由于在Java 中不支持多重继承,而且有破坏封装之嫌。而且我们也提倡[多用组合少用继承][2]。所以本文主要介绍对象适配器。
## 用途
相信大家都有这样的生活常识:就是目前我们使用的电子设备充电器的型号是不一样的。现在主流的手机充电器口主要包含Mini Usb、Micro Usb和Lightning三种。其中Mini Usb广泛出现在读卡器、MP3、数码相机以及移动硬盘上。由于Micro Usb比Mini Usb更薄,所有广泛应用于手机上,常见于安卓手机。还有一个比较常见的充电器口就是苹果手机常用的Lightning。
当然,特定型号的手机只能使用特定型号的充电器充电。比如Iphone6手机只能使用Lightning接口的充电器进行充电。但是,如果我们身边只有一条安卓的Micro Usb充电器线的话,我们能不能为苹果手机充电呢?答案是肯定的,只要有一个适配器就可以了。
<img src="http://www.hollischuang.com/wp-content/uploads/2016/05/adapter-300x300.jpg" alt="adapter" width="300" height="300" class="aligncenter size-medium wp-image-1501" />
适配器,在我们日常生活中随处可见。适配器模式也正是解决了类似的问题。
在程序设计过程中我们可能也遇到类似的场景:
> 1、系统需要使用现有的类,而此类的接口不符合系统的需要。
>
> 2、想要建立一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作,这些源类不一定有一致的接口。
>
> 3、通过接口转换,将一个类插入另一个类系中。(比如老虎和飞禽,现在多了一个飞虎,在不增加实体的需求下,增加一个适配器,在里面包容一个虎对象,实现飞的接口。)
以上场景都适合使用适配器模式。
## 实现方式
适配器模式包含如下角色:
> Target:目标抽象类
>
> Adapter:适配器类
>
> Adaptee:适配者类
>
> Client:客户类
<img src="http://www.hollischuang.com/wp-content/uploads/2016/05/Adapter-pattern.jpg" alt="Adapter-pattern" width="724" height="313" class="aligncenter size-full wp-image-1520" />
这里采用文章开头介绍的手机充电口的例子,我们定义一个适配器,该适配器的功能就是使用安卓充电器给苹果设备充电。
先定义接口:
/**
* MicroUsb充电器接口
*/
public interface MicroUsbInterface {
public void chargeWithMicroUsb();
}
/**
* Lightning充电器接口
*/
public interface LightningInterface {
public void chargeWithLightning();
}
定义具体的实现类
/**
* 安卓设备的充电器
*/
public class AndroidCharger implements MicroUsbInterface {
@Override
public void chargeWithMicroUsb() {
System.out.println("使用MicroUsb型号的充电器充电...");
}
}
/**
* 苹果设备的充电器
*/
public class AppleCharger implements LightningInterface {
@Override
public void chargeWithLightning() {
System.out.println("使用Lightning型号的充电器充电...");
}
}
> 因为我们要使用适配器模式将MicroUsb转成Lightning,所以这里的AppleCharger是本来不需要定义的。因为我们使用适配器的目的就是代替新建一个他。这里定义出来是为了使例子更加完整。
定义两个手机
public class Iphone6Plus {
private LightningInterface lightningInterface;
public Iphone6Plus() {
}
public Iphone6Plus(LightningInterface lightningInterface) {
this.lightningInterface = lightningInterface;
}
public void charge() {
System.out.println("开始给我的Iphone6Plus手机充电...");
lightningInterface.chargeWithLightning();
System.out.println("结束给我的Iphone6Plus手机充电...");
}
public LightningInterface getLightningInterface() {
return lightningInterface;
}
public void setLightningInterface(LightningInterface lightningInterface) {
this.lightningInterface = lightningInterface;
}
}
public class GalaxyS7 {
private MicroUsbInterface microUsbInterface;
public GalaxyS7() {
}
public GalaxyS7(MicroUsbInterface microUsbInterface) {
this.microUsbInterface = microUsbInterface;
}
public void charge(){
System.out.println("开始给我的GalaxyS7手机充电...");
microUsbInterface.chargeWithMicroUsb();
System.out.println("开始给我的GalaxyS7手机充电...");
}
public MicroUsbInterface getMicroUsbInterface() {
return microUsbInterface;
}
public void setMicroUsbInterface(MicroUsbInterface microUsbInterface) {
this.microUsbInterface = microUsbInterface;
}
}
这里定义手机的作用是为了更方便的理解适配器模式,在该模式中他不扮演任何角色。
定义适配器
/**
* 适配器,将MicroUsb接口转成Lightning接口
*/
public class Adapter implements LightningInterface {
private MicroUsbInterface microUsbInterface;
public Adapter() {
}
public Adapter(MicroUsbInterface microUsbInterface) {
this.microUsbInterface = microUsbInterface;
}
@Override
public void chargeWithLightning() {
microUsbInterface.chargeWithMicroUsb();
}
public MicroUsbInterface getMicroUsbInterface() {
return microUsbInterface;
}
public void setMicroUsbInterface(MicroUsbInterface microUsbInterface) {
this.microUsbInterface = microUsbInterface;
}
}
该适配器的功能是把一个MicroUsb转换成Lightning。实现方式是实现目标类的接口(`LightningInterface`),然后使用组合的方式,在该适配器中定义microUsb。然后在重写的`chargeWithLightning()`方法中,采用microUsb的方法来实现具体细节。
定义客户端
public class Main {
public static void main(String[] args) {
Iphone6Plus iphone6Plus = new Iphone6Plus(new AppleCharger());
iphone6Plus.charge();
System.out.println("==============================");
GalaxyS7 galaxyS7 = new GalaxyS7(new AndroidCharger());
galaxyS7.charge();
System.out.println("==============================");
Adapter adapter = new Adapter(new AndroidCharger());
Iphone6Plus newIphone = new Iphone6Plus();
newIphone.setLightningInterface(adapter);
newIphone.charge();
}
}
输出结果:
开始给我的Iphone6Plus手机充电...
使用Lightning型号的充电器充电...
结束给我的Iphone6Plus手机充电...
==============================
开始给我的GalaxyS7手机充电...
使用MicroUsb型号的充电器充电...
开始给我的GalaxyS7手机充电...
==============================
开始给我的Iphone6Plus手机充电...
使用MicroUsb型号的充电器充电...
结束给我的Iphone6Plus手机充电...
上面的例子通过适配器,把一个MicroUsb型号的充电器用来给Iphone充电。从代码层面,就是通过适配器复用了MicroUsb接口及其实现类。在很大程度上福永了已有的代码。
## 优缺点
### 优点
将目标类和适配者类解耦,通过引入一个适配器类来重用现有的适配者类,而无须修改原有代码。
增加了类的透明性和复用性,将具体的实现封装在适配者类中,对于客户端类来说是透明的,而且提高了适配者的复用性。
灵活性和扩展性都非常好,通过使用配置文件,可以很方便地更换适配器,也可以在不修改原有代码的基础上增加新的适配器类,完全符合“开闭原则”。
### 缺点
过多地使用适配器,会让系统非常零乱,不易整体进行把握。比如,明明看到调用的是 A 接口,其实内部被适配成了 B 接口的实现,一个系统如果太多出现这种情况,无异于一场灾难。因此如果不是很有必要,可以不使用适配器,而是直接对系统进行重构。
对于类适配器而言,由于 JAVA 至多继承一个类,所以至多只能适配一个适配者类,而且目标类必须是抽象类
## 总结
结构型模式描述如何将类或者对象结合在一起形成更大的结构。
适配器模式用于将一个接口转换成客户希望的另一个接口,适配器模式使接口不兼容的那些类可以一起工作,其别名为包装器。适配器模式既可以作为类结构型模式,也可以作为对象结构型模式。
适配器模式包含四个角色:
> 目标抽象类定义客户要用的特定领域的接口;
>
> 适配器类可以调用另一个接口,作为一个转换器,对适配者和抽象目标类进行适配,它是适配器模式的核心;
>
> 适配者类是被适配的角色,它定义了一个已经存在的接口,这个接口需要适配;
>
> 在客户类中针对目标抽象类进行编程,调用在目标抽象类中定义的业务方法。
在对象适配器模式中,适配器类继承了目标抽象类(或实现接口)并定义了一个适配者类的对象实例,在所继承的目标抽象类方法中调用适配者类的相应业务方法。
适配器模式的主要优点是将目标类和适配者类解耦,增加了类的透明性和复用性,同时系统的灵活性和扩展性都非常好,更换适配器或者增加新的适配器都非常方便,符合“[开闭原则][3]”;类适配器模式的缺点是适配器类在很多编程语言中不能同时适配多个适配者类,对象适配器模式的缺点是很难置换适配者类的方法。
适配器模式适用情况包括:系统需要使用现有的类,而这些类的接口不符合系统的需要;想要建立一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类一起工作。
## 参考资料
[适配器模式][4]
[适配器模式][5]
文中所有代码见[GitHub][6]
[1]: http://www.hollischuang.com/archives/category/%E7%BB%BC%E5%90%88%E5%BA%94%E7%94%A8/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F
[2]: http://www.hollischuang.com/archives/1319
[3]: http://www.hollischuang.com/archives/220
[4]: http://www.runoob.com/design-pattern/adapter-pattern.html
[5]: http://design-patterns.readthedocs.io/zh_CN/latest/structural_patterns/adapter.html
[6]: https://github.com/hollischuang/DesignPattern
\ No newline at end of file
## 概念
建造者模式(英:Builder Pattern)是一种创建型设计模式,又名:生成器模式。GOF 给建造者模式的定义为:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。这句话说的比较抽象,其实解释一下就是:将建造复杂对象的过程和组成对象的部件解耦。
## 用途
假设现在我们是一家网游设计公司,现在我们要"抄袭"梦幻西游这款游戏,你是该公司的游戏角色设计人员。你怎么设计出该游戏中的各种角色呢? 在梦幻西游来中包括人、仙、魔等种族的角色,而每种不同的种族的角色中又包含龙太子、逍遥生等具体的角色。
作为一个出色的开发人员,我们设计的角色生成系统应该包含以下功能和特性:
> 为了保证游戏平衡,所有角色的基本属性应该一致
>
> 因为角色的创建过程可能很复杂,所以角色的生成细节不应该对外暴露
>
> 随时可以新增角色
>
> 对某个具体角色的修改应该不影响其他角色
其实,对于角色的设计,我们可以使用抽象工厂模式,将同一种族的角色看成是一个产品族。但是,这样做可能存在一个问题,那就是我们可能要在每个角色的创建过程中都要从头到尾的构建一遍该角色。比如一个角色包含头部、身体。其中头部又包括脸部、和其他部位。其中脸部又包含眉毛、嘴巴、鼻子等部位。整个角色的创建过程是极其复杂的。很容易遗漏其中的某个步骤。
那么,我们可以将这些具体部位的创建工作和对象的创建进行解耦。这就是建造者模式。
## 实现方式
建造者模式包含如下角色:
> Builder:抽象建造者(`Builder`)
>
> ConcreteBuilder:具体建造者(`CommonBuilder`、`SuperBuilder`)
>
> Director:指挥者(`Director`)
>
> Product:产品角色(`Role`)
[<img src="http://www.hollischuang.com/wp-content/uploads/2016/05/Builder.jpg" alt="Builder" width="713" height="338" class="alignnone size-full wp-image-1478" />][2]
这里采用设计角色的例子,为了便于理解,我们只创建两个角色,分别是普通角色和超级角色。他们都有设置头部、脸部、身体、气血值、魔法值、能量值等方法。值得注意的是设置脸部是依赖于设置头部的,要有先后顺序。
产品角色:Role
public class Role {
private String head; //头部
private String face; //脸部(脸部依赖于头部)
private String body; //身体
private Double hp; //生命值
private Double sp; //能量值
private Double mp; //魔法值
//setter and getter
// toString
}
抽象建造者:Builder
public abstract class Builder {
protected Role role = new Role();
public abstract void buildHead();
public abstract void buildFace();
public abstract void buildBody();
public abstract void buildHp();
public abstract void buildSp();
public abstract void buildMp();
public Role getResult() {
return role;
}
}
具体建造者:
public class CommonRoleBuilder extends Builder {
private Role role = new Role();
@Override
public void buildHead() {
role.setBody("common head");
}
@Override
public void buildFace() {
role.setFace("common face");
}
@Override
public void buildBody() {
role.setBody("common body");
}
@Override
public void buildHp() {
role.setHp(100d);
}
@Override
public void buildSp() {
role.setSp(100d);
}
@Override
public void buildMp() {
role.setMp(100d);
}
@Override
public Role getResult() {
return role;
}
}
public class SuperRoleBuilder extends Builder {
private Role role = new Role();
@Override
public void buildHead() {
role.setBody("suoer head");
}
@Override
public void buildFace() {
role.setFace("super face");
}
@Override
public void buildBody() {
role.setBody("super body");
}
@Override
public void buildHp() {
role.setHp(120d);
}
@Override
public void buildSp() {
role.setSp(120d);
}
@Override
public void buildMp() {
role.setMp(120d);
}
@Override
public Role getResult() {
return role;
}
}
指挥者:
public class Director {
public void construct(Builder builder){
builder.buildBody();
builder.buildHead();
builder.buildFace();
builder.buildHp();
builder.buildMp();
builder.buildSp();
}
}
测试类:
public class Main {
public static void main(String[] args) {
Director director = new Director();
Builder commonBuilder = new CommonRoleBuilder();
director.construct(commonBuilder);
Role commonRole = commonBuilder.getResult();
System.out.println(commonRole);
}
}
到这里,一个建造者模式已经完成了,是不是很简单?
* * *
再回到之前的需求,看看我们是否都满足?
由于建造角色的过程比较复杂,其中还有相互依赖关系(如脸部依赖于头部),所以我们使用建造者模式将将建造复杂对象的过程和组成对象的部件解耦。这样既保证了基本属性全都一致(这里的一致指的是该包含的应该全都包含)也封装了其中的具体实现细节。
同时,在修改某个具体角色的时候我们只需要修改对应的具体角色就可以了,不会影响到其他角色。
如果需要新增角色,只要再增加一个具体建造者,并在该建造者中写好具体细节的建造部分代码就OK了。
## 建造者模式的优缺点
### 优点
建造者模式的**封装性很好。使用建造者模式可以有效的封装变化**,在使用建造者模式的场景中,一般产品类和建造者类是比较稳定的,因此,将主要的业务逻辑封装在导演类中对整体而言可以取得比较好的稳定性。
在建造者模式中,**客户端不必知道产品内部组成的细节**,将产品本身与产品的创建过程解耦,使得相同的创建过程可以创建不同的产品对象。
**可以更加精细地控制产品的创建过程** 。将复杂产品的创建步骤分解在不同的方法中,使得创建过程更加清晰,也更方便使用程序来控制创建过程。
其次,**建造者模式很容易进行扩展**。如果有新的需求,通过实现一个新的建造者类就可以完成,基本上不用修改之前已经测试通过的代码,因此也就不会对原有功能引入风险。符合开闭原则。
### 缺点
建造者模式所创建的产品一般具有较多的共同点,其组成部分相似,如果产品之间的差异性很大,则不适合使用建造者模式,因此其使用范围受到一定的限制。
如果产品的内部变化复杂,可能会导致需要定义很多具体建造者类来实现这种变化,导致系统变得很庞大。
## 适用环境
在以下情况下可以使用建造者模式:
> 需要生成的产品对象有复杂的内部结构,这些产品对象通常包含多个成员属性。
>
> 需要生成的产品对象的属性相互依赖,需要指定其生成顺序。
>
> 对象的创建过程独立于创建该对象的类。在建造者模式中引入了指挥者类,将创建过程封装在指挥者类中,而不在建造者类中。
>
> 隔离复杂对象的创建和使用,并使得相同的创建过程可以创建不同的产品。
## 建造者模式与工厂模式的区别
我们可以看到,建造者模式与工厂模式是极为相似的,总体上,建造者模式仅仅只比工厂模式多了一个"指挥者"的角色。在建造者模式的类图中,假如把这个导演类看做是最终调用的客户端,那么图中剩余的部分就可以看作是一个简单的工厂模式了。
与工厂模式相比,建造者模式一般用来创建更为复杂的对象,因为对象的创建过程更为复杂,因此将对象的创建过程独立出来组成一个新的类——导演类。
也就是说,工厂模式是将对象的全部创建过程封装在工厂类中,由工厂类向客户端提供最终的产品;而建造者模式中,建造者类一般只提供产品类中各个组件的建造,而将具体建造过程交付给导演类。由导演类负责将各个组件按照特定的规则组建为产品,然后将组建好的产品交付给客户端。
建造者模式与工厂模式类似,适用的场景也很相似。一般来说,如果产品的建造很复杂,那么请用工厂模式;如果产品的建造更复杂,那么请用建造者模式。哈哈哈。。。
## 总结
建造者模式将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
在建造者模式的结构中引入了一个指挥者类,该类的作用主要有两个:一方面它隔离了客户与生产过程;另一方面它负责控制产品的生成过程。指挥者针对抽象建造者编程,客户端只需要知道具体建造者的类型,即可通过指挥者类调用建造者的相关方法,返回一个完整的产品对象。
## 参考资料
[大话设计模式][3]
[深入浅出设计模式][4]
[建造者模式][5]
[1]: http://www.hollischuang.com/archives/category/%E7%BB%BC%E5%90%88%E5%BA%94%E7%94%A8/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F
[2]: http://www.hollischuang.com/wp-content/uploads/2016/05/Builder.jpg
[3]: http://s.click.taobao.com/t?e=m=2&s=R5B/xd29JVMcQipKwQzePOeEDrYVVa64K7Vc7tFgwiHjf2vlNIV67jN2wQzI0ZBVHBMajAjK1gBpS4hLH/P02ckKYNRBWOBBey11vvWwHXSniyi5vWXIZkKWZZq7zWpCC8X3k5aQlui0qVGgqDL2o8YMXU3NNCg/&pvid=10_42.120.73.203_224_1460382841310
[4]: http://s.click.taobao.com/t?e=m=2&s=Obpq8Qxse2EcQipKwQzePOeEDrYVVa64K7Vc7tFgwiHjf2vlNIV67utJaEGcptl2kfkm8XrrgBtpS4hLH/P02ckKYNRBWOBBey11vvWwHXTpkOAWGyim%2bw2PNKvM2u52N5aP5%2bgx7zgh4LxdBQDQSXEqY%2bakgpmw&pvid=10_121.0.29.199_322_1460465025379
[5]: http://design-patterns.readthedocs.io/zh_CN/latest/creational_patterns/builder.html
\ No newline at end of file
## 概念
工厂方法模式(Factory Method Pattern)又称为工厂模式,也叫虚拟构造器(Virtual Constructor)模式或者多态工厂(Polymorphic Factory)模式,它属于类创建型模式。
工厂方法模式是一种实现了“工厂”概念的面向对象设计模式。就像其他创建型模式一样,它也是处理在不指定对象具体类型的情况下创建对象的问题。
> 工厂方法模式的实质是“定义一个创建对象的接口,但让实现这个接口的类来决定实例化哪个类。工厂方法让类的实例化推迟到子类中进行。”
## 用途
工厂方法模式和[简单工厂模式][2]虽然都是通过工厂来创建对象,他们之间最大的不同是——**工厂方法模式在设计上完全完全符合“[开闭原则][3]”。**
在以下情况下可以使用工厂方法模式:
> 一个类不知道它所需要的对象的类:在工厂方法模式中,客户端不需要知道具体产品类的类名,只需要知道所对应的工厂即可,具体的产品对象由具体工厂类创建;客户端需要知道创建具体产品的工厂类。
>
> 一个类通过其子类来指定创建哪个对象:在工厂方法模式中,对于抽象工厂类只需要提供一个创建产品的接口,而由其子类来确定具体要创建的对象,利用面向对象的多态性和[里氏代换原则][3],在程序运行时,子类对象将覆盖父类对象,从而使得系统更容易扩展。
>
> 将创建对象的任务委托给多个工厂子类中的某一个,客户端在使用时可以无须关心是哪一个工厂子类创建产品子类,需要时再动态指定,可将具体工厂类的类名存储在配置文件或数据库中。
## 实现方式
工厂方法模式包含如下角色:
> Product:抽象产品(`Operation`)
>
> ConcreteProduct:具体产品(`OperationAdd`)
>
> Factory:抽象工厂(`IFactory`)
>
> ConcreteFactory:具体工厂(`AddFactory`)
[<img src="http://www.hollischuang.com/wp-content/uploads/2016/04/QQ20160412-0.png" alt="QQ20160412-0" width="798" height="518" class="alignnone size-full wp-image-1402" />][4]
这里还用计算器的例子。在保持`Operation``OperationAdd``OperationDiv``OperationSub``OperationMul`等几个方法不变的情况下,修改简单工厂模式中的工厂类(`OperationFactory`)。替代原有的那个"万能"的大工厂类,这里使用工厂方法来代替:
//工厂接口
public interface IFactory {
Operation CreateOption();
}
//加法类工厂
public class AddFactory implements IFactory {
public Operation CreateOption() {
return new OperationAdd();
}
}
//除法类工厂
public class DivFactory implements IFactory {
public Operation CreateOption() {
return new OperationDiv();
}
}
//除法类工厂
public class MulFactory implements IFactory {
public Operation CreateOption() {
return new OperationMul();
}
}
//减法类工厂
public class SubFactory implements IFactory {
public Operation CreateOption() {
return new OperationSub();
}
}
这样,在客户端中想要执行加法运算时,需要以下方式:
public class Main {
public static void main(String[] args) {
IFactory factory = new AddFactory();
Operation operationAdd = factory.CreateOption();
operationAdd.setValue1(10);
operationAdd.setValue2(5);
System.out.println(operationAdd.getResult());
}
}
到这里,一个工厂方法模式就已经写好了。
* * *
从代码量上看,这种工厂方法模式比简单工厂方法模式更加复杂。针对不同的操作(Operation)类都有对应的工厂。很多人会有以下疑问:
> 貌似工厂方法模式比简单工厂模式要复杂的多?
>
> 工厂方法模式和我自己创建对象没什么区别?为什么要多搞出一些工厂来?
下面就针对以上两个问题来深入理解一下工厂方法模式。
## 工厂方法模式的利与弊
### 为什么要使用工厂来创建对象?
> 封装对象的创建过程
在工厂方法模式中,工厂方法用来创建客户所需要的产品,同时还向客户**隐藏了哪种具体产品类将被实例化这一细节,用户只需要关心所需产品对应的工厂,无须关心创建细节,甚至无须知道具体产品类的类名。**
基于工厂角色和产品角色的多态性设计是工厂方法模式的关键。**它能够使工厂可以自主确定创建何种产品对象,而如何创建这个对象的细节则完全封装在具体工厂内部。**工厂方法模式之所以又被称为多态工厂模式,是因为所有的具体工厂类都具有同一抽象父类。
### 为什么每种对象要单独有一个工厂?
> 符合『[开放-封闭原则][5]』
主要目的是为了解耦。在系统中加入新产品时,无须修改抽象工厂和抽象产品提供的接口,无须修改客户端,也无须修改其他的具体工厂和具体产品,而只要添加一个具体工厂和具体产品就可以了。这样,系统的可扩展性也就变得非常好,完全符合“[开闭原则][3]”。
以上就是工厂方法模式的优点。但是,工厂模式也有一些不尽如人意的地方:
> 在添加新产品时,需要编写新的具体产品类,而且还要提供与之对应的具体工厂类,系统中类的个数将成对增加,在一定程度上增加了系统的复杂度,有更多的类需要编译和运行,会给系统带来一些额外的开销。
>
> 由于考虑到系统的可扩展性,需要引入抽象层,在客户端代码中均使用抽象层进行定义,增加了系统的抽象性和理解难度,且在实现时可能需要用到DOM、反射等技术,增加了系统的实现难度。
## 工厂方法与简单工厂的区别
工厂模式克服了简单工厂模式违背[开放-封闭原则][3]的缺点,又保持了封装对象创建过程的优点。
他们都是集中封装了对象的创建,使得要更换对象时,不需要做大的改动就可实现,降低了客户端与产品对象的耦合。
## 总结
工厂方法模式是简单工厂模式的进一步抽象和推广。
由于使用了面向对象的多态性,工厂方法模式保持了简单工厂模式的优点,而且克服了它的缺点。
在工厂方法模式中,核心的工厂类不再负责所有产品的创建,而是将具体创建工作交给子类去做。这个核心类仅仅负责给出具体工厂必须实现的接口,而不负责产品类被实例化这种细节,这使得工厂方法模式可以允许系统在不修改工厂角色的情况下引进新产品。
工厂方法模式的主要优点是增加新的产品类时无须修改现有系统,并封装了产品对象的创建细节,系统具有良好的灵活性和可扩展性;其缺点在于增加新产品的同时需要增加新的工厂,导致系统类的个数成对增加,在一定程度上增加了系统的复杂性。
文中所有代码见[GitHub][6]
## 参考资料
[大话设计模式][7]
[深入浅出设计模式][8]
[工厂方法模式(Factory Method Pattern)][9]
[1]: http://www.hollischuang.com/archives/category/%E7%BB%BC%E5%90%88%E5%BA%94%E7%94%A8/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F
[2]: http://www.hollischuang.com/archives/1391
[3]: http://www.hollischuang.com/archives/220
[4]: http://www.hollischuang.com/wp-content/uploads/2016/04/QQ20160412-0.png
[5]: http://www.hollischuang.com/archives/220http://
[6]: https://github.com/hollischuang/DesignPattern
[7]: http://s.click.taobao.com/t?e=m=2&s=R5B/xd29JVMcQipKwQzePOeEDrYVVa64K7Vc7tFgwiHjf2vlNIV67jN2wQzI0ZBVHBMajAjK1gBpS4hLH/P02ckKYNRBWOBBey11vvWwHXSniyi5vWXIZkKWZZq7zWpCC8X3k5aQlui0qVGgqDL2o8YMXU3NNCg/&pvid=10_42.120.73.203_224_1460382841310
[8]: http://s.click.taobao.com/t?e=m%3D2%26s%3DObpq8Qxse2EcQipKwQzePOeEDrYVVa64K7Vc7tFgwiHjf2vlNIV67utJaEGcptl2kfkm8XrrgBtpS4hLH%2FP02ckKYNRBWOBBey11vvWwHXTpkOAWGyim%2Bw2PNKvM2u52N5aP5%2Bgx7zgh4LxdBQDQSXEqY%2Bakgpmw&pvid=10_121.0.29.199_322_1460465025379
[9]: http://design-patterns.readthedocs.org/zh_CN/latest/creational_patterns/factory_method.html#id11
\ No newline at end of file
## 概念
一提到迭代器模式很多人可能感觉很陌生,但是实际上,迭代器模式是所有设计模式中最简单也是最常用的设计模式,正是因为他太常用了,所以很多人忽略了他的存在。
> 迭代器模式提供一种方法访问一个容器中各个元素,而又不需要暴露该对象的内部细节。
那么,这里提到的容器是什么呢?其实就是可以包含一组对象的数据结构,如Java中的`Collection``Set`
## 用途
从迭代器模式的概念中我们也看的出来,迭代器模式的重要用途就是帮助我们遍历容器。拿List来举例。如果我们想要遍历他的话,通常有以下几种方式:
for (int i = 0; i < list.size(); i++) {
System.out.print(list.get(i) + ",");
}
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
System.out.print(iterator.next() + ",");
}
for (Integer i : list) {
System.out.print(i + ",");
}
其实,第二种和[第三种][2]都是基于迭代器模式实现的。本文重点是介绍迭代器模式,那么先暂不介绍Java中内置的迭代器,我们尝试自己实现一下迭代器模式,这样更有助于我们彻底理解迭代器模式。
## 实现方式
迭代器模式包含如下角色:
> Iterator 抽象迭代器
>
> ConcreteIterator 具体迭代器
>
> Aggregate 抽象容器
>
> Concrete Aggregate 具体容器
[<img src="http://www.hollischuang.com/wp-content/uploads/2017/02/iterator.jpg" alt="iterator" width="542" height="287" class="aligncenter size-full wp-image-1767" />][3]
这里我们举一个菜单的例子,我们有一个菜单,我们想要展示出菜单中所有菜品的名字和报价信息等。
先定义抽象迭代器:
public interface Iterator<E> {
boolean hasNext();
E next();
default void remove() {
throw new UnsupportedOperationException("remove");
}
}
这里的迭代器提供了三个方法,分别包括hasNext方法、next方法和remove方法。
> hasNext 返回该迭代器中是否还有未遍历过的元素
>
> next 返回迭代器中的下一个元素
在定义一个具体的迭代器:
public class MenuIterator implements Iterator {
String[] foods;
int position = 0;
public MenuIterator(String[] foods){
this.foods = foods;
}
@Override
public boolean hasNext() {
return position != foods.length;
}
@Override
public Object next() {
String food = foods[position];
position += 1;
return food;
}
}
这个具体的类实现了Iterator接口,并实现了其中的方法。具体实现就不详细写了,相信都能看得懂(请忽略线程安全问题)。
接下来定义一个抽象容器:
/**
* Created by hollis on 17/2/18.
* /
public interface Menu {
void add(String name);
Iterator getIterator();
}
这里定义一个菜单接口,只提供两个方法,一个add方法和一个getIterator方法,用于返回一个迭代器。
然后定义一个具体的容器,用于实现Menu接口:
public class ChineseFoodMenu implements Menu {
private String[] foods = new String[4];
private int position = 0;
@Override
public void add(String name) {
foods[position] = name;
position += 1;
}
@Override
public Iterator getIterator() {
return new MenuIterator(this.foods);
}
}
该类的实现也相对简单。至此,我们已经具备了一个迭代器模式需要的所有角色。接下来写一个测试类看看具体使用:
public class Main {
public static void main(String[] args) {
ChineseFoodMenu chineseFoodMenu = new ChineseFoodMenu();
chineseFoodMenu.add("宫保鸡丁");
chineseFoodMenu.add("孜然羊肉");
chineseFoodMenu.add("水煮鱼");
chineseFoodMenu.add("北京烤鸭");
Iterator iterator = chineseFoodMenu.getIterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
}
//output:
//宫保鸡丁
//孜然羊肉
//水煮鱼
//北京烤鸭
我们通过迭代器的方式实现了对一个容器(Menu)的遍历。迭代器的好处就是我们在Main类中使用Menu的时候根本不知道他底层的实现,只需要通过迭代器来遍历就可以了。
## 总结
迭代器的使用现在非常广泛,因为Java中提供了java.util.Iterator。而且Java中的很多容器(Collection、Set)也都提供了对迭代器的支持。
迭代器甚至可以从23种设计模式中移除,因为他已经普遍的可以称之为工具了。
最后最后,迭代器模式很好用,本文中介绍了如何写迭代器模式,但是,如果你要做Java开发,请直接用Java提供的Iterator。
文中所有代码见[GitHub][4]
[1]: http://www.hollischuang.com/archives/1691
[2]: http://www.hollischuang.com/archives/1776
[3]: http://www.hollischuang.com/wp-content/uploads/2017/02/iterator.jpg
[4]: https://github.com/hollischuang/DesignPattern
\ No newline at end of file
## 概念
单例模式(`Singleton Pattern`)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式。在 [GOF 书][3]中给出的定义为:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
单例模式一般体现在类声明中,单例的类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
## 用途
单例模式有以下两个优点:
> 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如网站首页页面缓存)。
>
> 避免对资源的多重占用(比如写文件操作)。
有时候,我们在选择使用单例模式的时候,不仅仅考虑到其带来的优点,还有可能是有些场景就必须要单例。比如类似"一个党只能有一个主席"的情况。
## 实现方式
我们知道,一个类的对象的产生是由类构造函数来完成的。如果一个类对外提供了`public`的构造方法,那么外界就可以任意创建该类的对象。所以,如果想限制对象的产生,一个办法就是将构造函数变为私有的(至少是受保护的),使外面的类不能通过引用来产生对象。同时为了保证类的可用性,就必须提供一个自己的对象以及访问这个对象的静态方法。
[<img src="http://www.hollischuang.com/wp-content/uploads/2016/04/QQ20160406-0.png" alt="QQ20160406-0" width="325" height="232" class="alignnone size-full wp-image-1388" />][4]
### 饿汉式
下面是一个简单的单例的实现:
//code 1
public class Singleton {
//在类内部实例化一个实例
private static Singleton instance = new Singleton();
//私有的构造函数,外部无法访问
private Singleton() {
}
//对外提供获取实例的静态方法
public static Singleton getInstance() {
return instance;
}
}
使用以下代码测试:
//code2
public class SingletonClient {
public static void main(String[] args) {
SimpleSingleton simpleSingleton1 = SimpleSingleton.getInstance();
SimpleSingleton simpleSingleton2 = SimpleSingleton.getInstance();
System.out.println(simpleSingleton1==simpleSingleton2);
}
}
输出结果:
true
code 1就是一个简单的单例的实现,这种实现方式我们称之为饿汉式。所谓饿汉。这是个比较形象的比喻。对于一个饿汉来说,他希望他想要用到这个实例的时候就能够立即拿到,而不需要任何等待时间。所以,通过`static`的静态初始化方式,在该类第一次被加载的时候,就有一个`SimpleSingleton`的实例被创建出来了。这样就保证在第一次想要使用该对象时,他已经被初始化好了。
同时,由于该实例在类被加载的时候就创建出来了,所以也避免了线程安全问题。(原因见:[在深度分析Java的ClassLoader机制(源码级别)][5][Java类的加载、链接和初始化][6]
还有一种饿汉模式的变种:
//code 3
public class Singleton2 {
//在类内部定义
private static Singleton2 instance;
static {
//实例化该实例
instance = new Singleton2();
}
//私有的构造函数,外部无法访问
private Singleton2() {
}
//对外提供获取实例的静态方法
public static Singleton2 getInstance() {
return instance;
}
}
code 3和code 1其实是一样的,都是在类被加载的时候实例化一个对象。
**饿汉式单例,在类被加载的时候对象就会实例化。这也许会造成不必要的消耗,因为有可能这个实例根本就不会被用到。而且,如果这个类被多次加载的话也会造成多次实例化。其实解决这个问题的方式有很多,下面提供两种解决方式,第一种是使用静态内部类的形式。第二种是使用懒汉式。**
### 静态内部类式
先来看通过静态内部类的方式解决上面的问题:
//code 4
public class StaticInnerClassSingleton {
//在静态内部类中初始化实例对象
private static class SingletonHolder {
private static final StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();
}
//私有的构造方法
private StaticInnerClassSingleton() {
}
//对外提供获取实例的静态方法
public static final StaticInnerClassSingleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
这种方式同样利用了classloder的机制来保证初始化`instance`时只有一个线程,它跟饿汉式不同的是(很细微的差别):饿汉式是只要`Singleton`类被装载了,那么`instance`就会被实例化(没有达到lazy loading效果),而这种方式是`Singleton`类被装载了,`instance`不一定被初始化。因为`SingletonHolder`类没有被主动使用,只有显示通过调用`getInstance`方法时,才会显示装载`SingletonHolder`类,从而实例化`instance`。想象一下,如果实例化`instance`很消耗资源,我想让他延迟加载,另外一方面,我不希望在`Singleton`类加载时就实例化,因为我不能确保`Singleton`类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化`instance`显然是不合适的。这个时候,这种方式相比饿汉式更加合理。
### 懒汉式
下面看另外一种在该对象真正被使用的时候才会实例化的单例模式——懒汉模式。
//code 5
public class Singleton {
//定义实例
private static Singleton instance;
//私有构造方法
private Singleton(){}
//对外提供获取实例的静态方法
public static Singleton getInstance() {
//在对象被使用的时候才实例化
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
上面这种单例叫做懒汉式单例。懒汉,就是不会提前把实例创建出来,将类对自己的实例化延迟到第一次被引用的时候。`getInstance`方法的作用是希望该对象在第一次被使用的时候被`new`出来。
有没有发现,其实code 5这种懒汉式单例其实还存在一个问题,那就是线程安全问题。在多线程情况下,有可能两个线程同时进入`if`语句中,这样,在两个线程都从if中退出的时候就创建了两个不一样的对象。(这里就不详细讲解了,不理解的请恶补多线程知识)。
### 线程安全的懒汉式
针对线程不安全的懒汉式的单例,其实解决方式很简单,就是给创建对象的步骤加锁:
//code 6
public class SynchronizedSingleton {
//定义实例
private static SynchronizedSingleton instance;
//私有构造方法
private SynchronizedSingleton(){}
//对外提供获取实例的静态方法,对该方法加锁
public static synchronized SynchronizedSingleton getInstance() {
//在对象被使用的时候才实例化
if (instance == null) {
instance = new SynchronizedSingleton();
}
return instance;
}
}
这种写法能够在多线程中很好的工作,而且看起来它也具备很好的延迟加载,但是,遗憾的是,他效率很低,因为99%情况下不需要同步。(因为上面的`synchronized`的加锁范围是整个方法,该方法的所有操作都是同步进行的,但是对于非第一次创建对象的情况,也就是没有进入`if`语句中的情况,根本不需要同步操作,可以直接返回`instance`。)
### 双重校验锁
针对上面code 6存在的问题,相信对并发编程了解的同学都知道如何解决。其实上面的代码存在的问题主要是锁的范围太大了。只要缩小锁的范围就可以了。那么如何缩小锁的范围呢?相比于同步方法,同步代码块的加锁范围更小。code 6可以改造成:
//code 7
public class Singleton {
private static Singleton singleton;
private Singleton() {
}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
code 7是对于code 6的一种改进写法,通过使用同步代码块的方式减小了锁的范围。这样可以大大提高效率。(对于已经存在`singleton`的情况,无须同步,直接return)。
但是,事情这的有这么容易吗?上面的代码看上去好像是没有任何问题。实现了惰性初始化,解决了同步问题,还减小了锁的范围,提高了效率。但是,该代码还存在隐患。隐患的原因主要和[Java内存模型(JMM][7])有关。考虑下面的事件序列:
> 线程A发现变量没有被初始化, 然后它获取锁并开始变量的初始化。
>
> 由于某些编程语言的语义,编译器生成的代码允许在线程A执行完变量的初始化之前,更新变量并将其指向部分初始化的对象。
>
> 线程B发现共享变量已经被初始化,并返回变量。由于线程B确信变量已被初始化,它没有获取锁。如果在A完成初始化之前共享变量对B可见(这是由于A没有完成初始化或者因为一些初始化的值还没有穿过B使用的内存(缓存一致性)),程序很可能会崩溃。
(上面的例子不太能理解的同学,请恶补JAVA内存模型相关知识)
[J2SE 1.4][8]或更早的版本中使用双重检查锁有潜在的危险,有时会正常工作(区分正确实现和有小问题的实现是很困难的。取决于编译器,线程的调度和其他并发系统活动,不正确的实现双重检查锁导致的异常结果可能会间歇性出现。重现异常是十分困难的。) 在[J2SE 5.0][8]中,这一问题被修正了。[volatile][9]关键字保证多个线程可以正确处理单件实例
所以,针对code 7 ,可以有code 8 和code 9两种替代方案:
使用`volatile`
//code 8
public class VolatileSingleton {
private static volatile VolatileSingleton singleton;
private VolatileSingleton() {
}
public static VolatileSingleton getSingleton() {
if (singleton == null) {
synchronized (VolatileSingleton.class) {
if (singleton == null) {
singleton = new VolatileSingleton();
}
}
}
return singleton;
}
}
**上面这种双重校验锁的方式用的比较广泛,他解决了前面提到的所有问题。**但是,即使是这种看上去完美无缺的方式也可能存在问题,那就是遇到序列化的时候。详细内容后文介绍。
使用`final`
//code 9
class FinalWrapper<T> {
public final T value;
public FinalWrapper(T value) {
this.value = value;
}
}
public class FinalSingleton {
private FinalWrapper<FinalSingleton> helperWrapper = null;
public FinalSingleton getHelper() {
FinalWrapper<FinalSingleton> wrapper = helperWrapper;
if (wrapper == null) {
synchronized (this) {
if (helperWrapper == null) {
helperWrapper = new FinalWrapper<FinalSingleton>(new FinalSingleton());
}
wrapper = helperWrapper;
}
}
return wrapper.value;
}
}
### 枚举式
在1.5之前,实现单例一般只有以上几种办法,在1.5之后,还有另外一种实现单例的方式,那就是使用枚举:
// code 10
public enum Singleton {
INSTANCE;
Singleton() {
}
}
这种方式是[Effective Java][10]作者`Josh Bloch` 提倡的方式,它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象(下面会介绍),可谓是很坚强的壁垒啊,在深度分析Java的枚举类型—-枚举的线程安全性及序列化问题中有详细介绍枚举的线程安全问题和序列化问题,不过,个人认为由于1.5中才加入`enum`特性,用这种方式写不免让人感觉生疏,在实际工作中,我也很少看见有人这么写过,但是不代表他不好。
## 单例与序列化
[单例与序列化的那些事儿][11]一文中,[Hollis][12]就分析过单例和序列化之前的关系——序列化可以破坏单例。要想防止序列化对单例的破坏,只要在`Singleton`类中定义`readResolve`就可以解决该问题:
//code 11
package com.hollis;
import java.io.Serializable;
/**
* Created by hollis on 16/2/5.
* 使用双重校验锁方式实现单例
*/
public class Singleton implements Serializable{
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
private Object readResolve() {
return singleton;
}
}
## 总结
本文中介绍了几种实现单例的方法,主要包括饿汉、懒汉、使用静态内部类、双重校验锁、枚举等。还介绍了如何防止序列化破坏类的单例性。
从单例的实现中,我们可以发现,一个简单的单例模式就能涉及到这么多知识。在不断完善的过程中可以了解并运用到更多的知识。所谓学无止境。
**文中所有代码见[GitHub][13]**
## 参考资料
[单例模式的七种写法][14]
[双重检查锁定模式][15]
[深入浅出设计模式][16]
[单例模式][17]
[1]: http://www.hollischuang.com/archives/category/%E7%BB%BC%E5%90%88%E5%BA%94%E7%94%A8/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F
[2]: http://www.hollischuang.com/archives/1368
[3]: http://s.click.taobao.com/t?e=m%3D2%26s%3DT5l23XuxzMIcQipKwQzePOeEDrYVVa64K7Vc7tFgwiHjf2vlNIV67sqCcISrC8hOF%2FSaKyaJTUZpS4hLH%2FP02ckKYNRBWOBBey11vvWwHXSniyi5vWXIZhtlrJbLMDAQihpQCXu2JnMU7C4KV%2Fo0CcYMXU3NNCg%2F&pvid=10_42.120.73.203_2589754_1459955095482
[4]: http://www.hollischuang.com/wp-content/uploads/2016/04/QQ20160406-0.png
[5]: http://www.hollischuang.com/archives/197
[6]: http://www.hollischuang.com/archives/201
[7]: http://www.hollischuang.com/archives/1003
[8]: https://zh.wikipedia.org/wiki/Java_SE
[9]: https://zh.wikipedia.org/wiki/Volatile%E5%8F%98%E9%87%8F
[10]: http://s.click.taobao.com/t?e=m=2&s=ix/dAcrx42AcQipKwQzePOeEDrYVVa64K7Vc7tFgwiHjf2vlNIV67vbkYfk%2bHavVTHm2guh0YLtpS4hLH/P02ckKYNRBWOBBey11vvWwHXSniyi5vWXIZtVr9sOV2MxmP1RxEmSieVPs8Gq%2bZDw%2bWcYMXU3NNCg/&pvid=10_42.120.73.203_425_1459957079215
[11]: http://www.hollischuang.com/archives/1144
[12]: http://www.hollischuang.com
[13]: https://github.com/hollischuang/DesignPattern
[14]: http://www.hollischuang.com/archives/205
[15]: https://zh.wikipedia.org/wiki/%E5%8F%8C%E9%87%8D%E6%A3%80%E6%9F%A5%E9%94%81%E5%AE%9A%E6%A8%A1%E5%BC%8F
[16]: http://s.click.taobao.com/t?e=m%3D2%26s%3Detkt7EP2O5scQipKwQzePOeEDrYVVa64K7Vc7tFgwiHjf2vlNIV67nWAf3rZA5A%2FrumJQoe%2FxcNpS4hLH%2FP02ckKYNRBWOBBey11vvWwHXTpkOAWGyim%2Bw2PNKvM2u52N5aP5%2Bgx7zgh4LxdBQDQSXEqY%2Bakgpmw&pvid=10_42.120.73.203_1238_1459955035603
[17]: http://www.runoob.com/design-pattern/singleton-pattern.html
\ No newline at end of file
## 概念
学习过设计模式的人大概都知道[Head First设计模式][2]这本书,这本书中介绍的第一个模式就是策略模式。把策略模式放在第一个,笔者认为主要有两个原因:1、这的确是一个比较简单的模式。2、这个模式可以充分的体现面向对象设计原则中的`封装变化``多用组合,少用继承``针对接口编程,不针对实现编程`等原则。
> 策略模式(Strategy Pattern):定义一系列算法,将每一个算法封装起来,并让它们可以相互替换。策略模式让算法独立于使用它的客户而变化,也称为政策模式(Policy)。
## 用途
结合策略模式的概念,我们找一个实际的场景来理解一下。
假设我们是一家新开的书店,为了招揽顾客,我们推出会员服务,我们把店里的会员分为三种,分别是初级会员、中级会员和高级会员。针对不同级别的会员我们给予不同的优惠。初级会员买书我们不打折、中级会员买书我们打九折、高级会员买书我们打八折。
[<img src="http://www.hollischuang.com/wp-content/uploads/2016/09/bookstore-300x300.jpg" alt="bookstore" width="300" height="300" class="alignright size-medium wp-image-1694" />][3] 我们希望用户在付款的时候,只要刷一下书的条形码,会员再刷一下他的会员卡,收银台的工组人员就能直接知道应该向顾客收取多少钱。
在不使用模式的情况下,我们可以在结算的方法中使用`if/else`语句来区别出不同的会员来计算价格。
但是,如果我们有一天想要把初级会员的折扣改成9.8折怎么办?有一天我要推出超级会员怎么办?有一天我要针对中级会员可打折的书的数量做限制怎么办?
使用`if\else`设计出来的系统,所有的算法都写在了一起,只要有改动我就要修改整个类。我们都知道,只要是修改代码就有可能引入问题。为了避免这个问题,我们可以使用策略模式。。。
> 对于收银台系统,计算应收款的时候,一个客户只可能是初级、中级、高级会员中的一种。不同的会员使用不同的算法来计算价格。收银台系统其实不关心具体的会员类型和折扣之间的关系。也不希望会员和折扣之间的任何改动会影响到收银台系统。
在介绍策略模式的具体实现方式之前,再来巩固一下几个面向对象设计原则:`封装变化``多用组合,少用继承``针对接口编程,不针对实现编程`。想一想如何运用到策略模式中,并且有什么好处。
## 实现方式
策略模式包含如下角色:
> Context: 环境类
>
> Strategy: 抽象策略类
>
> ConcreteStrategy: 具体策略类
[<img src="http://www.hollischuang.com/wp-content/uploads/2016/09/Strategy.jpg" alt="strategy" width="764" height="329" class="aligncenter size-full wp-image-1692" />][4]
我们运用策略模式来实现一下书店的收银台系统。我们可以把会员抽象成一个策略类,不同的会员类型是具体的策略类。不同策略类里面实现了计算价格这一算法。然后通过组合的方式把会员集成到收银台中。
先定义一个接口,这个接口就是抽象策略类,该接口定义了计算价格方法,具体实现方式由具体的策略类来定义。
/**
* Created by hollis on 16/9/19. 会员接口
*/
public interface Member {
/**
* 计算应付价格
* @param bookPrice 书籍原价(针对金额,建议使用BigDecimal,double会损失精度)
* @return 应付金额
*/
public double calPrice(double bookPrice);
}
针对不同的会员,定义三种具体的策略类,每个类中都分别实现计算价格方法。
/**
* Created by hollis on 16/9/19. 初级会员
*/
public class PrimaryMember implements Member {
@Override
public double calPrice(double bookPrice) {
System.out.println("对于初级会员的没有折扣");
return bookPrice;
}
}
/**
* Created by hollis on 16/9/19. 中级会员,买书打九折
*/
public class IntermediateMember implements Member {
@Override
public double calPrice(double bookPrice) {
System.out.println("对于中级会员的折扣为10%");
return bookPrice * 0.9;
}
}
/**
* Created by hollis on 16/9/19. 高级会员,买书打八折
*/
public class AdvancedMember implements Member {
@Override
public double calPrice(double bookPrice) {
System.out.println("对于中级会员的折扣为20%");
return bookPrice * 0.8;
}
}
上面几个类的定义体现了`封装变化`的设计原则,不同会员的具体折扣方式改变不会影响到其他的会员。
定义好了抽象策略类和具体策略类之后,我们再来定义环境类,所谓环境类,就是集成算法的类。这个例子中就是收银台系统。采用组合的方式把会员集成进来。
/**
* Created by hollis on 16/9/19. 书籍价格类
*/
public class Cashier {
/**
* 会员,策略对象
*/
private Member member;
public Cashier(Member member){
this.member = member;
}
/**
* 计算应付价格
* @param booksPrice
* @return
*/
public double quote(double booksPrice) {
return this.member.calPrice(booksPrice);
}
}
这个Cashier类就是一个环境类,该类的定义体现了`多用组合,少用继承``针对接口编程,不针对实现编程`两个设计原则。由于这里采用了组合+接口的方式,后面我们在推出超级会员的时候无须修改Cashier类。只要再定义一个`SuperMember implements Member` 就可以了。
下面定义一个客户端来测试一下:
/**
* Created by hollis on 16/9/19.
*/
public class BookStore {
public static void main(String[] args) {
//选择并创建需要使用的策略对象
Member strategy = new AdvancedMember();
//创建环境
Cashier cashier = new Cashier(strategy);
//计算价格
double quote = cashier.quote(300);
System.out.println("高级会员图书的最终价格为:" + quote);
strategy = new IntermediateMember();
cashier = new Cashier(strategy);
quote = cashier.quote(300);
System.out.println("中级会员图书的最终价格为:" + quote);
}
}
//对于中级会员的折扣为20%
//高级会员图书的最终价格为:240.0
//对于中级会员的折扣为10%
//中级会员图书的最终价格为:270.0
从上面的示例可以看出,策略模式仅仅封装算法,提供新的算法插入到已有系统中,策略模式并不决定在何时使用何种算法。在什么情况下使用什么算法是由客户端决定的。
* 策略模式的重心
* 策略模式的重心不是如何实现算法,而是如何组织、调用这些算法,从而让程序结构更灵活,具有更好的维护性和扩展性。
* 算法的平等性
* 策略模式一个很大的特点就是各个策略算法的平等性。对于一系列具体的策略算法,大家的地位是完全一样的,正因为这个平等性,才能实现算法之间可以相互替换。所有的策略算法在实现上也是相互独立的,相互之间是没有依赖的。
* 所以可以这样描述这一系列策略算法:策略算法是相同行为的不同实现。
* 运行时策略的唯一性
* 运行期间,策略模式在每一个时刻只能使用一个具体的策略实现对象,虽然可以动态地在不同的策略实现中切换,但是同时只能使用一个。
* 公有的行为
* 经常见到的是,所有的具体策略类都有一些公有的行为。这时候,就应当把这些公有的行为放到共同的抽象策略角色Strategy类里面。当然这时候抽象策略角色必须要用Java抽象类实现,而不能使用接口。([《JAVA与模式》之策略模式][5]
## 策略模式的优缺点
### 优点
* 策略模式提供了对“开闭原则”的完美支持,用户可以在不修改原有系统的基础上选择算法或行为,也可以灵活地增加新的算法或行为。
* 策略模式提供了管理相关的算法族的办法。策略类的等级结构定义了一个算法或行为族。恰当使用继承可以把公共的代码移到父类里面,从而避免代码重复。
* 使用策略模式可以避免使用多重条件(if-else)语句。多重条件语句不易维护,它把采取哪一种算法或采取哪一种行为的逻辑与算法或行为的逻辑混合在一起,统统列在一个多重条件语句里面,比使用继承的办法还要原始和落后。
### 缺点
* 客户端必须知道所有的策略类,并自行决定使用哪一个策略类。这就意味着客户端必须理解这些算法的区别,以便适时选择恰当的算法类。
* 由于策略模式把每个具体的策略实现都单独封装成为类,如果备选的策略很多的话,那么对象的数目就会很可观。可以通过使用享元模式在一定程度上减少对象的数量。
文中所有代码见[GitHub][6]
## 参考资料
[《JAVA与模式》之策略模式][5]
[1]: http://www.hollischuang.com/archives/category/%E7%BB%BC%E5%90%88%E5%BA%94%E7%94%A8/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F
[2]: http://s.click.taobao.com/DxA2xSx
[3]: http://www.hollischuang.com/wp-content/uploads/2016/09/bookstore.jpg
[4]: http://www.hollischuang.com/wp-content/uploads/2016/09/Strategy.jpg
[5]: http://www.cnblogs.com/java-my-life/archive/2012/05/10/2491891.html
[6]: https://github.com/hollischuang/DesignPattern
\ No newline at end of file
......@@ -371,6 +371,7 @@ Gitee Pages 完整阅读:[进入](http://hollischuang.gitee.io/tobetopjavaer)
* [冬令时和夏令时](/basics/java-basic/StandardTime-vs-daylightSavingTime.md)
* [时间戳](/basics/java-basic/timestamp.md)
* Java中时间API
* [格林威治时间](/basics/java-basic/GMT.md)
......@@ -427,7 +428,7 @@ Gitee Pages 完整阅读:[进入](http://hollischuang.gitee.io/tobetopjavaer)
* lambda表达式
* Stream API
* [Stream API](/basics/java-basic/stream.md)
* 时间API
......@@ -810,19 +811,19 @@ Gitee Pages 完整阅读:[进入](http://hollischuang.gitee.io/tobetopjavaer)
* 创建型设计模式
* 单例模式
* [单例模式](/advance/design-patterns/singleton-pattern.md)
* 抽象工厂模式
* [抽象工厂模式](/advance/design-patterns/abstract-factory-pattern.md)
* 建造者模式
* [建造者模式](/advance/design-patterns/builder-pattern.md)
* 工厂模式
* [工厂模式](/advance/design-patterns/factory-method-pattern.md)
* 原型模式
* 结构型设计模式
* 适配器模式
* [适配器模式](/advance/design-patterns/adapter-pattern.md)
* 桥接模式
......@@ -842,7 +843,7 @@ Gitee Pages 完整阅读:[进入](http://hollischuang.gitee.io/tobetopjavaer)
* 命令模式
* 迭代器模式
* [迭代器模式](/advance/design-patterns/iterator-pattern.md)
* 观察者模式
......@@ -854,7 +855,7 @@ Gitee Pages 完整阅读:[进入](http://hollischuang.gitee.io/tobetopjavaer)
* 状态模式
* 策略模式
* [策略模式](/advance/design-patterns/strategy-pattern.md)
* 责任链模式
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册