前段时间和一个好哥们聚餐,他提到了我好久前准备的设计模式札记,问:写得怎么样了?答曰:大概有一半多点。因为项目跟进,已经很长时间基本没有更新。哥们笑着说:那你要继续哈哈。
谈起设计模式,其实有好多可以讲的。比如:之前面试过程中,对较多候选人问过一些设计模式的问题。有很多同学貌似学习设计模式有一个误区:就是努力地去记住模式长啥样?但是却忽视了模式其所解决的问题是什么。
本文主要是对如何学好设计模式做一个简单的阐述,也算是一个设计模式爱好者对自己学习设计模式的学习回顾、心得分享吧。主要包括如下几个部分:
- 学习设计模式的“门槛“ -- 简述学习设计模式之前我们最好具备哪些知识
- 学习设计模式的不同阶段 -- 简述个人在熟悉、进阶、实践、沉淀四个阶段所做的一些事情
- 学习设计模式的小结 -- 简述我们学习设计模式的“误区”等
学习设计模式的“门槛”
个人而言,学习设计模式之前最好已经有一定的知识储备,主要体现在如下三个方面:
- 具备一定的面向对象的知识
- 懂点统一建模语言(UML)的知识
- 最好有一定的项目经验
具备一定的面向对象的知识
这个怎么讲呢,以Java语言为例,如果对其特征( 封装、继承、抽象和多态 )还不清楚,那么,我还是建议先把这些弄清楚之后,才去学习设计模式,毕竟在设计模式的学习中,抽象、多态都是很重要的。
懂点统一建模语言(UML)的知识
设计模式基本上是通过UML的类图和时序图来表示的。以一个观察者模式为例,其类图和时序图如下:
所以,学习设计模式之前,至少对UML的类图和时序图表示有所了解。
比如,类图中类与类之间的关系:
- is -- 用于表示继承(泛化)、实现
- has -- 用于表示关联、聚合和组合
- use -- 表示依赖
具体UML的知识点这里就不再展开,有兴趣的同学可以去阅读UML相关书籍或者文章。
最好有一定的项目参与经验
具体项目有具体解决的问题,一般项目中都会有较多设计模式应用的影子,如果参与过一定的项目,可以结合问题和代码,对某个设计模式的应用有更好的理解和体感。
PS:如果没有一定的项目经验,也可以通过阅读源代码来看看其中设计模式在哪些场景中做了使用,主要解决了什么问题。源代码可以选择JDK、Spring、Mybatis、Guava等。
接下来,来回顾和分享下我个人学习模式所经历的不同阶段:
阶段一、熟悉
记得我第一次接触设计模式是大学《设计模式》的课上,忘记是必修课还是选修课了,当时老师使用的书是下面这本书:
不过我也买了如下这本“砖头书”作为我的辅导书,个人觉得是一本很不错的书。
在这个阶段的学习算是比较中规中矩吧,毕竟有考试的要求。这个阶段,主要学习了设计模式的由来、含义、组成要求、设计原则、设计模式分类以及课堂选讲的差不多有10个设计模式。
设计模式由来
模式最早是应用在建筑学上,然后Erich Gamma、Richard Helm、Ralph Johnson和John Vlissides四人(GOF)将模式的思想引入软件工程学,他们在1994年归纳发表了23种在软件开发中使用频率较高的设计模式,旨在用模式来统一沟通面向对象方法在分析、设计和实现间的鸿沟。
设计模式的含义和组成要素
每一个模式描述了一个在我们周围不断重复发生的问题,以及该问题的解决方案的核心。这样,你就能一次又一次地使用该方案而不必做重复劳动。可以说,模式是前辈们对代码开发经验的总结,是解决特定问题的一系列的“套路“,使用模式可以提高代码的可复用性、可扩展性、可维护性等
一般而言,一个模式由四个要素组成:
- 模式名称(Pattern Name) - - 通过一两个词来描述模式的问题、解决方案和效果,以便更好地理解模式并方便开发人员之间的交流,绝大多数模式都是根据其功能或者模式结构来命名的。
- 问题(Problem)- - 描述了应该在何时使用模式,它包含了设计中存在的问题以及问题存在的原因。
- 解决方案(Solution)- - 描述了一个设计模式的组成部分,以及这些组成成分之间的相互关系,各自的职责和协作方式,通常解决方案通过UML类图和核心代码来进行描述
- 效果(Consequences)- - 描述了模式的优缺点以及在使用模式时应权衡的问题。
设计模式的分类
虽然GOF设计模式只有23个,但是他们各具特色,每个模式都为某一个可重复的设计问题提供了一套解决方案。根据它们的用途,设计模式可分为创建型(Creational)、结构型(Strutural)和行为型(Behavioral)三种,其中:
- 创建型模式 主要用于是描述如何创建对象
- 结构型模式主要用于描述如何实现类或者对象的组合
- 行为型模式主要用于描述类或者对象怎么交互以及怎么分配职责
设计原则
更多的设计模式原则内容可以参考《设计模式几大原则》
问题
在这个阶段,一下子好多设计模式要去接受,其实挺容易遗忘的。可以结合生活的一些示例来加深记忆。
比如:
建造者模式
适配器模式
中介者模式
模版方法模式
享元模式
备忘录模式
... ...
体感
在这个阶段,大致对设计模式有了一个比较宏观的了解。知道很多模式的样子,但是缺少足够的理解和思考。比如:
- 单例 - - 知道根对象的初始化有懒汉和饿汉2种方式。单例构造函数使用private,提供一个静态方法如getInstance()获取对象。如果是饿汉式,需要使用syncronized修饰方法;或者使用D.C.L(双重检测锁),同时保证对象使用volatile修饰,等等。
更多单例模式可阅读《单例模式详解》。
- 建造者- - 知道使用Builder最终会构建一个完整的对象,也知道怎么做去完成建造者模式。
更多建造者模式可阅读《建造者模式浅析》
... ...
至于为什么要这样?用在什么场景中?.... ? 缺少对模式真正在解决的问题的认识。
阶段二、进阶
在这个阶段,我主要是带着更多的问题(为什么要这样做呢?)去巩固和学习,真正去理解和感受设计模式的味道。我主要通过如下几个方面来进行了。
- 读源码 - - 看JDK、Spring、MyBatis、Guava等源代码
- 扩展性思考 - - 带着自己的思考去理解
- 记笔记
看源码
可以从JDK、Spring、MyBatis、Guava、JUnit等入手去感受其使用到的一些模式,增加我们对设计模式在什么场景使用的认知和体感。
如:
饿汉式单例
JDK源码中的Runtime就是饿汉式单例。
枚举单例
Guava包的MoreExecutors类中,可以看到枚举单例的示例。
代码语言:javascript复制 public static Executor directExecutor() {
return DirectExecutor.INSTANCE;
}
/** See {@link #directExecutor} for behavioral notes. */
private enum DirectExecutor implements Executor {
INSTANCE;
@Override
public void execute(Runnable command) {
command.run();
}
@Override
public String toString() {
return "MoreExecutors.directExecutor()";
}
}
内部类单例
代码语言:javascript复制class ThreadLocalBufferManager
{
... ...
/**
* Returns the lazily initialized singleton instance
*/
public static ThreadLocalBufferManager instance() {
return ThreadLocalBufferManagerHolder.manager;
}
... ...
/**
* ThreadLocalBufferManagerHolder uses the thread-safe initialize-on-demand, holder class idiom that implicitly
* incorporates lazy initialization by declaring a static variable within a static Holder inner class
*/
private static final class ThreadLocalBufferManagerHolder {
static final ThreadLocalBufferManager manager = new ThreadLocalBufferManager();
}
}
装饰者模式
MyBatis中的Cache采取的就是装饰者模式
访问者模式
jsqlparser比较典型的就是访问者模式
... ...
所以,我们可以从很多的优秀源代码中看到设计模式的影子,帮助我们去理解和去感知模式在不同的场景中的应用。
扩展性思考
当然,在这个阶段,我还带着很多“为什么”去理解。比如:
单例模式更多实现的尝试
当然,在这个阶JDK1.5 引入开始包含了并发包,我们也可以尝试用Lock,比如:
代码语言:javascript复制
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import javax.annotation.concurrent.ThreadSafe;
@ThreadSafe
public class DoubleCheckLockSingleton2 {
private static DoubleCheckLockSingleton2 INSTANCE = null;
private static final Lock lock = new ReentrantLock();
private DoubleCheckLockSingleton2() {}
public static DoubleCheckLockSingleton2 getInstance() {
if(INSTANCE == null) {
lock.lock();
try {
if(INSTANCE == null) {
INSTANCE = new DoubleCheckLockSingleton2();
}
}finally {
lock.unlock();
}
}
return INSTANCE;
}
}
是否可以通过CAS来实现呢?此种场景,可能会创建多个实例,但是只有一个实例会返回。
代码语言:javascript复制private static final AtomicReference<SingletonExample> reference = new AtomicReference<>();
public static SingletonExample get() {
if (reference.get() == null)
reference.compareAndSet(null, new SingletonExample());
return reference.get();
}
在早期JDK不支持volatile的时候,ThreadLocal也是解决D.C.L不足的一种方式。
MyBatis的ErrorContext使用ThreadLocal来完成的,其确保了线程中的唯一。
单例如何保证只创建一个对象?
以Java为例,创建对象除了New之外,还可以通过反射Reflection、克隆Clone、序列化/反序列化完成,所以,如果要保证真正只有一个对象,需要规避这些“陷阱”。
... ...
记笔记
好记性不如笔头,看到不一样的内容,可以是对自己知识的补充。比如,我在看Guava源代码的时候,发现了有个Suppliers的memoize方法也是实现单例的,就记录下来。
代码语言:javascript复制package com.wangmengjun.learning.designpattern.singleton;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
public class SingletonBySupplierMemoize {
private static final Supplier<SingletonBySupplierMemoize> INSTANCE = Suppliers.memoize(SingletonBySupplierMemoize::new);
private SingletonBySupplierMemoize() {}
public static SingletonBySupplierMemoize getInstance() {
return INSTANCE.get();
}
}
另外,也可以通过ppt在组内分享或者写Blog/公众号来加强设计模式的记忆。如:
分享一个PPT: 聊聊设计模式
设计模式之创建型模式集合
设计模式之结构模式集合
设计模式之行为模式集合
回过头看,感觉当时写的真的好简单。
体感
这个阶段之后,我对设计模式的感受更深,也对模式真正解决的问题以及应用场景有了更多的了解,也会有更多的思考:比如:
- 简单的单例与静态方法有什么区别、其优势是啥?
- 访问者模式的本质是啥?double-dispatch
... ...
阶段三、实践
模板方法模式
以前遇到过一个场景,我们有不同渠道去扣税,每个渠道的输入报文各不相同,但是,其大致的流程有类似性:
代码语言:javascript复制1、必要的账号有效性检查
2、个别场景特殊性的验证
3、插入扣税记录
4、调用扣税服务(如dubbo)
5、根据不同的调用结果,处理不一样的逻辑
... ... 其他流程,如发送MQ消息
如果每个渠道交易都写一遍,代码会产生冗余,还会产生其他各种不一致的问题。需要做的就是从业务特点出发,做如下几点改造:
- 统一输入参数在流程中的标准化,统一将交易的入参赋值成一个PayDetailRecord对象
- 流程采用统一模版来处理,不同子类主要负责构建PayDetailRecord对象,处理个性化的点
- ... ...
简单的一些伪代码如下:
统一扣税对象
代码语言:javascript复制package com.wangmengjun.learning.designpattern.cases.template.v2;
import java.io.Serializable;
public class PayDetailRecord implements Serializable {
private static final long serialVersionUID = 8693597750287467725L;
... ...
}
模版
代码语言:javascript复制package com.wangmengjun.learning.designpattern.cases.template.v2;
public abstract class AbstractPayTaxTemplate {
public void handle(PayCallback callback, PayContext payContext, PayDetailRecord record) {
//1、必要的账号有效性校验
this.processBasicValidation(payContext, record);
//2、子类个性化校验
this.processCustomizedValidation(payContext, record);
//3、插入扣税记录日志
this.insertPayRecordLog(payContext, record);
//4、调用扣税是服务, 如dubbo服务
int result = this.callPayService(callback, payContext, record);
/**
* 5、根据不同的调用结果,处理不一样的逻辑
*/
if(result == 0) {
//调用结果未知, 插入一个异步任务继续重新去调度调用扣税服务
this.insertAsyncTask(payContext, record);
}else {
//使用callback
callback.updatePayResult(payContext, record, result);
}
... ...
}
//public abstract void doUpdatePayResult( PayContext payContext, PayDetailRecord record, int result);
protected void processBasicValidation( PayContext payContext, PayDetailRecord record) {
System.out.println("AbstractPayTaxTemplate# processBasicValidation ... ... ");
}
protected void processCustomizedValidation( PayContext payContext, PayDetailRecord record) {
//留空,子类根据需要重写即可
//System.out.println("AbstractPayTaxTemplate# processCustomizedValidation ... ... ");
}
protected void insertAsyncTask( PayContext payContext, PayDetailRecord record) {
System.out.println("AbstractPayTaxTemplate# insertAsyncTask ... ... ");
}
protected void insertPayRecordLog( PayContext payContext, PayDetailRecord record) {
System.out.println("AbstractPayTaxTemplate# insertPayRecordLog ... ... ");
}
protected int callPayService(PayCallback callback, PayContext payContext, PayDetailRecord record) {
System.out.println("AbstractPayTaxTemplate# callPayService ... ... ");
return 1;
}
}
回调函数
代码语言:javascript复制package com.wangmengjun.learning.designpattern.cases.template.v2;
public interface PayCallback {
void updatePayResult( PayContext payContext, PayDetailRecord record, int result);
}
简单示例:
代码语言:javascript复制
public class Main {
public static void main(String[] args) {
AbstractPayTaxTemplate template = new Trade001();
template.handle(new PayTaxCallback(), new PayContext(), new PayDetailRecord());
}
}
这种做之后,每个渠道交易只要继承模版类(AbstractPayTaxTemplate),然后重写必要的个性化方法即可,更加易于扩展和维护。这就是一个模板方法模式的思考应用。
再比如:开发部门构建了一个统一的日志中心,为了让各应用有较为统一的日志打印,其提供的LogUtil工具,将不提供info(String info)的方法。另外,多个参数的log方法,将难以满足应用不同场景的日志要求(不同应用对参数打印有不同的要求)。这个时候,统一使用一个标准的LogRecord对象打印会更好,标准的对象有traceId , bizType, elapsedTime等参数。
代码语言:javascript复制public class LogUtils {
... ...
public static void info(LogRecord record) {
... ...
}
... ...
}
这时候,LogRecord的构建可以使用建造者模式。各应用可以灵活设值。
其实,还有很多设计模式的应用场景,这里不再详细展开,等后续其他文章中单独说明。
阶段四、沉淀
形成自己理解的体系
在这个阶段,需要继续沉淀,形成自己理解的体系,比如,对于创建型的设计模式,我的记忆如下:
- 工厂模式:创建什么(WHAT)对象。工厂方法模式采用抽象、多态完成扩展和子类个性化实现。抽象工厂对工厂也进行了抽象,支持多类型不同的对象。
- 建造者模式:怎么(HOW)一步一步创建对象。
- 单例模式:只创建一个对象。如果是线程级的单例,使用ThreadLocal实现。如果要实现集群环境下的唯一,那只能使用分布式锁了。
- 原型模式:使用克隆(Clone)来创建对象。
... ...
不断补充知识
我们可以认为模式是特定问题解决的“套路”。随着业务不断的变化和发展,会有新的一些新的问题需要解决,在典型的GOF23种模式的基础上,也形成了许多其他的模式,作为补充,如:
- NULL Object 模式
- 雇工(Servant)模式
- 规则(Specification)模式
- 访问者模式(无环)
- ... ... 等等
通用方法
比如单例模式是否可以定义一个抽象类来实现,把创建类型交由子类实现,如:
代码语言:javascript复制public abstract class Singleton<T> {
private T mInstance;
protected abstract T create();
public final T get() {
synchronized (this) {
if (mInstance == null) {
mInstance = create();
}
}
return mInstance;
}
}
再比如:观察者模式,能不能用一个抽象类呢? 多个主题topic,可以采用ConcurrentHashMap来存储,等等实现。
... ...
学习设计模式的总结
不知不觉已经写了好多内容。最后,再补充几点在误区和学习上吧。
误区
- 模式有其使用的场景,不是所以场景都要用模式,适合的才是最好的。
- 模式的学习要理解其真正解决的问题,而不是熟记之后就好
- 不要着急往场景中去套模式。这个意思是要先重点梳理清楚业务场景具体在做什么?其在解决的问题是什么?有了较好业务理解和抽象,才能真正去用到某种模式,或者某2种模式的组合。
所以,要先关注业务核心问题,然后,如果问题和模式解决的问题类似,那么,可以使用模式的思想看是否能带来更好的设计。
学习
- 设计模式的学习是一个循序渐进的过程,每次回顾看,因为业务场景接触的增加,都会有不一样的体感
- 保持好奇心,多和别人一起交流。大家不同的点,可能是对自己知识的有效补充
- 有时候可以翻翻优秀源代码,可以增加自己对设计模式的理解和模式在场景中的应用,遇到好的模式应用,要记录下来。好记性不如烂笔头。
其实,当你真正对面向对象特性(封装、继承、抽象、多态)、对设计原则以及业务场景其在解决的问题有足够理解的时候,你可能已经不知不觉在项目中就使用设计模式。
暂时就写这么多吧,有兴趣的读者可以留言交流。