常用设计模式

0x00 概述

学习设计模式是一个漫长的过程,也是一个程序员登堂入室的必经之路。这篇博文就是记录学习设计模式的过程,会持续更新(属于天狼星计划的一部分,严肃脸),每个模式都有对应的学习代码,学习在https://github.com/xsfelvis/DesignPattern.git

0x01 大纲

创建型 结构型 行为型
对象 Factory Method Adapter_Class Interpreter
Template Method
Abstract Factory
Builder
Prototype
Singleton
Adapter_Object
Bridge
Composite
Decorator
Facade
Flyweight
Proxy
Chain of Responsibility
Command
Iterator
Mediator
Memento
Observer
State
Strategy
Visitor

0x02 Factory Method (简单工厂方法)

Factory Method (简单工厂方法)

又称简单工厂模式或者静态工厂模式

  • 使用动机

定义一个用于创建对象的接口,让子类决定去实例化哪一个类。Factory Method使用一个类的实例化延迟到其子类。

  • 通用UML图

工厂模式

这个uml基本概括了工厂模式的核心,其他的都是可以根据这个衍生发展。

主要分为4大模块,

抽象工厂:其为工厂方法模式的核心;

具体工厂:体现实现具体的业务逻辑

抽象产品:是工厂方法模式所创建的产品的父类

具体产品:实现抽象产品的某个具体产品的对象

  • 适用场景
  1. 当一个类不知道它所必须创建对象类的时候
  2. 当一个类希望由它的子类来指定它所创建的对象的时候
  3. 当类将创建对象的职责委托给多个帮助子类中的某一个,并且你希望将哪一个帮助子类是代理者这一信息局部化的时候
  • 要点分析

工厂根据type生成对应示例

在最简单的工厂中,往往传入产品的type,然后得到具体的产品示例。在我们金融app中分享部分的代码就是按照这个思路来处理的(代码已经脱敏)

1
2
3
4
5
6
7
8
public void onShareButtonClick(int btnType) {
switch (btnType){
case R.id.weixin:
client = ShareFactory.getClient(this, ShareFactory.ClientEnum.wx);
((WXClient) client).setType(SendMessageToWX.Req.WXSceneSession);
break;
……
}

可以看出通过在工厂中定义一个枚举类型,然后根据传入的type生成对应的示例

传入class 根据反射更加简洁的生产出具体产品

1
2
3
4
5
6
7
8
9
/*
* 抽象工厂方法
* 具体交由子类实现
*/
public abstract class Factory {
//public abstract Product createProduct();
//使用抽象类
public abstract <T extends Product> T createProduct(Class<T> clz);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

/*
* 具体工厂实现类
*/
public class ConcreateFactory extends Factory{
//根据传入的class通过反射取得
@Override
public <T extends Product> T createProduct(Class<T> clz) {
Product product = null;
try {
product = (Product) (Class.forName(clz.getName())).newInstance();
}catch (Exception e){
e.printStackTrace();
}
return (T) product;

}
/*@Override
public Product createProduct() {
return new ProductA();
}*/
}

具体使用

1
2
3
4
5
6
7
8
9

public class TestClient {
public static void main(String[] args) {
Factory factory = new ConcreateFactory();
//Product product = new ProductA();
Product product = factory.createProduct(ProductA.class);
product.method();
}
}

工厂方法模式 降低了对象之间的耦合度,而且依赖于抽象的架构,扩展性比较好

0x03 Abstract Factory(抽象工厂模式)

Abstract Factory(抽象工厂模式)

  • 使用动机

提供一个创建一些列相关或相互依赖对象的接口,而无需指定它们的具体类

相比之前的工厂都是生产具体的产品,抽象工厂生产的产品是不确定的。起源于操作系统的图形化结业方案,每个操作系统都是一个产品类,而各自的按钮与文本框也是构成一个产品类,这两种产品都有自己的特性,如android和ios的Button和TextView等。

  • 通用UML图

abstract factory

跟普通工厂一样,主要的类还是4个:

AbstractFactroy:抽象工厂角色,声明了一组用于创建一种产品的方法,每个方法对应一种产品。

ConcreateFactroy:具体工厂角色,实现在抽象工厂中定义的产品的方法,生成一组具体产品,这些产品构成一个产品种类,每个产品都位于某个产品等级结构中,如ConcreateFactroy1和ConcreateFactroy2。

AbstractProduct:抽象产品角色,它为每种产品声明接口,如AbstractProductA和AbstractProductB。

ConcreateProduct:具体产品角色,它定义具体工厂生产的具体产品对象,实现抽象产品接口中声明的业务方法。

  • 适用场景

    1. 一个系统要独立于它的产品的创建、组合和表示
    2. 一个系统要由多个产品系列中的一个来配置时
    3. 当需要强调一系列相关的产品对象的设计以便进行联合使用
    4. 当提供一个产品类库,只想显示它们接口而不是实现

缺点:

抽象工厂的缺点就是随着工厂类增多而导致类文件非常多,这个在实际开发中要权衡使用

0x04 Builder(建造者模式)

  • 使用动机

将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示

  • 适用场景
  1. 当要实例化的类是在运行时刻指定时,例如,通过动态装载;
  2. 为了避免创建一个与产品类层次平行的工厂类层次时;
  3. 当一个类的实例只能有几个不同状态组合中的一种时,建立相应得原型并克隆它们可能比每次用合适的状态手工实例化该类更方便一些。
  • 通用UML图

Builder

建造者模式通常也是有4大部分组成

Product产品类: 产品的抽象类

Builder: 抽象Builder类,规范产品的组建,一般是由子类实现具体的构建过程

ConcreateBuilder: 具体的Builder类

Director: 统一组装过程

  • 要点分析

链式调用

通常在开发中,Director可以当做内部类来处理,直接使用一个Builder来进行对象的组装,这个Buidler通常称为链式调用,它的关键点是每个setter方法都返回自身即return this 这样就使得setter方法可以称为链式调用,套路如下:

1
2

new TestBuilder().setA("A").setB("B").create();

Dialog的经典实现

0x05 单例模式

  • 使用动机

保证一个类仅有一个实例,而且自行实例化并向整个系统提供这个实例

  • 使用场景
  1. 当类只能有一个实例而且客户可以从一个众所周知的访问点访问它时
  2. 当这个唯一实例应该是通过子类化可以扩展,并且客户应该无需更改代码就可以使用一个扩展的实例时
  • 核心UML图

核心角色有2个

Client 高层客户端
Singleton 单例类

  • 要点分析
  1. 构造函数不对外开放,一般为Private
  2. 通过一个静态方法或者枚举类返回单例类对象
  3. 确保单例类的对象有且只有一个,尤其是在多线程的环境下
  4. 确保单例类对象在反序列化是不会重新构建对象
  5. 实现方式:

单例看似简单,其实很有门道的,常见的比如饿汉模式懒汉模式双重检验(valtile)等,下面将一一道来。

饿汉模式&懒汉模式

首先需要介绍下什么是延迟加载,即等到真正使用的时候才去创建实例,不用的时候不去创建实例。

因此从速度和反应时间角度来看,非延时加载(饿汉模式)好,从资源利用率上看,延时加载(又称懒汉模式)好。
先从两个简单的入手:

第一种:非延时加载(饿汉模式)

1
2
3
4
5
6
7
8
9
10
11

public class HungrySingleton {
public HungrySingleton() {
}

private static final HungrySingleton instance = new HungrySingleton();

public static HungrySingleton getInstance() {
return instance;
}
}

第二种:同步延迟加载(懒汉模式)

1
2
3
4
5
6
7
8
9
10
11
12
13
14

public class LazySingleton {
private static LazySingleton instance = null;

public LazySingleton() {
}

public static synchronized LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}

双重检验(valtile) 同步延时加载

第三种:双重检测同步延迟加载(懒汉模式)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

public class DCSingleton {
private volatile static DCSingleton instance = null;

public DCSingleton() {
}
public static DCSingleton getInstance(){
if(instance==null){
synchronized (DCSingleton.class){
if(instance==null){
instance = new DCSingleton();
}
}

}
return instance;
}

}

第二种单例方式在java中会行不通的,可以看到上面的代码对instance进行二次检查,第一层判断是为了避免不必要的同步,第二层判断是为了在null情况下创建实例,目的是避开过多的同步,需要声明instance时定义为volatile即可。java中之所以需要双重加锁进行单例,是由于java内存模型允许无序写入,假设未加上关键字volatile时,执行到instance=new Singleton()时,实际上它并不是一个原子操作,会被编译成多条汇编指令,大致做了3件事:

(1)给DCSingleton 的实例分配内存
(2)调用DCSingleton()的构造函数,初始化成员字段
(3)将instance对象指向分配的内存空间(此时instance就不是null)

由于java编译器无序写入,Cache、寄存器到主内存的回写顺序,上面第二第三条顺序是无法保证的,即可以是1-2-3也有可能是1-3-2,如果是后者,并且在3执行完毕、2未执行之前,被切换到线程B上,此时A线程已经执行了第三点,instance已经是非空了,B线程直接取走instance再使用时就会报错,这就是DCL失效的问题。而加上volatile关键字则禁止了指令的重排序从而避免了这个问题。

更加优雅的单例

DLC并不优雅,在《java 并发编程实践》一书中建议使用下面的单例

1
2
3
4
5
6
7
8
9
10
11
12
13
14

public class BetterSingleton {
public BetterSingleton() {
}

public static BetterSingleton getInstance() {
return SingletonHolder.instance;
}

private static class SingletonHolder {
private static final BetterSingleton instance = new BetterSingleton();
}

}

这种方式当第一次加载是不会初始化实例instance,只有第一次调用getInstance方法时才会导致instance被初始化。这样不会能够确保线程安全,也能够保证单例对象的唯一性,同时也延时了单例的实例化,这是推荐的单例模式实现方式

使用容器实现单例模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

public class SingletonManager {
private static Map<String,Object> objMap = new Hashtable<>();

public SingletonManager() {
}
public static void registerService(String key,Object instance){
if(!objMap.containsKey(key)){
objMap.put(key,instance);
}
}
public static Object getService(String key){
return objMap.get(key);
}
}

可以在程序的初始将多种单例类型注入到一个统一的管理类中,在使用时根据key获取对象对应类型的对象,这种方式使得我们可以管理多种类型的单例,并且在使用可以通过统一的接口进行获取操作,降低了用户的成本,也对用户隐藏了具体实现,降低了耦合度。

0x06 策略模式

  • 使用动机

策略模式定义了一些列的算法,并将每一个算法封装起来,而且使他们可以相互替换。。策略模式让算法独立于使用它的客户端而独立变化。

  • 使用场景
  1. 针对同一问题的多种处理方式,仅仅是具体行为有差别时
  2. 需要安全地封装多种同一类型的操作时
  3. 出现同一抽象类有多个子类,而又需要使用if-else或者switch-case来选择具体子类时
  • 核心UML图

策略模式

策略模式主要有3个核心

Context: 用来操作策略的上下文环境

Stragety:策略的抽象

ConcreateStragetyX:具体策略的实现

  • 要点分析

通过一个Context(实际中可能称作Manager/Controller)结合注入的方式来管理不同的策略

干掉了多余的if-else

可扩展性变得很强

缺点: 随着策略的增加,子类会变得繁多

0x07责任链模式

  • 动机

使很多对象都有机会处理请求,从而避免请求的发送者和接受者之间的耦合关系,将这些对象连成一条链,并沿着这条链条传递,直到有一个对象处理它为止。

  • 使用场景
  1. 有多个的对象可以处理一个请求,哪个对象处理该请求,运行时刻自动确定
  2. 你想在不明确指定接收者的情况下,向多个对象中的一个提交一个请求
    3.可处理一个请求的对象集合应被动态指定
  • 核心UML

责任链模式

主要抽象成两个角色:

Hanlder: 抽象处理者角色,声明一个请求处理的方法,并在其中保持Handler对象的引用

ConcreateHanlder: 具体处理者角色,对请求进行处理,如果不能处理则将该请求转发给下一个节点上的对象处理

  • 要点分析

精髓在与保持Handler对象的引用

1
2
3
4
5
6

public abstract class Handler {
protected Handler successor;//下一节点的处理者

public abstract void handleRequest(String condition);
}

通常,责任链中的请求和处理规则是不尽相同的,在这种请求下可以将请求进行封装,同时对请求的处理规则也进行封装作为一个独立的对象

下面将介绍一下基于核心UML的稍微复杂的扩展,UML图如下:

复杂责任链

android源码中最典型就是Touch事件的分发

可以借鉴责任链的思想来优化 有序广播

优缺点

  1. 最大的缺点就是对链中请求的遍历,如果处理者爱多那么遍历必定会影响性能,特别是在一些递归调用中,需要谨慎使用

  2. 优点显而易见,就是请求者和处理者的关系解耦

0x07 状态模式

  • 使用动机

允许一个对象再起内部状态改变时改变它的行为,这个对象看起来像是改变了其类

  • 使用场景
  1. 一个对象的行为取决于它的状态,并且它必须在运行时根据状态改变它的行为
  2. 代码中包含大量与对象状态有关的条件语句,且这些分值依赖于该对象的状态。状态模式将每一个条件分值放入一个独立的类中,使得可以根据对象自身情况将对象的状态作为一个对象,这个对象可以不依赖于其他对象而独立变化,这样通过多态来去除过多的、重复的if-else等分支语句
  • 核心UML图

state

抽象成3和核心角色

Context: 环境类,定义客户感兴趣的接口,维护一个State子类实例,这个子类定义了对象当前的状态

State: 抽象状态类或者状态接口,定义一个或者一组接口,表示该状态下的行为

ConcreateX: 具体状态类,每一个具体的状态类实现抽象State中定义的接口,从而达到不同的行为

  • 要点分析
  1. 状态模式的关键是在不同的状态下对于同一行为在不同状态下有着不同的响应

0x08 观察者模式

  • 使用动机

定义对象间一种一对多的依赖关系,使得每当一个对象改变状态,则所有依赖于它的对象都会得到通知并且被自动更新

  • 使用场景
  1. 关联行为场景,需要注意的是关联行为是可拆分的,而不是组合关系
  2. 事件多级触发场景
  3. 跨系统的消息交换场景,如消息队列,事件总线的处理机制
  • 核心UML图

Observer

核心为2个

Observer:抽象观察者,该角色是观察者的抽象类,它定义了一个更新接口,使得在主题更改时通知更新自己

ConcreateObserver: 具体的观察者,该角色实现抽象观察者角色所定义的更新接口,以便在主题的状态发生改变时更新自己

  • 要点分析

1.在java中观察者可以继承Observer,发布者可以继承Observable来迅速实现

2.观察者模式最经典的就是listview的adapter,当然也有EventBus这种事件总线的库也是采用观察者模式

Note:

Step1在ListView中设置Adapter时会构建一个AdapterDataSetObserver,并且注册到Adapter中,这就是一个观察者。Adapter中包含一个数据集可观察者DataSetObservable。
Step2 在数据发生变更时,开发者手动调用Adapter.notifyDataSetChanged,而notifyDataSetChanged会去调用notifyChanged函数,该函数会遍历所有观察者的onChanged函数。
Step3 在AdapterDataSetObserver的onChanged函数中会获取Adapter中数据集的新数据,然后调用ListView的requestLayout()方法重新布局更新用户界面

0x09 代理模式

  • 使用动机

为其他对象提供一种代理以控制这个对象的访问。

  • 适用场景
  1. 当无法或者不想直接访问某个对象或者访问某个对象存在困难时可以通过一个代理对象来间接访问,为了保证客户端使用的透明性,委托对象与代理对象需要实现相同的接口
  • 核心UML图

代理模式

主要有3个角色组成:
Subject:抽象主题类,该类主要职责是声明真实主题与代理的共同接口方法,该类既可以是一个接口也可以是一个接口

RealSubject:真实主题类,该类也被称为委托类或被代理类,该类定义了代理表示的真实对象,由其执行具体的业务逻辑方法。

ProxSubject:代理类,该类也称为委托类,该类持有一个对真实主题类的引用,在其所实现的接口方法中调用真实主题类中相应的接口方法执行,以此起到代理的作用

Client: 客户类,即使用代理类的类型

  • 要点分析
  1. 通常代理模式分为静态代理动态代理,静态代理即在我们的代码运行前代理类的class编译文件就已经存在,而动态代理则与静态代理相反,通过反射机制动态地生成代理者对象,而java也给我们提供了一个便捷的动态代理接口InvocationHandler实现该接口需要重写其调用方法invoke
  2. 静态代理和动态代理只是从code方面来区分代理模式的两种方法,我们也可以通过适用范围来区分不同类型的代理实现
    • 远程代理(Remote Proxy):为某个对象在不同的内存地址空间提供局部代理,使系统可以将Server部分实现隐藏,以便Client可以不考虑Server的存在
    • 虚拟代理(Virtual Proxy):虚拟代理经常直到我们真正需要一个对象时才创建它,当对象在创建前和创建中时,由虚拟代理来扮演对象的替身,对象创建后,代理就会将请求直接委托给对象
    • 保护代理(Protection Proxy):使用代理控制对原始对象的访问,该类型的代理常用于原始对象有着不同的访问权限的情况保护模式实例

未完待续……

0%