23种设计模式-详细总结

23种设计模式-详细总结

概述

mark

  • 设计模式是解决问题的方案,学习现有的设计模式可以做到经验的复用
  • 拥有设计模式的词汇,在沟通是就能用到更少的词汇来讨论,并且不需要知道底层的细节

话在前头:

  1. 什么是好的设计模式?
  • 提高复用
  • 应对变化
  1. 在什么时候,什么地方使用设计模式?
  • 在需求频繁变化的变化点使用设计模式

  • Refactoring to Patterns(重构的方式一步一步到模式)

重构的关键方法:

  1. 静态 -> 动态
  2. 早绑定 -> 晚绑定
  3. 继承 -> 组合
  4. 编译时依赖 -> 运行时依赖
  5. 紧耦合 -> 松耦合

1. 设计原则

1.1 依赖倒置原则(DIP)

  • Dependence Inversion Principle,缩写DIP

  • 高层次的模块不依赖于低层次模块的实现细节

    • 高层模块不应该依赖低层模块,两者都应该依赖其抽象
    • 细节应该依赖抽象
    • 抽象不应该依赖细节

是不是觉得和没说一个样,至少我是这么觉得的;继续往后看才明白,

  • 所谓高层模块就是调用端
  • 低层模块就是具体的实现类
  • 依赖倒置原则在java中的表现就是:模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系
    • 也就是通过接口或者抽象类产生依赖关系(调用关系)
    • 也就是面向接口编程或者说面向抽象编程

其实依赖倒置原则主要目的就是解耦

mark

可以使用这张图来表示,表达出来就是ImageLoaderMemonyCache等并没有直接关系,甚至ImageLoader只需要实现ImageCache类或继承其他已有的ImageCache子类完成相应的缓存功能,然后将具体的实现注入到ImageLoader即可实现缓存功能的替换。这也是依赖倒置原则的体现。

1.2 开闭原则(OCP)

  • Open Close Principle,缩写OCP

  • 定义:软件中得对象应该对于扩展是开放的,但是对于修改是封闭的。

mark

简单地说,当软件需要变化时,应该尽量通过扩展的方式来实现变化,而不是通过修改已有的代码来实现。

“应该尽量”4个字说明OCP原则并不是说绝对不可以修改原始类的,当代码需要需要重构的时候要及时重构,使代码恢复正常,而不是通过继承等方式添加新的实现,这会导致类型的膨胀以及历史遗留代码的冗余。

1.3 单一职责原则(SRP)

  • 单一职责原则,就一个类而言,应该只有一个引起它变化的原因。
  • 简单说,一个类应该是一组高度相关的函数、数据的封装;也就是高内聚。
  • Single Responsibility Principle,缩写SRP

举个例子:

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 ImageLoader{
//图片缓存
LruCache<String,Bitmap> mImageCache;
//线程池,线程数量为CPU的数量
ExecutorService mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProessors());

public ImageLoader(){
initImageCache();
}

private void initImageCache() {
//省略...
}

//显示图片
public void displayImage(final String url, final ImageView imageView) {
//省略...
}

//下载图片
public Bitmap downloadImage(String imageUrl) {
//省略...
return bitmap;
}
}

这里可以看出来 ImageLoader 类作用有初始化图片缓存、显示图片、下载图片,显然显示图片和下载图片两个方法与初始化图片缓存方法相比作用就显得有些不相关。(也就是不符合单一职责原则)。

按照逻辑进行拆分后得到ImageLoaderImageCache两个类。

  • ImageLoader负责图片加载逻辑
  • ImageCache负责处理图片缓存逻辑
  • 这样职责就很清楚了,当与缓存相关的逻辑需要改变时,不需要修改ImageLoader类,而图片加载的逻辑需要修改时也不会影响到缓存处理逻辑。

修改后代码如下所示:

  • 添加的ImageCache类用于处理图片缓存,具体代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ImageCache {
// 图片LRU缓存
LruCache<String, Bitmap> mImageCache;

public ImageCache() {
initImageCache();
}

private void initImageCache() {
//省略...
}

public void put(String url, Bitmap bitmap) {
mImageCache.put(url, bitmap) ;
}

public Bitmap get(String url) {
return mImageCache.get(url) ;
}
}
  • ImageLoader代码修改如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/** 图片加载类 */
public class ImageLoader {
//图片缓存
ImageCache mImageCache = new ImageCache() ;
//线程池,线程数量为CPU的数量
ExecutorService mExecutorService = Executors.newFixedThreadPool (Runtime.getRuntime().availableProcessors());

//加载图片
public void displayImage(final String url, final ImageView imageView) {
//省略...
}

public Bitmap downloadImage(String imageUrl) {
//省略...
return bitmap;
}
}

1.4 里氏替换原则 (LSP)

  • Liskov Substitution Principle
  • 里氏替换原则,书上原话的定义简直看不得(解释的辣眼睛,完全看不懂),简单的来说就是所有引用基类的地方必须能透明的时候其子类的对现象。只要父类能出现的地方子类就能出现,而且替换成子类也不会产生任何的错误和异常。使用者可能根本就不需要知道是父类还是子类。
  • 但是,反过来就不行了,有子类出现的地方,父类未必就能适应。其实就是:抽象。

mark

  • 上图可以看出,Window依赖于View,而ButtonTextView继承View。这里任何继承自View类的子类都可以设置给show()方法,也就是里氏替换原则
  • 通过里氏替换,就可以自定义各式各样的View,然后传递给Window,并且将View显示到屏幕上。

里氏替换原则的核心原理是抽象,抽象又依赖于继承这个特性,继承的优缺点都相当明显

优点:

  • 代码重用,减少创建类的成本,每个子类都拥有父类的方法和属性
  • 子类与父类基本相似,但又与父类有所区别
  • 提高代码的可扩展性

缺点:

  • 继承是侵入性的,只要继承就必须拥有父类的所有属性和方法
  • 可能造成子类代码冗余、灵活性降低,因为子类必须拥有父类的属性和方法

事物总是具有两面性,如何权衡利与弊都是需要根据具体场景来做出选择并加以处理。

1.5 接口隔离原则(ISP)

  • Interface Segregation Principle

  • 接口隔离原则将非常庞大、臃肿的接口拆分成为更小的和更具体的接口;目的就是解耦。

  • 这个原则的做法和单一职责有点相似,就是说接口中的方法保持更高的相关性,尽量少,避免调不需要的方法。

  • java中类尽量不使用public

1.6 迪米特法则(LOP)

  • Law of Principle

  • 迪米特法则还有一个英文解释是:Only talk to your immedate friends,翻译过来就是:只与直接的朋友通信。(什么叫做直接的朋友,每个对象都必然会与其他对象有耦合关系,两个对象之间的耦合就成为朋友关系,这种关系类型有很多,例如:组合,聚合,依赖等)

  • 迪米特原则也称为最少知识原则(Least Knowledge Principle),定义:一个对象应该对其他对象有最少的了解。通俗地讲,一个类要对自己需要调用的类知道得最少,类的内部如何实现、如何复杂都与调用者(或依赖者)没关系,调用者(或依赖者)只需要知道他需要的方法即可,其他的不需要关心。

  • 类与类之间的关系越密切,耦合度越大;当一个类发生改变时,对另一个类的影响也越大。

一 . 创建型

1. 原型模式(Prototype)

  • 使用原型实例指定要创建对象的类型,通过复制这个原型来创建新对象。

  • 简单来说,其实就是当需要创建一个指定对象的时候,我们刚好有这样一个对象,但是又不能直接使用这个对象,那么我就会clone 一个一模一样的对象,基本上这就是原型模型

  • 关键字: clone

这些场景可能派的上用场

  • 当new一个对象时,非常繁琐复杂时,可以使用原型模式来进行复制一个对象。即使需求的变更,这些对象需要作出调整,我们依然拥有比较稳定一致的接口创建对象。
  • 需要提供数据对象,同时有需要避免外部对数据对象进行修改。

简单的UML类图

mark

角色:

  • client : 使用者
  • Prototype : 接口(抽象类),声明具备clone 能力,例如Java中的Cloneable 接口
  • ConcretePrototype: 具体的原型类

关于浅拷贝和深拷贝:

  • 浅拷贝:一个对象通过赋值的形式直接传递的其实是对象在内存中的内存地址
1
2
3
ArrayList<String> a = new ArrayList();
ArrayList<String> b = a;
//当修改a时,b的值同样会被修改

以上的代码就是浅拷贝,从某种角度来说,这种浅拷贝的方式并不合适在原型模式中使用,更多情况下我们需要一个不会影响原始对象的一个新对象,也就需要使用到深拷贝

  • 深拷贝:一个不会影响原始对象的一个新对象
1
2
3
4
ArrayList<String> a = new ArrayList();
ArrayList<String> b = a.clone();
//或者
ArrayList<String> c = new ArrayList(a);

上面代码的

第一种方式,是使用Object 类的super.clone()方法来使用实现拷贝的过程。

第二种方式:使用a在创建了一个新的对象并且赋值给c , 这样a和 c是两个值相同的两个对象。

1
2
3
4
5
6
7
8
9
10
11
12
//ArrayList的clone()方法代码
public Object clone() {
try {
ArrayList<?> v = (ArrayList<?>) super.clone();
v.elementData = Arrays.copyOf(elementData, size);
v.modCount = 0;
return v;
} catch (CloneNotSupportedException e) {
// this shouldn't happen, since we are Cloneable
throw new InternalError(e);
}
}

需要注意的是:通过实现Cloneable接口的原型模式在调用clone() 方法构造实例并不一定比new操作速度快,只有new 对象操作复杂且耗时成本较高的时候,clone方法才有效率上的提升。

简单实现

  1. Prototype
1
2
3
public abstract class Prototype {
abstract Prototype myclone();
}
  1. ConcretePrototype
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class ConcretePrototype extends Prototype {

private String filed;


public ConcretePrototype(String filed) {
this.filed = filed;
}


Prototype myclone() {
return new ConcretePrototype(filed);
}

@Override
public String toString() {
return "ConcretePrototype{" +
"filed='" + filed + '\'' +
'}';
}
}
  1. Client
1
2
3
4
5
6
7
public class Client {
public static void main(String[] args) {
Prototype prototype = new ConcretePrototype("abc");
Prototype clone = prototype.myclone();
System.out.println(clone.toString());
}
}

JDK

2. 生成器(Builder)

  • 封装一个对象的构造过程,并允许按照步骤构造。
  • Builder 模式也就是建造者模式,先说定义,将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
    • 首先,将复杂对象的创建过程和部件分离出来,其实就是把创建过程和自身的部件解耦,使得构建过程和部件都可以自由扩展,两者之间的耦合度降到最低。
    • 然后,再是相同的构建过程可以创建不同的表示,相同的组合也可以通过不同的部件创建出不同的对象。

可能使用的场景

  • 相同的方法,不同的执行顺序,产生不同的结果
  • 多个部件(代码中就对应类的属性),都可以装配到同一个对象中,但是产生的结果又不相同的时候
  • 初始化一个对象特别复杂,参数多且很多参数都有默认值的时候。

实现

mark

  • Product : 抽象的产品类。
  • Builder : 抽象的Builder 类,规范产品的组件。
  • ConcreteBuilder : 具体的Builder类,实现具体的组件过程。
  • Director : 统一组装的过程。
  1. 手机配置简化的抽象类,设置CPU、OS以、内存大小以及运存大小
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

// 这是一个手机配置简化的抽象类,
// 设置CPU、OS以、内存大小以及运存大小
public abstract class Phone {

protected String mCPU;
protected String mOS;
protected int mMemorySize;
protected int mStorageSize;


public abstract void setmCPU();

public abstract void setmOS();

public void setmMemorySize(int mMemorySize) {
this.mMemorySize = mMemorySize;
}

public void setmStorageSize(int mStorageSize) {
this.mStorageSize = mStorageSize;
}

@Override
public String toString() {
return "Phone{" +
"mCPU='" + mCPU + '\'' +
", mOS='" + mOS + '\'' +
", mMemorySize=" + mMemorySize +
", mStorageSize=" + mStorageSize +
'}';
}
}
  1. 具体的IPhoneX的类,由于CPU和系统是固定,而内存和运存运存可选。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 具体的IPhoneX的类,
// 由于CPU和系统是固定,而内存和运存运存可选。

public class IPhoneX extends Phone {

public IPhoneX() {
}

public void setmCPU() {
mCPU = "A11";
}

public void setmOS() {
mOS = "iOS 11";
}
}
  1. 抽象的Builder类,作为主要隔离作用的类,Phone的API每一个方法都有对应的builder方法,并且都返回自身来实现链式API
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 抽象的Builder类,作为主要隔离作用的类,
// Phone的API的每一个方法都有对应的build方法,并都返回自身来实现链式API。


public abstract class Builder {

//设置CPU
public abstract Builder buildCPU();
//设置系统
public abstract Builder buildOS();
//设置运存大小
public abstract Builder buildMemorySize(int memorySize);
//设置储存大小
public abstract Builder buildStorageSize(int storageSize);
//创建一个Phone对象
public abstract Phone create();

}
  1. IPhoneXBuilder,具体的Builder
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
// IPhoneXBuilder,具体的Builder类

public class IPhoneXBuilder extends Builder {
private IPhoneX mIPhoneX = new IPhoneX();

public Builder buildCPU() {
mIPhoneX.setmCPU();
return this;
}

public Builder buildOS() {
mIPhoneX.setmOS();
return this;
}

public Builder buildMemorySize(int memorySize) {
mIPhoneX.setmMemorySize(memorySize);
return this;
}

public Builder buildStorageSize(int storageSize) {
mIPhoneX.setmStorageSize(storageSize);
return this;
}

// 注意一定要返回一个具体的对象
public Phone create() {
return mIPhoneX;
}
}
  1. Director:负责指挥phone的建造顺序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Director类,负责构造Phone

public class Director {

Builder mBuilder = null;

public Director(Builder Builder) {
mBuilder = Builder;
}

public void construct(int memorySize,int StorageSize){
mBuilder.buildCPU().
buildOS().
buildMemorySize(memorySize).
buildStorageSize(StorageSize);
}
}
  1. 客户端测试:
1
2
3
4
5
6
7
8
9
10
11
public class Client {
public static void main(String[] args) {
Builder builder = new IPhoneXBuilder();

Director director = new Director(builder);

director.construct(6,256);

System.out.println(builder.create().toString());
}
}

再来一个JDK中简易的实现 (StringBuilder)

  1. AbstractStringBuilder
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
public class AbstractStringBuilder {
protected char[] value;

protected int count;

public AbstractStringBuilder(int capacity) {
count = 0;
value = new char[capacity];
}

public AbstractStringBuilder append(char c) {
ensureCapacityInternal(count + 1);
value[count++] = c;
return this;
}

private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
if (minimumCapacity - value.length > 0)
expandCapacity(minimumCapacity);
}

void expandCapacity(int minimumCapacity) {
int newCapacity = value.length * 2 + 2;
if (newCapacity - minimumCapacity < 0)
newCapacity = minimumCapacity;
if (newCapacity < 0) {
if (minimumCapacity < 0) // overflow
throw new OutOfMemoryError();
newCapacity = Integer.MAX_VALUE;
}
value = Arrays.copyOf(value, newCapacity);
}
}
  1. StringBuilder
1
2
3
4
5
6
7
8
9
10
11
public class StringBuilder extends AbstractStringBuilder {
public StringBuilder() {
super(16);
}

@Override
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}
}
  1. Client:测试
1
2
3
4
5
6
7
8
9
10
public class Client {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder();
final int count = 26;
for (int i = 0; i < count; i++) {
sb.append((char) ('a' + i));
}
System.out.println(sb.toString()); // abcdefghijklmnopqrstuvwxyz
}
}

JDK中Builder模式的具体实现

3. 工厂模式(Factory)

  • 顾名思义,工厂模式就是生产产品的!
  • 工厂模式(Factory Pattern) 提供了一种创建对象的最佳方式,属于创建型模式。
  • 在工厂模式中,创建对象时不会对客户端暴露创建逻辑,并且是通过一个共同的接口来指定新创建的对象。(绕开了new)

大致描述:

  • 将上述思想对应到code中,工厂模式需要做的就是帮助我们构建对象,因为构建对象的过程可能比较复杂,我们无法掌握(例如:无法直接new 出来)。这是对工厂模式的一个大致描述,接下来可以从实现方式来说明。

3.1 简单工厂模式(Simple Factory)

1
2
3
4
5
6
7
8
9
10
11
12
13
// 简单工厂模式的大致模式
public interface Product {
}

// 商品A
public class ConcreteProductA implements Product {
}
// 商品B
public class ConcreteProductB implements Product {
}
// 商品C
public class ConcreteProductC implements Product {
}
  • 首先,简单工厂模式并没有被归纳到23中GOF设计模式中,其实可以理解为工厂模式的简单使用。
  • 一个工厂对象决定创建出哪一种类产品,而产品有不同的系列相互之间有些许差异。举个生产鞋的例子,先看UML图

mark

大致可以分为三部分:工厂类,抽象产品类,具体的产品类

  • Factory : 工厂类,生产shoe
  • Shoe : 抽象的产品类
  • SportShoe : 具体的产品,运动鞋
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/** 鞋的抽象类 */
public abstract class Shoe{

}

/** 运动鞋 */
public class SportShoe extends Shoe{

}

/** 工厂类 */
public class ShoeFactory {

public Shoe produceShoe(){
return new SportShoe();
}
}

以上就是最简单的工厂,但是一个只生产运动鞋的工厂,老板想去赚更多的钱,要求添加生成HighHeeledShoe(高跟鞋)。我们需要对ShoeFactory的设计进行修改,如图:

mark

改造后的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 在produceShoe()的函数中添加了type参数,
// 表示Shoe的不同类型,以此来生产不同的鞋子。我们开始改造ShoeFactory类


/** 高跟鞋 */
public class HighHeeledShoe extends Shoe{

}

/** 修改后的工厂类 */
public class ShoeFactory {

public Shoe produceShoe(String type){
switch (type) {
case "sport":
return new SportShoe();
case "highHeeled":
return new HighHeeledShoe();
default:
return new Shoe();
}
}
}

这样我们可以根据给定的不同类型生产对应鞋子了。这里看到代码可能会有两个问题

  • 第一:每次添加一个品牌需要加一个case(虽然现实中是很正常的逻辑。但是在代码层面并不优雅)。同时,简单工厂模式违反了开闭原则,即对扩展开放,对修改关闭;因为增加了具体产品,就需要修改对应工厂类代码;
  • 第二:调用produceShoe()的时候,我们还需要去创建一个Factory对象并进行控制管理
    • 直接new出Factory的对象(如return new SportShoe();),我们就必须自己控制工厂类的构造和生成,同时我们也需要非常清楚工厂的构造函数(比如构造函数有多少个参数,输入参数时有什么条件等等),还需要知道工厂的内部细节,一旦工厂扩展或者改变了,就很可能不知道怎么调用了,对于调用者来说无疑会是噩梦。

优化问题:

  • 先优化第一个问题:从技术的层面我们不使用swith case 这种形式来实现不同类型的对象创建,我们使用反射就可以实现优化(编译期到运行期)
  • 对于第二个问题:最直接的解决方案就是我们直接调用生产鞋子的方法,直接告诉工厂再由工厂生产,这样我们就不需要创建工厂。(那就是给produceShoe()函数添加static关键字就可以解决问题了)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/** 优化后的工厂类 */
public class ShoeFactory {
/**
* 根据类型生产鞋子
* @param cls
* @return
*/
public static Shoe produceShoe(Class<? extends Shoe> cls){
Shoe shoe = null;

try {
shoe = (Shoe) Class.forName(cls.getName()).newInstance();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return shoe;
}
}

这样一来问题就解决了

  • 用静态方法则完全不用关心如何构造对象,我们需要关心工厂的构造细节,即使工厂内部发生变化也不需要关心
  • 简单工厂模式主要适用于创建抽象子类的业务相同但具体实现不同的对象的情况。

3.2 工厂方法模式

  • 实际上,制造鞋子的厂商一定不会只有一家,肯定存在多家竞争的关系,所以有更多的制造不同鞋子的工厂,produceShoe函数就可以抽象出来,不同的Factory子类根据自己的需求去实现。
  • 例如下图的NikeFactoryDaphneFactory

mark

  • 相对于简单工厂,工厂方法的区别就在于工厂类分类抽象工厂和具体的实现工厂类。
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
/** 抽象工厂类 */
pulibc abstract class Factory{
public abstract Shoe produceShoe(String type)
}

/** Nike工厂类 */
public class NikeFactory extends Factory {

@Override
public Shoe produceShoe(String type){
switch (type) {
case "sport":
return new SportShoe();
case "highHeeled":
return new HighHeeledShoe();
default:
return new Shoe();
}
}
}

/** Daphne工厂类 */
public class DaphneFactory extends Factory{

@Override
public Shoe produceShoe(String type){
switch (type) {
case "sport":
return new SportShoe();
case "highHeeled":
return new HighHeeledShoe();
default:
return new Shoe();
}
}
}

使用的时候就很简单了,只需要调用对应的工厂,选择客户需要的类型就可以获得相应的产品了,这样不同的制造商就能分别生产不同的鞋子。

1
2
3
4
5
Factory daphneFactory = new DaphneFactory();
daphneFactory.produceShoe("highHeeled");

Factory nikeFactory = new NikeFactory();
nikeFactory.produceShoe("sport");

在工厂的环节采用抽象类的形式实现,其实也可以将produceShoe抽象成IProduceShoe接口,当然,这也是我的个人理解。其实觉得抽象类或者接口使用其中一个就可以了,不必要同时使用;但也不是绝对的,这个需要根据具体的情况而定。

对应JDK中:

通用形式:

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 abstract class Factory {
abstract public Product factoryMethod();
public void doSomething() {
Product product = factoryMethod();
// do something with the product
}
}

public class ConcreteFactory extends Factory {
public Product factoryMethod() {
return new ConcreteProduct();
}
}

public class ConcreteFactory1 extends Factory {
public Product factoryMethod() {
return new ConcreteProduct1();
}
}

public class ConcreteFactory2 extends Factory {
public Product factoryMethod() {
return new ConcreteProduct2();
}
}

3.3 抽象工厂模式(Abstract Factory)

  • 生产鞋子的厂商不一定只会生产衣服cloth,我们需要为了工厂添加制造衣服的这条生产线。为了工厂添加生产衣服后的UML图如下:

mark

  • ShoeFactoryClothFactory抽象成接口(原因:Java无法多继承,所以使用Interface)

  • 需要对应的产品就要实现工厂对应的接口。

  • 添加NikeFactory中的生产衣服的功能produceCloth,这样也就是实现NikeFactory可以生产多种产品。

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
public class NikeFactory extends Factory {
@Override
public Shoe produceShoe(String type){
switch (type) {
case "sport":
return new SportShoe();
case "highHeeled":
return new HighHeeledShoe();
default:
return new Shoe();
}
}


//新增方法
@Override
public Cloth produceCloth() {
switch (type) {
case "sport":
return new SportCloth();
case "skirt":
return new Skirt();
default:
return new Shoe();
}
}
}
  • 当你需要创建一些对象家族时候,抽象工厂也是不错的选择,因为它可以一次性创建多个对象。

  • 抽象工厂十分强大灵活(至少比前两种都要好),可以用多个抽象子类完成复杂的需求,同时保证了外界接触不到任何类型的具体产品类型,某种意思上很符合开闭原则 。

  • 当然,缺点也显而易见,模式比较庞大,从UML图就能看出来。

3.4 小结

  • 简单工厂只能有一个工厂生产相同类型的产品
  • 工厂方法模式只能生产一系列的产品,可以同时有不同的工厂去实现。
  • 抽象工厂模式通过实现不同的抽象方法可以生产出多个系列的产品。

4. 单例模式

  • 单例模式是应用最广泛的模式之一,定义就是单例对象的类必须保证只有一个实例存在
  • 单例模式适用于创建一个对象需要消耗过多资源的情况,例如数据库等资源是需要考虑使用的。

实现单例模式关键点如下:

  • 构造函数私有化(不会让你有机会再创建一个对象)
  • 通过一个静态方法或枚举(后面会有举例)返回单例类对象
  • 确保单例类的对象有且只有一个,尤其是多线程的环境下(难点)
  • 确保单例类的对象在反序列化的时候不会重新构建对象

4.1 饿汉式-线程安全

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 饿汉式实现

public class Singleton {

// 声明对象的时候就 已经初始化
private static Singleton instance = new Singleton();

// 私有化构造器
private Singleton(){}

// 共有静态方法,对外暴露获取的途径(Getter方法)
public static Singleton getInstance() {
return instance;
}
}

4.2 懒汉式-线程不安全

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 2. 懒汉式实现

public class Singleton{

// 声明对象的时候不进行初始化
private static Singleton instance = null;

// 私有化构造器
private Singleton() {
}

// Getter方法加锁保证线程安全
public static synchronized Singleton getInstance() {
// 进入是检查有没有被创建
if (null == instance){
instance = new Singleton();
}
return instance;
}
}
  • 懒汉式与饿汉式不同的地方是单例对象初始化的时机,实现了懒加载
  • 同时getInstance()方法前添加了synchronized关键字,以此来确保多线程环境下单例模式的唯一性.(相对的,每次调用getInstance()方法都是会进行同步的,也是懒汉式最大的问题)

4.3 懒汉式(DCL)-线程不安全

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 3. DCL
public class Singleton{

// 声明对象的时候不进行初始化
// volatile作用:避免指令的重排,保证原子性
private static volatile Singleton instance = null;

// 私有化构造器
private Singleton(){}

// 双重检测锁
public static Singleton getInstance() {
if (null == instance){
synchronized (Singleton.class){
if (null == instance){
instance = new Singleton(); // 造成问题的关键
}
}
}
// 返回对象实例
return instance;
}
}
  • 可以看到getInstance()方法对instance进行了两次判空,

    • 第一次判断是为了判断不必要的同步
    • 第二次是为了判断null的情况下创建实例
  • 同时instance对象前面还添加了 volatile关键字,如果不使用volatile是无法保证instance的原子性的

    • instance = new Singleton();这句代码不是一个原子性操作
    • 这句代码最终会被编译成多条汇编指令,大致做了3件事:
      1. 给Singleton的实例分配内存空间
      2. 调用Singleton()的构造函数,初始化成员字段
      3. 指向分配的内存空间(此时instance不为null)**
  • 由于JMM允许指令重排来保证代码效率,所以上述第二点和第三点的顺序无法保证,也就是说执行顺序可能是1-2-3或者是1-3-2。

    • 如果是1-3-2, instance 在 3 步骤的时候就已经是非空了,所以2步骤没有执行,那么另外一个线程会取走一个还没初始化完毕的实例,导致DCL失效
    • 在JDK1.5之后,调整了JVM,具体化了volatile关键字。然,volatile或多或少会影响到性能,考虑到正确性这点性能的牺牲还是值得的。
  • DCL模式能够在绝大多数场景下保证单例对象的唯一性,资源利用率高,只有第一次加载时反应稍慢,一般能够满足需求

4.4 静态内部类

  • 懒汉式的改进
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 4 . 静态内部类实现
public class Singleton{
// 1. 私有化构造器
private Singleton() {
}

// 2. 静态内部类
private static class SingletonHolder{
// final:保证线程中只有一个存在
private static final Singleton instance = new Singleton();
}

// 3. 只有调用的时候才会加载
public static Singleton getInstance(){
return SingletonHolder.instance;
}
}
  • 当第一次加载Singleton类时并不会初始化instance,只有在第一次调用getInstance()方法是回初始化。

  • 第一次调用getInstance()方法导致虚拟机加载SingletonHolder类,这种方式嫩确保线程安全,也能确保单例对象的唯一性,

  • 同时也延迟了单例对象的实例化,所以推荐使用这种实现方式。

4.5 枚举单例-线程安全

1
2
3
4
//  5.枚举单例模式
public enmu SingletonEnum {
INSTANCE;
}

测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 5. 枚举单例
public enum Singleton{
INSTANCE; //纯天然单例模式
}


class Test{
public static void main(String[] args) {
Singleton instance1 = Singleton.INSTANCE;
Singleton instance2 = Singleton.INSTANCE;
System.out.println(instance1 == instance2);
}
}
  • 就是这么简单粗暴,其实最大优点在于关键点的第4点,即是反序列化也不会重新生成新的实例。

4.6 总结

不管哪种形式实现单例模式,核心原理都是那四个关键点,具体选择哪种实现方式取决于项目本身以及具体的开发环境等等。

而对于客户端来说通常没有高并发的情况,推荐使用DCL模式或者是静态内部类的方式实现。

优点

  • 只存在一个实例,减少了内存开支,减少了系统的性能开销
  • 避免对资源的多重占用
  • 全局的访问点,优化和共享资源访问

缺点

  • 没有接口,难扩展,只能修改代码
  • 如果持有Context容易导致内存泄露(需要传递Context的话最好是Application Context)

JDK实现:

二. 行为型

1. 模板方法模式(Template Method)

  • 动机:对于一项任务,常常有稳定的整体操作结构,但各个子步骤却又很多改变的需求,或者需要子步骤的晚期实现(延迟到子类去实现)。
  • Template Method 使得子类可以复用一个算法的结构(Override 重写)该算法的某些特定步骤。
  • 不要调用我,让我来调用你,实现晚绑定机制,这也就是控制反转的思想。
  • 声明成 protected ,因为具体步骤在流程中才有意义。

mark

  • AbstractClass : 稳定的骨架(里面有具体的方法和需要被重写的方法)
  • ContreteClass : 具体的重写方法
  • 具体实现(举例)

冲咖啡和冲茶都有类似的流程,但是某些步骤会有点不一样,要求复用那些相同步骤的代码。

mark

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
public abstract class AbstractClass {

// 骨架流程
final void prepareRecipe(){
boilWater();
brew();
pourIncoup();
addCondiments();
}

// 待重写的方法
abstract void brew();

// 待重写的方法
abstract void addCondiments();


void pourIncoup() {
System.out.println("倒进杯子");
}

void boilWater() {
System.out.println("倒水");
}

}
1
2
3
4
5
6
7
8
9
public class Coffee extends AbstractClass {
void brew() {
System.out.println("倒咖啡");
}

void addCondiments() {
System.out.println("加入咖啡粉");
}
}
1
2
3
4
5
6
7
8
9
public class Tea extends AbstractClass {
void brew() {
System.out.println("倒茶");
}

void addCondiments() {
System.out.println("加入茶叶");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
public class Client {
public static void main(String[] args) {
AbstractClass coffee = new Coffee();
coffee.prepareRecipe();

System.out.println("========");


AbstractClass tea = new Tea();
tea.prepareRecipe();
}
}

结果显示:

1
2
3
4
5
6
7
8
9
倒水
倒咖啡
倒进杯子
加入咖啡粉
========
倒水
倒茶
倒进杯子
加入茶叶

JDK中实现:

  • java.util.Collections#sort()
  • java.io.InputStream#skip()
  • java.io.InputStream#read()
  • java.util.AbstractList#indexOf()

2. 策略模式(Strategy)

  • 定义:针对同一个算法的问题多种处理方式,仅仅是具体的行为有差别的时候

  • 在工厂模式中,为了创建不同的产品使用了switch case(或if else)的形式的代码,这样违背了开闭原则:对扩展开放,对修改封闭

  • 服务端的性能负担会随着条件判断的增加而增加,而本文的策略模式可以较好的解决这个问题

定义:定义了一系列的算法,把它们一个个封装起来,并且是他们可以相互替换。策略模式让算法独立于它的使用者之外,可以自由修改。

mark

来看看UML类图,图中主要有三个部分组成

  • Context: 上下文环境,持有Strategy 引用
  • Strategy: 抽象的策略(接口或者抽象类)
  • ConcreteStrategy: 具体的实现策略,实现了具体的算法(一般是工厂模式的具体实现

注意:

Strategy是使用接口还是抽象类

  • 取决于一系列的策略中是否有共同的属性或者方法,如果没有,使用接口更加灵活方便
  • 反之,使用抽象类,抽象类中便可存放公共的属性以及方法。

原始待修改代码:

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
// 待修改的代码

public class LetGo {

public static final String MODE_AIRPLAN = "airPlan";
public static final String MODE_TRAVEL = "travel";
public static final String MODE_CAR = "car";


public void printSpend(){
System.out.println("出行花费:" + needSpend(MODE_AIRPLAN));
}


private int needSpend(String model){
switch (model){
case MODE_AIRPLAN:
return 1400;
case MODE_TRAVEL:
return 500;
case MODE_CAR:
return 2400;
// 异常值
default:
return -1;
}
}


public static void main(String[] args) {
LetGo letGo = new LetGo();
letGo.printSpend();
}
}

如同前文所说的,当需要添加可选的出行方案时,我们不得不去修改needSpend()函数中的switch case来达到目的;然而这样并不利于后期的维护。接下来,试着使用策略模式来使实现这个简单的出行案例。

策略模式实现:

  • 既然,出现是可选的策略,我们可以先抽象除一个出行策略的的接口,包含needSpend()方法,返回出行花费的方法
1
2
3
public interface ITravelStrategy {
public int needSpend();
}
  • 接着,实现各种出行方案的实现类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/** 飞机出行 */
class AirPlanStrategy implements ITravelStrategy {
@Override
public int needSpend() {
return 1400;
}
}

/** 火车出行 */
class TrainStrategy implements ITravelStrategy {
@Override
public int needSpend() {
return 500;
}
}

/** 自驾出行 */
class CarStrategy implements ITravelStrategy {
@Override
public int needSpend() {
return 2400;
}
}

这样我们需要的策略就都已经完成了,就等着我们选一种方案,看看所需要的花费。

  • 我们创建一个类,和Strategy组合使用来获取各种出行方的花费,并在printSpend()方法中打印出所需要的花费。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class LetGoII {

// 组合的思想
ITravelStrategy iTravelStrategy;

public LetGoII(ITravelStrategy iTravelStrategy) {
this.iTravelStrategy = iTravelStrategy;
}


public void printSpend(){
System.out.println("出行花费:" + iTravelStrategy.needSpend());
}

}

测试:

1
2
3
4
public static void main(String[] args) {
LetGoII letGoII = new LetGoII(new AirPlanStrategy());
letGoII.printSpend();
}

以上的代码就可以完成自驾游花费的输出,使用了策略模式。

现在代码是不是比之前使用switch case实现的代码结构更加清晰、简洁

如果要实现更多的出行方式,只需要再实现ITravelStrategy接口,替换掉传入的LetGo 构造器的参数即可。

总结:

  • 策略模式,其实可以简单地理解成,将过多的switch casecase的代码封装成一个个具有共性的对象,需要什么我们就直接使用什么;
  • 对于这种共性的实现就利用interface或者是抽象类来实现。这从对代码的封装以及解耦的角度来理解,可能会更加容易理解。

使用场景:

  • 针对同一个算法的问题多种处理方式,仅仅是具体的行为有差别的时候
  • 出现同一抽象类的多个子类,而有需要使用switch caseif else来选择具体子类时

3. 观察者模式(Obersver)

  • 观察者模式是使用频率非常高的模式了,它定义了对象间一种一对多的关系,使得每当一个对象改变状态,则所有依赖它的对象都会收到通知,并且自动更新。
  • 例如:Java中的监听器Listener用的就是观察者模式。

UML类图:

mark

  • Subject: 具有注册和移除观察者,并且通知观察者的功能,主体是通过某种数据结构(可能是列表)来维护一张观察者列表实现这些操作的。
  • 观察者(Observer)的注册功能需要调用主体的registerObserver() 方法。

实现:

  • 开发技术周报,每周会更新一些内容,但是不知道具体的更新时间,又想第一时间阅读更新内容。
  • 难道要一直按住F5等它更新么?那估计F5烂了可能都没有更新。其实我们只需要简单的订阅一下就好,当有新的内容更新的时候,会发邮件到你订阅的邮箱中。

上述例子中:

  • 订阅者就是观察者,技术周报就是被观察者

代码实现:

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
import java.util.ArrayList;
import java.util.Observable;
import java.util.Observer;

// 观察者:程序员
public class Coder implements Observer {

private final String name;

public Coder(String name) {
this.name = name;
}


public void update(Observable o, Object arg) {
System.out.println("收到的更新内容为" + o);
}

}


// 被观察者:主体(开发周报)
class Weekly extends Observable {

// 一对多的通知
private ArrayList<Observer> observers = new ArrayList<Observer>();

// 增加一个观察者
public void subscribe(Observer observer) {
observers.add(observer);
}

// 删除一个观察者
public void unsubscribe(Observer observer) {
observers.remove(observer);
}

@Override
public void notifyObservers(Object obj) {
observers.forEach(observer -> observer.update("数据更新了"));
}
}

JDK中自带的观察者模式的类

mark

  • java.util 包中内置了Observerobservable类,同时Observable类实现了注册和反注册等方法,使用起来方便很多。可见观察者模式是非常重要的。
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
58
59
60
61
62
63
public class Observable {
private boolean changed = false;
private Vector<java.util.Observer> obs;

public Observable() {
obs = new Vector<>();
}

// 注册
public synchronized void addObserver(Observer o) {
if (o == null)
throw new NullPointerException();
if (!obs.contains(o)) {
obs.addElement(o);
}
}

// 反注册
public synchronized void deleteObserver(Observer o) {
obs.removeElement(o);
}

// 反注册所有观察者
public synchronized void deleteObservers() {
obs.removeAllElements();
}

// 通知更新
public void notifyObservers() {
notifyObservers(null);
}

// 通知更新
public void notifyObservers(Object arg) {
Object[] arrLocal;

synchronized (this) {
if (!changed)
return;
arrLocal = obs.toArray();
clearChanged();
}

for (int i = arrLocal.length-1; i>=0; i--)
((Observer)arrLocal[i]).update(this, arg);
}

protected synchronized void setChanged() {
changed = true;
}

protected synchronized void clearChanged() {
changed = false;
}

public synchronized boolean hasChanged() {
return changed;
}

public synchronized int countObservers() {
return obs.size();
}
}
  • Observer接口则是比较简单的代码,update()的参数中除了可以传递数据意外,还提供了被观察者的引用对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
 */
public interface Observer {
/**
* This method is called whenever the observed object is changed. An
* application calls an <tt>Observable</tt> object's
* <code>notifyObservers</code> method to have all the object's
* observers notified of the change.
*
* @param o the observable object.
* @param arg an argument passed to the <code>notifyObservers</code>
* method.
*/
void update(Observable o, Object arg);
}

JDK中的实现:

4 . 中介者模式(Mediator)

  • 中介者模式(Mediator Pattern) , 使用一个中介对象封装一系列对象交互,使得个对象之间没有明显的交互,并且能够独立的改变对象之间的交互(可能说的有点绕T_T
  • 看看下面这张UML类图,就能理解了。

mark

  • 其实和门面模式(Facade)有异曲同工之处,Facade解决的是系统内外的耦合性问题,而Mediator解决的是系统内的对象间的耦合问题。

举个例子:

  • 简单的说就是,中介者对象聚合了对象的交互,其他的对象都是通过中介者对象进行交互,没有直接的交互。
  • 这个可以想象一下租房,中介手里有房东的房子,你找中介租房,中介就是你和房东之间的那个中介对象。(代理模式也是如此)

虽然跳过中介直接找房东更加便宜,但是相对的,中介起的作用也简化和房东扯皮的一些过程

mark

  • Meadiator: 抽象中介者,定义了同事对象到中介者对象的接口。
  • ConcreteMediator: 具体的中介者角色,继承抽象中介者,实现了父类定义的方法,负责具体的同事对象之间的交互
  • Colleague : 抽象同事类,定义了中介者对象接口,只于中介者交互.不与其他同事对象交互.
  • ConcreteColleagueA/B:具体的同事类,继承抽象的同事类,每个具体的同事类都知道本身小范围内的行为,不知道自己大范围内的目的.

其实这样描述,有没有觉得想MVC模式里面的Controller(当然在Controller里面的代码可能不会像中介者这样降低View 之间的耦合),也是起到Model 和 View 聚合的一个作用,使得Model 和 View的耦合性降低.

代码实现:

  • 直接实现房客找中介联系房东的过程,首先是抽象中介者
1
2
3
public abstract class Mediator {
public abstract void constact(Person person,String message);
}
  • 然后是抽象的同事类
1
2
3
4
5
6
7
8
9
public class Person {
protected String name;
protected Mediator mediator;

public Person(String name,Mediator mediator){
this.name = name;
this.mediator = mediator;
}
}
  • 接下来就是创建两个具体的同事类了,也就是HouseOwner房东和Tenant租客
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
public class HouseOwner extends Person {
public HouseOwner(String name, Mediator mediator) {
super(name, mediator);
}


/**
* @desc 与中介者联系
* @param message
* @return void
*/
public void constact(String message){
mediator.constact(this,message);
}

/**
* @desc 获取信息
* @param message
* @return void
*/
public void getMessage(String message){
System.out.println("房主:" + name +",获得信息:" + message);
}
}

public class Tenant extends Person{

public Tenant(String name, Mediator mediator) {
super(name, mediator);
}

/**
* @desc 与中介者联系
* @param message
* @return void
*/
public void constact(String message){
mediator.constact(this,message);
}

/**
* @desc 获取信息
* @param message
* @return void
*/
public void getMessage(String message){
System.out.println("租房者:" + name +",获得信息:" + message);
}
}
  • 接下来就需要具体的中介者,将房东和租客聚合起来
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
public class MediatorStructure extends Mediator {
//首先中介结构必须知道所有房主和租房者的信息
private HouseOwner houseOwner;
private Tenant tenant;

public HouseOwner getHouseOwner() {
return houseOwner;
}

public void setHouseOwner(HouseOwner houseOwner) {
this.houseOwner = houseOwner;
}

public Tenant getTenant() {
return tenant;
}

public void setTenant(Tenant tenant) {
this.tenant = tenant;
}

@Override
public void constact(Person person, String message) {
if (person == houseOwner) {
//如果是房主,则租房者获得信息
tenant.getMessage(message);
} else {
//反正则是房主获得信息
houseOwner.getMessage(message);
}
}
}
  • 测试类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Client {
public static void main(String[] args) {
//一个房主、一个租房者、一个中介机构
MediatorStructure mediator = new MediatorStructure();

//房主和租房者只需要知道中介机构即可
HouseOwner houseOwner = new HouseOwner("包租婆", mediator);
Tenant tenant = new Tenant("酱爆", mediator);

//中介结构要知道房主和租房者
mediator.setHouseOwner(houseOwner);
mediator.setTenant(tenant);

tenant.constact("怎么停水啦?");
houseOwner.constact("你没交水费");
}
}

输出结果:
房主:包租婆,获得信息:包租婆,怎么停水啦?
租房者:酱爆,获得信息:打死你呀的!!

总结

  • 简化了对象之间的关系,将系统的哥哥对现象之间相互关联进行封装,将各个同事解耦
  • 使得多对多的关系变成了一对多的关系,简化对象之间的直接交互

缺点:

  • 由于中介者对象封装了系统中对象之间的交互,导致器变得非常复杂,随着同事类的增加,维护难度会逐渐增大
  • 其实中介者模式的缺点和优点在MVC模式体现的都很明显,所有的交互逻辑在Controller中,但是交互复杂之后经常导致Controller对象过于臃肿,难以维护更加不要说扩展了

JDK:

5. 状态模式(State)

  • 定义:类的内部状态改变的时候,可以改变它的行为
    • 可能描述的有点模糊,举个栗子(真就是举个栗子)

(Android系统在未root时,是无法卸载系统预装应用的;root成功,开启root权限之后,不仅可以卸载系统预装应用,还可以使用xposed,简直就是可以为所欲为啊!这就可以看出,Android系统在root的状态变更时,影响到了自身的某些行为,进而某些行为也相对应的发生了改变。)

mark

  • 从UML类图上来看,结构似乎和策略模式是一模一样的。

  • 状态模式的关键点在于不同的状态下,对于同一行为有不同的响应,避免使用if-elseswitch-case区分状态所带来的代码臃肿;在保持代码结构的清晰的同时还保证了扩展和维护性。

代码实现

  • 平常使用的登陆或者注销状态,经常使用的收藏和关注的功能,然而没有登录会提示去登陆,功能是无法使用的。
  • 实现收藏功能很容易出现以下代码:
1
2
3
4
5
6
7
public void collect(Context context){
if(isLogin()){
//收藏
}else{
//去登陆
}
}

如果还需要添加关注功能的话,又需要再次判断是否登录,一旦需要判断登录的功能一多,这种coding方式就会成为噩梦。

  • 接下来使用状态模式可以很好的避免这种情况,首先我们需要抽象出一个公共行为接口UserState ,其中包含collect()以及follow()方法。
1
2
3
4
5
6
public interface UserState {
/** 收藏行为 */
public void collect(Context context);
/** 关注行为 */
public void follow(Context context);
}
  • 对应登录未登录两种状态会有两种不同的实现方式,如下
1
2
3
4
5
6
7
8
9
public class LoginState implements UserState{
public void collect(Context context) {
System.out.println("收藏成功");
}

public void follow(Context context) {
System.out.println("关注成功");
}
}
1
2
3
4
5
6
7
8
9
public class LogoutState implements UserState{
public void collect(Context context) {
System.out.println("请登录");
}

public void follow(Context context) {
System.out.println("请登录");
}
}
  • 接下来,我们需要一个类来关联登录状态与外界,并为外界提供collect()函数和follow()函数对应的调用;并将LoginStateLogoutState结合起来。
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
// 这里主要是实现状态模式,
// 对于用户这个案例来说,结合单例模式可能会更好。

public class UserContext {
private final UserState LOGIN = new LoginState();
private final UserState LOGOUT = new LogoutState();
private UserState mState = LOGOUT;


/** 登录,供外部调用 */
public void login(){
mState = LOGIN;
}

/** 退出登录,供外部调用 */
public void logout(){
mState = LOGOUT;
}

/** 调用mState的coolect() */
public void collect(Context context) {
mState.collect(context);
}

/** 调用mState的follow() */
public void follow(Context context) {
mState.follow(context);
}

/** 此方法判断不太严谨,请忽略 */
public boolean isLogin(){
return mState instanceof LoginState;
}

}

注意:

  • 这里的UserContext 提供给外部的collect()follow() 方法的同时,还要提供login()logout()两个方法给外部调用来更改UserContext 的内部状态。

  • 这里主要是实现状态模式,对于用户这个案例来说,通常会结合单例模式使用的更好

从某种程度上来说,提供login()和logout()两个方法是为了防止外界篡改内部的行为,因为外部无法传入自己实现的UserState对象,也就无法修改UserContext对应状态的行为。

确实也看到有些文章,在设置状态时,状态是由外部传入的,这种写法觉得不太合适,所以这里单独说明。

扩展思考

其实对于实际情况来说,用户的操作行为可能远远不止这两个方法,若还有需要状态判断登录状态的行为,我们可以继续在UserState接口中为新增的行为添加相应的方法;但是,我们也可以预见,UserState接口将会越来越庞大。

这种问题的话,也可以将每个行为独立成一个接口,进而让UserState继承各个行为接口,或者将UserState改成抽象类实现各个行为接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//独立的收藏的接口
public interface ICollect {
public void collect(Context context);
}

//独立的关注的接口
public interface IFollow {
public void follow(Context context);
}

//所有行为接口的集合
public interface UserState extends ICollect,IFollow{

}
  • 总结:
    • 状态模式的关键点在于不同的状态下,对于同一行为有不同的响应,避免使用if-elseswitch-case区分状态所带来的代码臃肿;在保持代码结构的清晰的同时还保证了扩展和维护性。
    • 随着状态的增加,系统类和对象的个数也会增加,使用不当会导致程序结构变得越来越庞大。

6. 备忘录模式(Memento)

  • 定义:在不破坏封闭的前提下,将对象的当前内部状态保存到对象之外,之后可以再次恢复到此对象。(典型的例子就是游戏存档和读档的这个行为)

mark

  • Originator: 发起者,负责创建一个备忘录,并且可以记录,恢复自身的内部状态。可以根据需要决定Memeto保存自身的那些内部状态。
  • Memento:备忘录,用于存储Originator的状态,防止Originator以外的对象访问Memento
  • Caretaker : 备忘录管理者,负责存储备忘录,不能对备忘录的内容进行操作和访问,只能将备忘录传递给其他对象。

代码实现:

  • 发起者通过创建一个新的备忘录对象来保存自己的内部状态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//看出其模式的封闭性,对于状态得存储只有Originator知道

public class Originator {

private String state;


// 创建一个新的备忘录对象
public Memento createMemento(){
return new Memento(state);
}

// 将发起者的状态恢复到备忘录的状态
public void restore(Memento memento){
this.state = memento.getState();
}
}
  • 备忘录,将发起人对象传入的状态存储起来。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Memento {
private String state;

public Memento(String state) {
this.state = state;
}

public String getState() {
return state;
}

public void setState(String state) {
this.state = state;
}
}
  • 备忘录管理者,负责保存备忘录对象,但是从不修改(甚至不查看)备忘录对象的内容。
1
2
3
4
5
6
7
8
9
10
11
public class Caretaker {
private Memento mMemento;

public Memento restoreMemento(){
return mMemento;
}

public void storeMemengto(Memento memento){
this.mMemento = memento;
}
}

基本上这就是备忘录模式的结构了,可以看出其模式的封闭性,对于状态的存储只有Originator 知道。

总结:

  • 备忘录模式在不破坏封装性的条件下,通过备忘录对象存储另外一个对象的内部状态的快照,在将来合适的时候把这个对象还原到存储的状态。

优点:

  • 给用户提供了一种可以恢复状态的机制,可以是用户能够方便的回到某个历史的状态
  • 实现了信息的封装,是的用户不需要关心状态的保存细节

缺点

  • 现在很多序列化的技术已经比备忘录模式优秀很多

JDK:

  • java.io.Serializable

7. 迭代器模式(Iterator)

  • 迭代器模式:行为型设计模式之一。

  • 定义:提供一种方法顺序访问要给容器对象中的各个元素,而有不需要暴露该对象的内部数据存储的实现。(应用于数据结构)

  • 迭代器模式源于对数据集合的访问,1. 将遍历的方法封装到数据集合中,2. 或者不提供方法由用户自己遍历。

    • 以上两种方式都有弊端
    • 封装在数据集合中,那数据集合似乎多了些功能,数据集合不仅要维护自身数据元素,还要对外提供遍历的结构方法,遍历状态下还不能对同一个数据集合进行多个遍历操作。
    • 不提取公共的遍历方法,如果让使用者自己实现,又会让容器内部细节对外暴露。

UML类图:

mark

  • Iterator:迭代器接口,负责定义,访问和遍历元素的接口
  • ConcreteIterator:具体的迭代器,实现迭代器接口,并记录遍历的当前位置。
  • Aggregate: 容器接口,负责创建具体的迭代器角色的接口。
  • ConcreteAggregate:具体的容器类,具体迭代器和该容器相关联。

代码实现

  1. 迭代器接口
1
2
3
4
5
6
7
8
9
10
11
12
13
public interface Iterator<T> {
/**
* 返回当前位置元素并将位置移至下一位
*/
public T next();

/**
* 是否还有下一个元素
*
* @return true-有,false-没有
*/
public boolean hasNext();
}
  1. 具体的迭代器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class ConcreteIterator<T> implements Iterator<T> {

private final List<T> list;
// 用于移动的游标
private int cursor = 0;

public ConcreteIterator(List<T> list) {
this.list = list;
}


public T next() {
T t = null;
if (hasNext()){
t = list.get(cursor++);
}
return t;
}


public boolean hasNext() {
return cursor != list.size();
}
}
  1. 容器接口
1
2
3
4
5
6
7
8
9
10
public interface Aggregate<T> {
// 添加一个元素
public void add(T obj);

// 删除一个元素
public void remove(T obj);

// 获取迭代器对象
public Iterator<T> iterator();
}
  1. 具体的容器类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ConcreteAggregate<T> implements Aggregate<T> {

private List<T> list = new ArrayList<T>();


public void add(T obj) {
list.add(obj);
}

public void remove(T obj) {
list.remove(obj);
}

public Iterator<T> iterator() {
return new ConcreteIterator(list);
}
}

优点

  • 封装性好,对外部的使用者来说可以以相同的API来获取不同结构的数据集合中的数据元素,不需要关心遍历算法,简化了遍历方式
  • 便于扩展遍历方式,例如:可以创建一个正序迭代器同时还有一个倒序迭代器

看了ArratList代码中的Iterator主要的代码和上述是基本一致的,添加了更多的判断。而对于不同的数据结构MapSet根据其不同的实现方式Iterator的实现方式也各不相同。

迭代器模式,在数据结构中有广泛的使用,由于现在很多语言的API都提供了高效数据集合API,所以我们很少有需要去运用此模式,有兴趣的可以学习数据结构或者阅读源码来深入了解迭代器模式的实现。

JDK

8. 职责链模式(Chain of Responsibility

  • 责任链模式(Chain of Responsibility),行为型设计模式之一。

  • 什么是责任链呢?这个链很像数据结构中的单链表,链中的每个节点都有自己的职责,同时也持有下一个节点的引用。属于自己职责范围内的请求就自己处理,并完成请求的处理;而不属于的职责就传递给下一个节点。每个节点都是如此循环,直至请求被处理或者已经没有处理节点。

  • 这种设计模式为了避免请求的发送者和接收者的耦合关系,而责任链就是中间请求处理者(其中可能包括多个有可能处理请求的对象,并将这些对象连成一条链。这样也使得请求发送者无需关注处理细节和请求的传递。)

UML类图

mark

  • Client:请求发送者
  • Handler:抽象处理者,声明一个请求方法,并且保持一个对下一个处理节点Hanler的引用
  • ConcreteHandler: 具体的处理角色。对请求进行处理,如果不能处理就转发给下一个对象处理。

这是一个基本的结构描述,实际应用中会有进一步的封装。

代码实现:

  • 以公司正常请假为例,1天以内的假需要客户端部门主管签字,3天以内(不包含3天)需要技术部门主管签字,3天及以上就需要找CEO签字。
  • 最简单的就是使用if-else实现,但是结构并不是那么美观,试着用责任链模式来实现。
  • 首先是假条的类,包含姓名、请假原因和请假时间,使用final 声明是为了便面外部修改属性而已。
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
/** 假条的对象 */
public class LeaveNote {
// 请假人
final String name;
// 请假理由
final String reason;
// 请假天数
final int leaveDays;

public LeaveNote(String name, String reason, int leaveDays) {
this.name = name;
this.reason = reason;
this.leaveDays = leaveDays;
}


public String getName() {
return name;
}

public String getReason() {
return reason;
}

public int getLeaveDays() {
return leaveDays;
}

}
  • 接下来就是比较重要的Handler类,代码比较简单,下一个Handler的对象引用,再就是handle()setNextHandler()方法,不多解释了,和之前的UML图中的的结构是一样的
1
2
3
4
5
6
7
8
9
10
11
12
13
public abstract class Handler {

// 下一个对象的引用
public Handler nextHandler;

// 处理什么东西
abstract void hand(LeaveNote note);

// 设置下一个对象的引用
public void setNextHandler(Handler nextHandler) {
this.nextHandler = nextHandler;
}
}
  • 现在我们需要实现客户端主管、技术主管和CEO三个Handler的子类
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
public class ClientLeader extends Handler {
void hand(LeaveNote note) {
if (note.leaveDays < 3){
System.out.println("部门主管同意" + note.name + "请假");
}else {
nextHandler.hand(note);
}
}
}


public class TechnologyLeader extends Handler {

void hand(LeaveNote note) {
if (note.leaveDays < 3){
System.out.println("技术主管" + note.name + "请假");
}else {
nextHandler.hand(note);
}
}
}


public class CEO extends Handler{
void hand(LeaveNote note) {
System.out.println("CEO同意" + note.name + "请假");
}
}

接着我们就需要测试调用我们写好的代码了

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
public class Client {

/** 测试方法 */
public void test(){
LeaveNote note = new LeaveNote("朱酱酱", "回家过年", 3);
requestLevel(note);
}
/** 具体的封装方法 */
public void requestLevel(LeaveNote note) {
ClientLeader leader = new ClientLeader();
TechnologyLeader technologyLeader = new TechnologyLeader();
CEO ceo = new CEO();

// 链表的形式
leader.setNextHandler(technologyLeader);
technologyLeader.setNextHandler(ceo);

// 从头节点开始判断是否能够处理
leader.hand(note);
}


public static void main(String[] args) {
Client client = new Client();
client.test();
}
}

这里完成了,比较简单地责任链模式就完成了。

其实对于链表,nextHandler还可以作为构造函数的参数传入,于是代码变成这个样子

1
2
3
4
5
6
7
public void requestLeave(LeaveNote leaveNote){
CEO ceo = new CEO(null);
TechnologyLeader technologyLeader = new TechnologyLeader(ceo);
ClientLeader clientLeader = new ClientLeader(technologyLeader);

clientLeader.hand(leaveNote);
}

总结:

责任链的优点就在于请求者和接受者松散耦合,以及能动态组合职责。

(例子中可以看出,请求者不知道接收者是谁,也不知道具体的处理过程,只需要发出请求就可以了。而对于每一个职责对象来说,也不关心请求者和其他职责对象,虽然由下一个职责对象的引用,但是还是只负责处理自己的职责部分就好了,其他部分交给其他职责对象去处理)

动态组合职责则是利用职责的拆分,可以灵活的组合形式责任链,从而可以灵活的分配职责对象,也可以灵活的实现职责对象。

PS:其实刚开始学习责任链模式的时候,我在想:“这种设计模式并没有做到很好地解耦啊!每个Handler还需要持有下一个Handler对象的引用,这不是造成更高的耦合度了么!”。之后才看明白,责任链的解决的时发出请求的一方和接受请求的一方的耦合度的问题,而处理这一切的Handler就是解决方案,所以Handler之间的耦合基本算是内部的

JDK:

9. 命令模式(Command

定义:将命令封装成对象,具有以下作用

  • 使用命令来参数化其他对象
  • 将命令放入队列进行排序
  • 将命令的操作记录到日志中
  • 支持可撤销的操作

UML类图:

mark

  • Client:客户端,也就是发出命令者。
  • Invoker:执行者,该类的职业就是调用命令对象执行具体的请求操作,相关的方法我们称为行动方法。
  • Command:命令,定义所有具体命令类的接口。
  • ConcreteCommand:具体命令,实现了Command接口,在execute()方法中调用Receiver相关的方法。
  • Receiver:接收者,负责执行具体的逻辑。

整体来看,命令模式比较繁琐,执行一个指令的过程被分解成了好几部分,相对的复杂度也提升了;但是命令模式结构还是很清晰的。

代码实现:

​ 记得有个段子:

如果有一个按钮,按下以后会忘记一切事情,你会怎样?
咦,这里有个按钮,按一下
咦,这里有个按钮,按一下
咦,这里有个按钮,按一下

。。。

其实这个按下这个按钮就是执行了一个命令,是一个让你忘记一切的命令(于是自己开始递归,最后还Stack Overflow了?)

  • 首先我们需要一个接收者Receiver , 来实现我们所传递的命令
1
2
3
4
5
6
7
8
9
// 这里接收者只执行这一个命令,
// 复杂的情况们可以继承Receiver来实现不同的接收者处理不同的命令。

public class Receiver {
public void action(){
//具体命令操作
System.out.println("清除所有记忆");
}
}

这里接收者只执行这一个命令,复杂的情况们可以继承Receiver来实现不同的接收者处理不同的命令。

  • 再是一个命令的接口Command和一个具体的Command实现类CleanMemoryCommand,很简单,只有一个没有实现的execute()方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/** 命令接口 */
public interface Command {
public void execute();
}

/** 清除记忆命令 */
public class CleanMemoryCommand implements Command {

Receiver receiver;

public CleanMemoryCommand(Receiver receiver){
this.receiver = receiver;
}

@Override
public void execute() {
receiver.action();
}
}
  • 这样,命令和接收者都有了,我们只差一个供用户直接使用的Invoker
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Invoker {

private Command command;

public Invoker(Command command){
this.command = command;
}

public void actoin(){
command.execute();
}

}
  • 调用的代码就比较简单了,这代码可以封装成一个方法
1
2
3
4
5
6
7
8
public class Client {
public static void main(String[] args){
Receiver receiver = new Receiver();
CleanMemoryCommand command = new CleanMemoryCommand(receiver);
Invoker invoker = new Invoker(command);
invoker.actoin();
}
}

总结:

  • 命令模式看似简单,细想来其实存在着几乎所有设计模式的通病,那就是大量衍生类的创建,这是一个不可避免的问题。
  • 尽管如此,也给我们带来了许多好处,更弱的耦合性、更灵活的控制性以及更好的扩展性。但是最后还是那句话,用不用,还是根据实际情况而定。

10 .访问者模式(Visitor)

  • 访问者模式是一种将数据操作和数据结构分离的设计模式,是23种设计模式中非常复杂的一种设计模式。

  • 定义:封装一些作用与某种数据结构中的各元素的操作(访问),可以在不改变这个数据的前提下定义作用于这些元素的新操作。

  • 顾名思义:某些不能改变的数据,对于不同的访问者有不同的操作(或者访问),为不同的访问者提供相对应的操作。例如:公司CEO就能看到公司所有的真实财报数据,而作为一个员工可能就只能知道同比去年的增长比例。

UML类图:

mark

  • Visitor访问者抽象类(或者接口),它定义了对每一个元素(Element)访问的行为,它的参数就是可以访问的元素;理论上,它的方法个数和元素个数是一样的。因此,访问者模式要求元素的类族要稳定,不能频繁的添加,移除元素。如果出现频繁修改Visitor接口的情况,说明可能并不适合使用访问者模式。

  • ConcreteVisitor : 具体的访问者,需要实现每一个元素类访问时所产生的具体的行为 。

  • Element: 元素接口(或者抽象类),它定义了一个接收访问者的方法(accept) 方法,意义在于对于每一个元素都要刻意被访问者访问。

  • ElementA、ElementB: 具体的元素类,提供接收访问方法的具体实现。而这个具体实现,通常情况下是使用访问者提供的访问该元素类的方法。

  • ObjectStructure: 定义当中所提到的对象结构,对象结构是一个抽象表述,它内部管理了元素集合,并且可以迭代这些元素供访问者访问。

PS:访问者模式违反了迪米特原则(对访问者公布元素细节)以及依赖倒置原则(依赖了具体类,没有依赖抽象),由此可见,此模式需要应用在特定的情况中。

案例实现

这里就以公司为例,公司员工暂且分为开发人员和运营人员,而公司的CEO和CTO对于不同员工的KPI关注点不同,因此我们需要做出不同的处理,接着看看代码实现

  • 员工基类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Element接口
public abstract class Staff {
public String name;

public int kpi;

public Staff(String name, int kpi) {
this.name = name;
this.kpi = kpi;
}


// 接受visitor的访问
public abstract void accept(Visitor visitor);
}
  • 具体员工类

具体的员工,根据各自不同的职责添加了不同的方法,开发人员的KPI和代码产量相关,于是添加了获取代码行数的方法,而运营人员的KPI和新增用户量相关,于是添加了获取新增用户数的方法。

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
// 开发人员
public class Developer extends Staff {
public Developer(String name, int kpi) {
super(name, kpi);
}

public void accept(Visitor visitor) {
visitor.visit(this);
}

// 代码量
public int getCodeLines(){
return new Random().nextInt(10 * 1000);
}

}

//运维人员
public class Operator extends Staff {

public Operator(String name, int kpi) {
super(name, kpi);
}

public void accept(Visitor visitor) {
visitor.visit(this);
}

// 代码量
public int getNewUserNum(){
return new Random().nextInt(10 * 1000);
}

}
  • 访问者
1
2
3
4
5
6
7
8
9
// 访问者Visitor
public interface Visitor {

// 访问开发人员
public void visit(Developer developer);

// 访问运营人员
public void visit(Operator operator);
}

这里可以看到,直接从方法上就区分DeveloperOperator,这里主要考虑到的是,如果使用基类Staff作为参数的话代码就会是这个样子

1
2
3
4
5
6
7
8
9
10
public void visit(Staff staff){
if(staff instanceof Developer){
Developer developer = (Developer)staff;
System.out.print("开发" + developer.name
+ ",KPI:" + developer.kpi + ",代码" + developer.getCodeLines() + "行");
}else if(staff instanceof Operator){
Operator operator = (Operator) staff;
System.out.print("运营" + operator.name + ",KPI:" + operator.kpi);
}
}

可以看到,在visit() 方法中,我们就需要判断参数类型和类型转换,这样代码就难以扩展和维护。

这是访问者模式的一个优点,也是一个缺点,优点在于代码清晰,某种程度上代码的维护和扩张更好;而缺点也是一样,如果需要添加一类Staff,所有的Visitor都需要在实现一个新的visit()方法。

  • 具体的访问者
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
// CTO对于不同人员操作不同
public class CTOVisitor implements Visitor {
@Override
public void visit(Developer developer) {
System.out.print("开发" + developer.name
+ ",KPI:" + developer.kpi + ",代码" + developer.getCodeLines() + "行");
}

@Override
public void visit(Operator operator) {
System.out.print("运营" + operator.name + ",KPI:" + operator.kpi);
}
}
// CEO对于不同人员操作不同
public class CEOVisitor implements Visitor {
@Override
public void visit(Developer developer) {
System.out.print("开发" + developer.name + ",KPI:" + developer.kpi);
}

@Override
public void visit(Operator operator) {
System.out.print("运营" + operator.name
+ ",KPI:" + operator.kpi + "新增用户:" + operator.getNewUserNum());
}
}
  • 对象结构

这里的对象结构,直接就设定成了公司,集合就是员工们

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 对象结构
public class Company {

private List<Staff> staffList = new ArrayList<>();

// 查询
public void action(Visitor visitor){
for (Staff staff : staffList) {
staff.accept(visitor);
}
}

// 新增
public void addStaff(Staff staff){
staffList.add(staff);
}

}
  • 客户端代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Client {
public static void main(String[] args) {
Company company = new Company();


company.addStaff(new Developer("Bruce Wayne",10));
company.addStaff(new Developer("ClarkKent",20));
company.addStaff(new Developer("Barry Allen",30));

company.addStaff(new Operator("Diana Prince",40));
company.addStaff(new Operator("Oliver Queen",50));
company.addStaff(new Operator("Dinah Lance",60));


CEOVisitor ceo = new CEOVisitor();
company.action(ceo);


CTOVisitor cto = new CTOVisitor();
company.action(cto);
}
}

具体输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
CEO所看到的======
开发Bruce Wayne,KPI:6
开发ClarkKent,KPI:2
开发Barry Allen,KPI:8
运营Diana Prince,KPI:4,新增用户:46642
运营Oliver Queen,KPI:1,新增用户:7687
运营Dinah Lance,KPI:3,新增用户:67382

CTO所看到的======
开发Bruce Wayne,KPI:6,代码8285
开发ClarkKent,KPI:2,代码8351
开发Barry Allen,KPI:8,代码658
运营Diana Prince,KPI:4
运营Oliver Queen,KPI:1
运营Dinah Lance,KPI:3
10.1 分派
  • 变量名被声明时的类型叫做变量的静态类型(Static Type),静态变量类型又可以叫做明显类型(Apparent Type);而变量所引起的对象的正式类型叫做变量的实际类型(Actual Type)。
1
2
// List静态类型,Arraylist动态类型
List list = new ArrayList();
  • 在java代码中有一种很常见的写法,声明父类对象创建子类对象(比如上面 声明是 List 类型(也就是静态类型即明显类型)),创建的是ArrayList 的对象(实际类型)。
  • 这里需要提到一个词,分派(Dispatch) 。 当使用上述形式声明并创建对象,根据对象的类型对方法进行选择。这就是分派,而分配又可以分为静态分派(Static Dispatch)动态分派(Dynamic Dispatch)
    • 静态分派:对应的就是编译时,根据静态类型信息发生的分派。方法重载就属于静态分派
    • 动态分派:对应的就是运行时,动态的自动的换掉某个方法。方法的重写就属于动态分派。

静态分派

1
2
3
4
5
6
7
8
9
10
11
12
// 方法的重载
public class Staff {
}


class Developer extends Staff{

}

class Operator extends Staff{

}

执行类,execute()方法有三个重载方法,方法的参数分别上面对应的三个类型StaffDeveloperOperator的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Execute {

public void execute(Staff staff){
System.out.println("员工");
}

public void execute(Developer developer){
System.out.println("开发人员");
}

public void execute(Operator operator){
System.out.println("运营人员");
}
}

测试代码和结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Client {
public static void main(String[] agrs){
System.out.println("运行结果:");

Staff staff = new Staff();
Staff staff1 = new Developer();
Staff staff2 = new Operator();

Execute execute = new Execute();
execute.execute(staff);
execute.execute(staff1);
execute.execute(staff2);
}
}

运行结果:
员工
员工
员工

静态分派总结:

  • 可以推断出,传入三个对象,最后执行的方法都是参数类型是Staff 的方法,即使三个对象有不同的真实类型。
  • 方法重载中实际起作用的是他们的静态类型,也就是在编译期间就完成了分派,即静态分派

动态分派

三个类自带execute()方法,DeveloperOperator继承Staff,并重写了execute()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 方法的重写
public class Staff {
public void execute(){
System.out.println("员工");
}
}

public class Developer extends Staff {
@Override
public void execute() {
System.out.println("开发人员");
}
}

public class Operator extends Staff {
@Override
public void execute() {
System.out.println("运营人员");
}
}

测试代码以及结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Client {
public static void main(String[] agrs) {
System.out.println("运行结果:");
Staff staff = new Staff();
staff.execute();

Staff staff1 = new Developer();
staff1.execute();

Staff staff2 = new Operator();
staff2.execute();
}
}

运行结果:
员工
开发人员
运营人员

动态分派总结

  • 测试的情况相同,三个对象,其静态方法都是Staff ,而实际类型分别是Staff , DeveloperOperator 。可以看到重写execute() 方法都生效了,各自输出了对应的内容
  • java编译器在编译期间不总是知道哪些代码会被执行,因为编译器仅仅知道对象的静态类型,而不知道对象的真实类型;而方法的调用则是根据对象的真实类型,而不是静态类型。
10.2 单分派和多分派
  • 首先要了解一个宗量的概念。一个方法所属的对象叫做方法的接收者,方法的接收者和方法的参量统称为方法的宗量。而根据分派可以基于多少种宗量,可以将面向对l象分为单分派语言和多分派语言。
    • 单分派语言根据一个宗量的类型(真实类型)进行对方法的选择
    • 多分派语言根据多个宗量的类型对方法进行选择。
  • 那Java属于什么类型呢?
    • Java静态分派时决定方法的选择的宗量包括方法的接收者和方法参数的静态类型。所以是多分派。
    • 动态分派时,方法的选择只会考虑方法的接收者和实际类型,所以是单分派
    • 其实Java语言是支持静态多分派和动态单分派的语言。
10.3 双重分派

那双重分派又是什么呢?分派和访问者模式又有什么关系呢?接下来就会解释这些问题

java 支持静态多分派和动态但分派,并不支持动态多分派,于是就有了两次单分派组成的双重分派来代替动态多分派。而访问者模式恰好使用到了双重分派的技术。

双重分派技术就是在选择一个方法的时候,不仅仅要根据方法的接收者的运行时区别,还要根据运行时的参数进行区别(达到二次分派的效果)

在访问者模式中,客户端将具体的对象传递给访问者,也就是staff.accpet(vistior) 方法的调用,完成第一次分派;然后具体的访问者作为参数传入到具体的对象方法中,也就是visitor.visit(this),将this 作为参数传递进去完成第二次分派。

双重分派也就是意味着得到的执行操作决定请求的种类和接收者的类型。双重分派的核心就是this 对象。

从访问者模式可以看出,双重分派就是在方法的委派前面加上了继承的重写,使得从某种角度上来说重载变成了动态的。

缺点:

  • 对象结构变化很困难: 不适用于对象结构中的类经常变化的情况,因为对象结构发生了改变,访问者的接口和访问者的实现都要发生相应的改变,代价太高。
  • 破坏封装: 访问者模式通常需要对象结构开放内部数据给访问者和ObjectStructrue,这破坏了对象的封装性。

11 解释器模式(Interpreter)

  • 解释器模式(Interpreter Pattern),实际应用中较少用到的行为模式。主要作用就是提供解释语言的语法或表达式的能力,从作用上来说,注定实际开发过程中会使用的少,毕竟很少有人需要构建一套自己的语法来解析吧!但是,这并不表示解释器模式我们可以忽略掉。

UML类图:

mark

  • Context:上下文环境,包含解释器之外的全局信息
  • Client:客户端,解析表达式,构建语法树,执行具体的解释操作等
  • AbstractExpression : 抽象的表达式,声明一个抽象的解释操作父类,并定义一个抽象的解释方案,其具体的实现在各个具体的子类解释其中完成。
  • TerminalExpression : 终结符表达式,实现文法中终结符有关的解释操作。文法中每一个终结符都有一个具体的中介表达式与之对应。
  • NonterminalExpression:非终结表达式,实现文法中非终结符有关的解释操作。

其中AbstractExpressioninterpret()是抽象的解析方法,参数是上下文的环境,而interpret()方法的具体实现则由TerminalExpressionNonterminalExpression实现。

具体实现:

如下我们通过对算术表达式的解释来看一个解释器模式的实现, 如表达式m+n+p中,如果我们使用解释器模式对该表达式进行解释,那么mnp代表的三个字母可以看成是终结符号,而+代表的运算符则可以看成是非终结符号。

  • 先创建抽象的解释器,表示数学运算
1
2
3
public abstract class ArithmeticExpression {
public abstract int interptet();
}
  • 解释器中定义了interptet()方法,ArithmeticExpression有两个子类,分别是NumExpressionOperatorExpression
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/** 对数字进行解释 */
public class NumExpression extends ArithmeticExpression {
private int num;

public NumExpression(int num) {
this.num = num;
}

@Override
public int interptet() {
return num;
}
}

/** 对运算符进行解释 */
public abstract class OperatorExpression extends ArithmeticExpression {
protected ArithmeticExpression mArithmeticExpression1,mArithmeticExpression2;

public OperatorExpression(ArithmeticExpression arithmeticExpression1,ArithmeticExpression arithmeticExpression2) {
mArithmeticExpression1 = arithmeticExpression1;
mArithmeticExpression2 = arithmeticExpression2;
}
}
  • 基础的类已经完成了,如果需要处理加法运算还需要继承OperatorExpression并实现interptet()方法来实现加法运算器AdditionExpression
1
2
3
4
5
6
7
8
9
10
public class AdditionExpression extends OperatorExpression {
public AdditionExpression(ArithmeticExpression arithmeticExpression1,ArithmeticExpression arithmeticExpression2) {
super(arithmeticExpression1, arithmeticExpression2);
}

@Override
public int interptet() {
return mArithmeticExpression1.interptet() + mArithmeticExpression2.interptet();
}
}
  • 还差一个业务逻辑处理类
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
public class Calculator {

//声明一个Stack栈,存储并操作所有相关的解释器
protected Stack<ArithmeticExpression> mArithmeticExpressionStack = new Stack<>();

public Calculator(String expression) {
//声明两个ArithmeticExpression类型的临时变量,存储运算符左右两边的数字解释器
ArithmeticExpression arithmeticExpression1, arithmeticExpression2;

//根据空格分隔表达式字符串
String[] elements = expression.split(" ");

//循环遍历表达式元素数组
for (int i = 0; i < elements.length; ++i) {
switch (elements[i].charAt(0)) {
//如果是加号
case '+':
//将栈中的解释器弹出作为运算符号左边的解释器
arithmeticExpression1 = mArithmeticExpressionStack.pop();

//同时将运算符号数组下标下一个元素构造为一个数字解析器
arithmeticExpression2 = new NumExpression(Integer.valueOf(elements[++i]));

//通过上面两个数字解释器构造加法运算解释器
mArithmeticExpressionStack.push(new AdditionExpression(arithmeticExpression1, arithmeticExpression2));
break;
default:
//如果不是运算符,则是数字,直接构造数字解释器并压入栈
mArithmeticExpressionStack.push(new NumExpression(Integer.valueOf(elements[i])));
break;
}
}
}

//计算结果
public int calculate() {
return mArithmeticExpressionStack.pop().interptet();
}
}

这里需要注意的是,为了简化逻辑,在约定的表达式的每个元素之间必须使用空格隔开,如123 + 32 + 666这种形式的表达式,这样才能在Calculator中使用空格来拆分字符串。

  • 如果想引入减法运算,我们只需要定义一个减法解释器
1
2
3
4
5
6
7
8
9
10
public class SubtractionExpreesion extends OperatorExpression{
public SubtractionExpreesion(ArithmeticExpression arithmeticExpression1,ArithmeticExpression arithmeticExpression2) {
super(arithmeticExpression1, arithmeticExpression2);
}

@Override
public int interptet() {
return mArithmeticExpression1.interptet() - mArithmeticExpression2.interptet();
}
}

Calculatorswitch中添加如下代码即可

1
2
3
4
5
6
case "-":
arithmeticExpression1 = mArithmeticExpressionStack.pop();
arithmeticExpression2 = new NumExpression(Integer.valueOf(elements[++i]));

mArithmeticExpressionStack.push(new SubtractionExpreesion(arithmeticExpression1, arithmeticExpression2));
break;

总结

这里,我们能看出来解释器模式灵活性强,但是这是对于相对简单的语言;如果需要加入乘除取余等等,一并进行混合预算的话还需要考虑不同符号的运算优先级逻辑处理,所以在“简单的语言”中适用解释器模式。其实解释器模式的本质就是,将复杂的问题模块化,分离实现、解释执行。

JDK:

三:结构型

1. 装饰者模式(Decorator)

  • 装饰者模式(Decorator Pattern)也称为包装模式(Wrapper Pattern),以透明动态的方式来动态扩展对象的功能,也是继承关系中一种代替方案
  • 装饰者模式与继承关系的目的都是要扩展对象的功能,但是装饰者模式可以提供比继承更多的灵活性。(减少代码的冗余)
  • 通过不同具体装饰类以及这些装饰类的排列组合,设计师可以创造出很多不同行为的组合。

mark

  • Component: 抽象组件(可以是抽象类或者接口),被装饰的原始对象
  • ConcreteComponent:具体的实现类,被装饰的具体对象
  • Decorator : 抽象的装饰者,职责就是为了装饰我们的组件对象,内部一定要有一个指向组件的对象引用。
  • ConcreteDecoratorA:装饰者具体实现类,只对抽象装饰者做出具体实现
  • ConcreteDecoratorB:同上

举个例子:

  1. 人定义为抽象类,有一个抽象方法eat()
1
2
3
public abstract class Person {
public abstract void eat();
}
  1. 创建一个NormalPerson类继承Person,对eat()方法有了具体实现;
1
2
3
4
5
public class NormalPerson extends Person {
public void eat() {
System.out.println("吃饭");
}
}
  1. 定义一个PersonFood类来表示装饰者的抽象类,保持了一个对Person的引用,可以方便调用具体被装饰的对象方法,这样就可以方便的对其进行扩展功能,并且不改变原类的层次结构。
1
2
3
4
5
6
7
8
9
10
11
12
public class PersonFood extends Person {
private Person person;

public PersonFood(Person person){
this.person = person;
}

@Override
public void eat() {
person.eat();
}
}
  1. 接着就是具体的装饰类了,这两个类没有本质上的区别,都是为了扩展PersonFood类,不修改原有类的方法和结构
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
public class ExpensiveFood extends PersonFood {
public ExpensiveFood(Person person) {
super(person);
}

@Override
public void eat() {
super.eat();
eatSteak();
drinkRedWine();
}

public void eatSteak(){
System.out.println("吃牛排");
}

public void drinkRedWine(){
System.out.println("喝拉菲");
}

}

public class CheapFood extends PersonFood {
public CheapFood(Person person) {
super(person);
}

@Override
public void eat() {
super.eat();
eatNoodles();
}

public void eatNoodles(){
System.out.println("吃面条");
}
}

Client:测试

1
2
3
4
5
6
7
8
9
10
11
public class Client {
public static void main(String[] args){
Person person = new NormalPerson();

PersonFood cheapFood = new CheapFood(person);
cheapFood.eat();

PersonFood expensiveFood = new ExpensiveFood(person);
expensiveFood.eat();
}
}

总结:

  • 优点
    • 装饰者模式与继承关系的目的都是要扩展对象的功能,但是装饰者模式可以提供比继承更多的灵活性。
    • 通过使用不同的具体装饰类以及这些装饰类的排列组合,设计师可以创造出很多不同行为的组合

与代理模式的区别:

  • 其实装饰着模式和代理模式很想,但两个目的不尽相同
  • 装饰者模式是以对客户端透明的方式扩展对象的功能,是继承关系的一个替代方案,目的是为了减少类的冗余
  • 代理模式是提供一个代理对象,并有代理对象来控制原来对象的引用。
  • 总而言之:代理模式是为了对代理对象进行控制,但不做功能的扩展;装饰者模式为本装饰的对象进行功能的扩展。

2. 桥模式(Bridge)

  • 定义:将抽象部分(业务功能)与实现部分(平台实现)分离,使他们可以独立的变化。(主要是违背了单一职责原则)

  • 动机:由于某些类型的固有的实现逻辑,使得他们具有两个变化的维度,乃至多个变化的维度

    • 使用在对象间的组合关系,使得抽象和实现之间沿着各自的维度来变化。

类图:

mark

  • Abstraction:定义抽象类的接口
  • Implementor:定义实现类接口

代码实现:

  • RemoteControl 表示遥控器,指代 Abstraction。
  • TV 表示电视,指代 Implementor。
  • 桥接模式将遥控器和电视分离开来,从而可以独立改变遥控器或者电视的实现。
1
2
3
4
5
6
7
8
9
public abstract class TV {

public abstract void on();

public abstract void off();

public abstract void tuneChannel();

}

TV具体实现:

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


public void on() {
System.out.println("Sony On");
}

public void off() {
System.out.println("Sony Off");
}

public void tuneChannel() {
System.out.println("Sony ChannelChange");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ChangHong extends TV{

public void on() {
System.out.println("ChangHong On");
}

public void off() {
System.out.println("ChangHong Off");
}

public void tuneChannel() {
System.out.println("ChangHong ChannelChange");
}
}

遥控器:

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

// 组合tv
protected TV tv;

// 指向tv的指针
public RemoteController(TV tv) {
this.tv = tv;
}



public abstract void on();

public abstract void off();

public abstract void tuneChannel();

}

具体遥控器:

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 ConcreteRemote1 extends RemoteController {

// 指向tv的指针
public ConcreteRemote1(TV tv) {
super(tv);
}



public void on() {
System.out.println("ConcreteRemoteControl1 on");
tv.on();
}

public void off() {
System.out.println("ConcreteRemoteControl1 off");
tv.off();
}

public void tuneChannel() {
System.out.println("ConcreteRemoteControl1 tuneChannel");
tv.tuneChannel();
}

}
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 ConcreteRemote2 extends RemoteController {

// 指向TV的指针
public ConcreteRemote2(TV tv) {
super(tv);
}



public void on() {
System.out.println("ConcreteRemoteControl2 on");
tv.on();
}

public void off() {
System.out.println("ConcreteRemoteControl2 off");
tv.off();
}

public void tuneChannel() {
System.out.println("ConcreteRemoteControl2 tuneChannel");
tv.tuneChannel();
}

}

客户端测试:

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
public class Client {

public static void main(String[] args) {
// 组合的方式
ConcreteRemote1 remote1 = new ConcreteRemote1(new ChangHong());
remote1.on();
remote1.off();
remote1.tuneChannel();
System.out.println(remote1.tv);

System.out.println("=====================");


// 组合的方式
ConcreteRemote2 remote2 = new ConcreteRemote2(new Sony());
remote2.on();
remote2.off();
remote2.tuneChannel();
System.out.println(remote2.tv);

}
}


//结果:
ConcreteRemoteControl1 on
ChangHong On
ConcreteRemoteControl1 off
ChangHong Off
ConcreteRemoteControl1 tuneChannel
ChangHong ChannelChange
ChangHong@4554617c
=====================
ConcreteRemoteControl2 on
Sony On
ConcreteRemoteControl2 off
Sony Off
ConcreteRemoteControl2 tuneChannel
Sony ChannelChange
Sony@74a14482

JDK中实现

  • AWT (It provides an abstraction layer which maps onto the native OS the windowing support.)
  • JDBC

3. 享元模式(Flyweight)

  • 享元模式 : 是对象池的一种实现,主要用于减少创建对象的数量,以减少内存占用和提供性能。

  • 定义:运用共享技术有效的支持大量细粒度的对象(要求对象是只读的)

mark

  • Flyweight: 享元模式的抽象类或者接口。
  • ConcreateFlyweight: 具体的享元对象。
  • FlyweightFactory:享元工厂,负责管理享元对象池和创建享元对象。

代码实现

  • 场景:过年抢票,大家肯定都不陌生,各种刷票插件、软件什么的。在用户设置好出发和到达之后,每次查询请求都返回一系列的车票结果。
    • 当数千万的用户在不断请求查询结果时,如果每次查询结果都是重新创建返回的,可想而知,肯定会有大量的重复对象的创建、销毁,内存占用和GC的压力都会随之增大。
    • 而享元模式就能很好的应对这种情况,车次是固定的,根据出发地和到达地查询出来的车次基本都是相同的
  1. 创建一个Ticket接口,定义输出车票信息的方法
1
2
3
4
// 创建一个Ticket接口,定义输出车票信息的方法
public interface Ticket {
public void showTicket(String info);
}
  1. 具体的实现类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class TrainTicket implements Ticket {

//出发地
private String from;
//到达地
private String to;
//铺位
private String bunk;
//价格
private int price;


public TrainTicket(String from, String to) {
this.from = from;
this.to = to;
}

public void showTicket(String info) {
price = new Random().nextInt(200);
System.out.println("查询 从 "+from+" 到 " + to + " 的 " + bunk + " 车票,价格:" + price);
}
}
  1. 享元工厂
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class TicketFactory {

// JUC MAP
private static Map<String,Ticket> ticketMap = new ConcurrentHashMap<>();

// 每次获取车票对象时的创建,享元模式有效的减少了重复对象的创建。
public static Ticket getTicket(String from,String to){
final String key = from + "-" + to;
// 享元模式的精髓,有就直接返回这个对象
if (ticketMap.containsKey(key)){
return ticketMap.get(key);
}else {
// 没有就加入到对象池中,并且返回这个对象
TrainTicket ticket = new TrainTicket(from, to);
ticketMap.put(key,ticket);
return ticket;
}
}
}

JDK:

Java 利用缓存来加速大量小对象的访问时间。

  • java.lang.Integer#valueOf(int)
  • java.lang.Boolean#valueOf(boolean)
  • java.lang.Byte#valueOf(byte)
  • java.lang.Character#valueOf(char)

4. 外观模式(Facade)

  • 外观模式:封装系统的复杂结构,向外提供一个可以访问系统的接口,这个接口就是系统内外通信的统一的出入口。这样使得系统更加易于维护和使用
    • 在我们集成很多第三方SDK的时候就会发现,我们集成一次之后,想要升级SDK的版本的话,我们只需要替换jar或者修改依赖库的版本,当然你也可以一直使用最新的版本。

mark

  • Client:客户端,直接使用Facade接口提供的方法
  • Facade:就是系统对外的同一对象,封装了各个子系统的交互简化了Client调用
  • SystemA、SystemB、SystemC:子系统接口
  • ConcreteSystemA、ConcreteSystemB、ConcreteSystemC:子系统的实现

这里就可以看出,对Client来说只需要知道一个Facade一个就行,不需要知道Facade内部的具体复杂逻辑,降低用户的使用成本。(思想:高内聚,松耦合)

优点

  • 对使用者隐藏内部的具体细节,降低使用者和子系统的耦合。
  • 外观类对子系统的接口封装,使得系统更加容易使用

缺点

  • 外观类接口会过于庞大
  • 没有遵循开闭原则,业务变化时需要修改外观类

代码实现:没有特定代码结构

  • 场景:生活中有很多这样的例子,我们经常使用的智能手机就是一个外观模式的例子,能够打电话、拍照等功能,而打电话和拍照又是独立的功能系统。
  1. 手机功能
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public interface Phone {
public void call();
public void hangUp();
}



class PhomeImpl implements Phone{
public void call() {
System.out.println("打电话");
}


public void hangUp() {
System.out.println("挂电话");
}
}
  1. 拍照功能
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public interface Camera {
public void open();
public void takePhoto();
public void close();
}



class SnoyCamera implements Camera {

public void open() {
System.out.println("开启相机");
}


public void takePhoto() {
System.out.println("拍照");
}

public void close() {
System.out.println("关闭相机");
}
}
  1. 组装手机
1
2
3
4
5
6
7
8
9
10
11
12
13
public class SmartPhone {
private PhomeImpl phome = new PhomeImpl();
private SnoyCamera camera = new SnoyCamera();

public void call(){
phome.call();
}

public void takePhoto(){
camera.open();
camera.takePhoto();
}
}

5. 代理模式(Proxy)

  • 代理模式: 为其他对象提供一个代理,并由代理对象控制原有对象的引用。也称作委托模式
  • 其实代理模式无论是在日常开发中还是设计模式汇总都随处可见,中介者模式的中介者对象也是代理模式的应用,其他的对象的交互都是交给终结者对象处理的。而在生活中就更多的类似代理模式的例子。例如:抢票插件,科学上网等等。

UML类图:

mark

  • subject: 抽象主体类(也可以是接口),声明共同的功能
  • RealSubject: 真实的主体类,也就是被代理的类,负责执行具体的业务逻辑方法;客户类调用代理类间接调用其定义的方法。
  • ProxySubject:代理主体类,也就是代理类,持有一个被代理类的真实对象,在实现抽象主体类共同方法中调用被代理类的相对应的方法,起到代理的作用。
  • Client: 客户类,直接去访问代理
5.1 静态代理
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
/**抽象主体*/
public interface Subject {
public void visit();
}


/**被代理主体*/
class RealSubject implements Subject{
public void visit() {
System.out.print("this is real subject");
}
}


/**代理主题*/
class ProxySubject implements Subject{
// 真实主体
private RealSubject realSubject;

public ProxySubject(RealSubject realSubject) {
this.realSubject = realSubject;
}


public void visit() {
System.out.print("proxy start");
realSubject.visit();
System.out.print("proxy end");
}
}


/**客户类*/
class Client{
public static void main(String[] args) {
// 创建被代理对象
RealSubject realSubject = new RealSubject();
// 创建代理对象
ProxySubject proxySubject = new ProxySubject(realSubject);
// 调用方法
proxySubject.visit();
}
}

静态代理的好处:

  • 可以使得我们的真实角色更加纯粹 . 不再去关注一些公共的事情 .
  • 公共的业务由代理来完成 . 实现了业务的分工 ,
  • 公共业务发生扩展时变得更加集中和方便 .

缺点:

  • 类多了 , 多了代理类 , 工作量变大了 . 开发效率降低 .
5.2 动态代理
  • 上一部分介绍的,其实就是静态代理,也就是在代码的编译阶段生成代理类来完成代理对象的一系列操作。
  • 动态代理则是在运行时动态生成代理类对象。
  • 代理对象的生成则是利用JDK中的java.lang.reflect.Proxy 类 ,使用newProxyInstance 方法则可以创建一个我们所需要代理的对象
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
// 主要是newProxyInstance方法,动态生成代理类
@CallerSensitive
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
throws IllegalArgumentException
{
Objects.requireNonNull(h);

final Class<?>[] intfs = interfaces.clone();
final SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
}

/*
* Look up or generate the designated proxy class.
*/
Class<?> cl = getProxyClass0(loader, intfs);

/*
* Invoke its constructor with the designated invocation handler.
*/
try {
if (sm != null) {
checkNewProxyPermission(Reflection.getCallerClass(), cl);
}

final Constructor<?> cons = cl.getConstructor(constructorParams);
final InvocationHandler ih = h;
if (!Modifier.isPublic(cl.getModifiers())) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
cons.setAccessible(true);
return null;
}
});
}
return cons.newInstance(new Object[]{h});
} catch (IllegalAccessException|InstantiationException e) {
throw new InternalError(e.toString(), e);
} catch (InvocationTargetException e) {
Throwable t = e.getCause();
if (t instanceof RuntimeException) {
throw (RuntimeException) t;
} else {
throw new InternalError(t.toString(), t);
}
} catch (NoSuchMethodException e) {
throw new InternalError(e.toString(), e);
}
}

newProxyInstance 是一个Proxy的静态方法,并且接收三个参数

  • ClassLoader loader 类加载器
  • Class<?>[] interfaces 目标对象实现的接口类型
  • InvocationHandler h 处理事件的对象,InvocationHandler 是一个接口,执行目标对象的方法,会触发InvocationHandlerinvoke

另外一个重要的类当然就是InvocationHandler 了,其中最重要的方法就是invoke

1
2
3
4
public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
}
  • Object proxy: 执行方法的代理对象
  • Method method :被执行的方法的对象
  • bject[] args : 被执行方法的参数
  • 返回值:代理对象调用方法所返回的值

依然使用Subject作为例子,看看简单的代码实现(代码没有封装)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Client{
public static void main(String[] args) {
// 创建被代理对象
final RealSubject realSubject = new RealSubject();

// 创建被代理对象
Subject subject = (Subject) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),
new Class[]{Subject.class},
new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 第一个参数是要代理的对象
// 第二个参数是需要传入的参数
return method.invoke(realSubject,args);
}
});

// 调用方法
subject.visit();
}
}
  • 从代码上来看,比之前静态代理要简单很多,没有那么多类和对象,但是相对的代码在性能上有牺牲,而且对于不太熟悉反射相关的知识的开发者并不是太友好。
  • 通过反射类ProxyInvocationHandler 回调接口实现JDK动态代理,要求被代理类必须实现一个接口,但事实上并不是所有的类都有接口,对于没有实现接口的类,便无法使用该方法实现动态代理。
5.3 其他代理分类

静态代理和动态代理是代码方法来区分代理模式,也可以从使用范围来区分不同的代理实现

  • 远程代理(Remote Proxy): 为某个对象在不同的内存地址空间提供局部代理。使得系统可以将Server 部分的实现隐藏,以便Client不必考虑Server的存在
  • 虚拟代理(Virtual Proxy) : 使用一个代理对象表示一个十分耗资源的对象并在真正需要时候才创建
  • 保护代理(Protection Proxy): 使用代理控制对原始对象的访问.该类型的代理常被用于原始对象有不同访问权限的情况.
  • 智能引用(Smart Reference) : 在访问原始对象时执行一些自己的附加操作并对指向原始对象的引用计数.

这里要注意的是,静态和动态代理都可以应用于上述4种情形,两者是各自独立的变化。

总结:

代理模式使用非常广泛,基本在其他的设计模式中也能看到代理模式的影子,但是使用时针对性较强,而且模式本身并没有什么突出的优缺点,基本上可以放心使用

通用的动态代理实现类:

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
public class ConfigProxy implements InvocationHandler {

// 要代理的对象
private Object target;

public void setTarget(Object target) {
this.target = target;
}


// 生成代理类
public Object getProxy(){
return Proxy.newProxyInstance(
this.getClass().getClassLoader(),
target.getClass().getInterfaces(),
this);
}


// proxy : 代理类
// method : 代理类中要代理的方法
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// ....
// 业务逻辑代码
return method.invoke(target,args);
}
}

JDK:

  • java.lang.reflect.proxy
  • java.lang.reflect.InvocationHandler
  • Spring的 Aop

6. 适配器模式(Adapter)

定义:此模式的作用就是兼容原本接口不匹配的两个类,起到桥梁的作用。

6.1 类的适配器模式
  • 类的适配器模式:采用继承实现

mark

  • Target:目标接口,也就是期望得到的接口
  • Adaptee:需要适配的接口
  • Adapter:适配器,负责把Adaptee转换成Target的类

不说远了,就说手机充电头,其实就是一个电源适配器,生活用电的电压是220v,而手机充电的电压是5v,所以这时候就需要使用到电源适配器将220v的电压转换为5v(如果你是直接插在插板的USB接口上的话,当我没说)。

对应上面的UML图, 生活电压就是Adaptee,手机充电电压就是Target,不用多说,电源适配器自然就是Adapter了。

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
// Target接口
public interface Volt5 {
public int getVolt5();
}

// Adaptee类
class Volt220{
public int getVlot220(){
return 220;
}
}


// Adapter类
class VoltAdapter extends Volt220 implements Volt5{
public int getVolt5() {
return 5;
}
}


class Client{
public static void main(String[] args) {
VoltAdapter adapter = new VoltAdapter();
System.out.println("输出电压" + adapter.getVolt5());
}
}
6.2 对象适配器模式
  • 与类适配器不同的是,对象适配器模式不使用继承关联链接Adaptee 类,而是使用代理关系连接到Adaptee

mark

  • 这种实现方式直接将要被适配的对象传递到Adapter 中,使用组合的形式实现接口的兼容效果。
  • 一般常用的适配器模式都是对象适配器模式
  • 还有就是Adaptee 对象不会被暴露出来,因为没有继承被适配的对象。
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
// Target接口
public interface Volt5 {
public int getVolt5();
}


//Adaptee类
class Volt220 {
public int getVlot220(){
return 220;
}
}


// Adapter类
class VoltAdapter implements Volt5{

// 组合的形式
Volt220 volt220;

public VoltAdapter(Volt220 volt220) {
this.volt220 = volt220;
}

public int getVolt5() {
return 5;
}


public int getVolt220(){
return volt220.getVlot220();
}
}


class Client{
public static void main(String[] args) {
VoltAdapter adapter = new VoltAdapter(new Volt220());
System.out.println("输出电压" + adapter.getVolt5());
}
}

JDK:

7. 组合模式(Composite)

  • 组合模式(Composite Pattern),也称作部分整体模式(Part-Whole Pattern)
  • 定义:将一组相似的对象看作一个对象来处理,并根据一个树状结构来组合对象;对象都提供一个统一的方法去访问相应的对象来处理多个对象的同一性的问题

组合模式属于结构设计模式之一,而其设计目的就是将对象组合成树形结构以表示“部分-整体”的层次结构,使得用户对单个对象的组合对象使用具有一致性;所以决定组合模式的设计基础就是树状结构。

组合模式所使用的情况也就是树状结构或者适用树状结构来解决问题的情况。

UML类图

mark

  • Component:抽象节点,为组合中的对象声明统一接口。
  • Composite:可以储存子节点的节点对象,并实现抽象节点的有关操作。
  • Leaf:叶子节点,没有子节点的对象了。
  • Client:组合节点对象,进行操作

注意:

  • 这里所描述的是透明的组合模式,可以看到Component类中除了统一的操作方法doSomthing()方法以外,还有操作子节点的相关方法,而叶子节点Leaf类定义就是没有叶子节点的,显然这些操作子节点的方法就是多余的。
  • 如果要让leaf类不继承这些方法,只能将compoent 类中的这些方法放到composite类中,然而这样的设计方式与依赖倒置原则相违背,所以这里没有采用这种组合模式(安全的组合模式)

代码实现:

  • 说到组合模式,最适合的就是文件系统的结构了,文件夹中就子文件夹和文件,子文件夹中可能又是如此,典型的树状结构。
  • 抽象的目录类,有目录名,有输出目录名,并提供添加目录,删除目录和清除目录的方法
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
public abstract class Directory {
// 当前目录名
private final String name;

public Directory(String name) {
this.name = name;
}

/**
* 输出目录结构
*/
public abstract void print();


/**
* 添加一个文件或者文件夹
* @param dir
*/
public abstract void addDir(Directory dir);

/**
* 删除一个文件或者文件夹
* @param dir
*/
public abstract void removeDir(Directory dir);


/**
* 清空目录
*/
public abstract void clear();


/**
* 获取目录中的所有目录
* @return
*/
public abstract List<Directory> getDirs();


/**
* 获取目录名
* @return
*/
public String getName() {
return name;
}
}
  • 文件夹类,申明一个集合存储自身的目录,并实现具体的操作方法,在print()方法中循环调用集合中目录的print()方法输出
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
public class Folder extends Directory {
/**
* 当前文件夹下的所有目录元素
*/
protected List<Directory> directories = new ArrayList<Directory>();


public Folder(String name) {
super(name);
}

// 打印文件目录
public void print() {
System.out.println(getName() + "[");
Iterator<Directory> iterator = directories.iterator();
while (iterator.hasNext()){
Directory directory = iterator.next();
directory.print();
if (iterator.hasNext()){
System.out.println(",");
}
}
System.out.println("]");
}


public void addDir(Directory dir) {
directories.add(dir);
}

public void removeDir(Directory dir) {
directories.remove(dir);
}

public void clear() {
directories.clear();
}

public List<Directory> getDirs() {
return directories;
}
}
  • 文件类:实现了print()方法,由于没有子目录,相关操作的方法都抛出异常
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 File extends Directory {
public File(String name) {
super(name);
}

public void print() {
System.out.println(getName());
}

public void addDir(Directory dir) {
throw new UnsupportedOperationException("文件对象不支持该操作");
}

public void removeDir(Directory dir) {
throw new UnsupportedOperationException("文件对象不支持该操作");
}

public void clear() {
throw new UnsupportedOperationException("文件对象不支持该操作");
}

public List<Directory> getDirs() {
throw new UnsupportedOperationException("文件对象不支持该操作");
}
}
  • 测试类:模拟输出C盘的结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Client {
public static void main(String[] args) {
Directory root = new Folder("C");
root.addDir(new Folder("windows"));
Directory program = new Folder("Program File(x86)");
program.addDir(new Folder("Intellij"));
program.addDir(new File("cache"));

root.addDir(new Folder("windows"));
root.addDir(new File("log.txt"));
root.addDir(new File("null.txt"));

root.print();
}
}

总结:

组合模式和解释器模式有一定的类同,都涉及递归调用。

但是组合模式所提供的属性层次结构使得可以同等对象单个对象和多个对象。

不过是以牺牲单一原则换来的,而组合模式是通过继承来实现的,这样缺少了些扩展性。

优点

高层模块可以一致的使用一个组合结构或者其中单个对象,不必关心处理的单个对象还是整个组合结构,简化代码

对于枝干构件和叶子构件的新增很方便

通过枝干对象和叶子对象的递归组合,可以形成复杂的树形结构,同时保持简单的方式进行控制

JDK:

  • javax.swing.JComponent#add(Component)
  • java.awt.Container#add(Component)
  • java.util.Map#putAll(Map)
  • java.util.List#addAll(Collection)
  • java.util.Set#addAll(Collection)

简书参考:

参考博客https://www.jianshu.com/u/70bd9fefe61f

打赏
  • 版权声明: 本博客所有文章除特别声明外,均采用 Apache License 2.0 许可协议。转载请注明出处!
  • © 2019-2022 Zhuuu
  • PV: UV:

请我喝杯咖啡吧~

支付宝
微信