前段时间和一个好哥们聚餐,他提到了我好久前准备的设计模式札记,问:写得怎么样了?答曰:大概有一半多点。因为项目跟进,已经很长时间基本没有更新。哥们笑着说:那你要继续哈哈。
谈起设计模式,其实有好多可以讲的。比如:之前面试过程中,对较多候选人问过一些设计模式的问题。有很多同学貌似学习设计模式有一个误区:就是努力地去记住模式长啥样?但是却忽视了模式其所解决的问题是什么。
本文主要是对如何学好设计模式做一个简单的阐述,也算是一个设计模式爱好者对自己学习设计模式的学习回顾、心得分享吧。主要包括如下几个部分:
个人而言,学习设计模式之前最好已经有一定的知识储备,主要体现在如下三个方面:
具备一定的面向对象的知识
这个怎么讲呢,以Java语言为例,如果对其特征( 封装、继承、抽象和多态 )还不清楚,那么,我还是建议先把这些弄清楚之后,才去学习设计模式,毕竟在设计模式的学习中,抽象、多态都是很重要的。
懂点统一建模语言(UML)的知识
设计模式基本上是通过UML的类图和时序图来表示的。以一个观察者模式为例,其类图和时序图如下:
所以,学习设计模式之前,至少对UML的类图和时序图表示有所了解。
比如,类图中类与类之间的关系:
具体UML的知识点这里就不再展开,有兴趣的同学可以去阅读UML相关书籍或者文章。
最好有一定的项目参与经验
具体项目有具体解决的问题,一般项目中都会有较多设计模式应用的影子,如果参与过一定的项目,可以结合问题和代码,对某个设计模式的应用有更好的理解和体感。
PS:如果没有一定的项目经验,也可以通过阅读源代码来看看其中设计模式在哪些场景中做了使用,主要解决了什么问题。源代码可以选择JDK、Spring、Mybatis、Guava等。
接下来,来回顾和分享下我个人学习模式所经历的不同阶段:
记得我第一次接触设计模式是大学《设计模式》的课上,忘记是必修课还是选修课了,当时老师使用的书是下面这本书:
不过我也买了如下这本“砖头书”作为我的辅导书,个人觉得是一本很不错的书。
在这个阶段的学习算是比较中规中矩吧,毕竟有考试的要求。这个阶段,主要学习了设计模式的由来、含义、组成要求、设计原则、设计模式分类以及课堂选讲的差不多有10个设计模式。
设计模式由来
模式最早是应用在建筑学上,然后Erich Gamma、Richard Helm、Ralph Johnson和John Vlissides四人(GOF)将模式的思想引入软件工程学,他们在1994年归纳发表了23种在软件开发中使用频率较高的设计模式,旨在用模式来统一沟通面向对象方法在分析、设计和实现间的鸿沟。
设计模式的含义和组成要素
每一个模式描述了一个在我们周围不断重复发生的问题,以及该问题的解决方案的核心。这样,你就能一次又一次地使用该方案而不必做重复劳动。可以说,模式是前辈们对代码开发经验的总结,是解决特定问题的一系列的“套路“,使用模式可以提高代码的可复用性、可扩展性、可维护性等
一般而言,一个模式由四个要素组成:
设计模式的分类
虽然GOF设计模式只有23个,但是他们各具特色,每个模式都为某一个可重复的设计问题提供了一套解决方案。根据它们的用途,设计模式可分为创建型(Creational)、结构型(Strutural)和行为型(Behavioral)三种,其中:
设计原则
更多的设计模式原则内容可以参考《设计模式几大原则》
问题
在这个阶段,一下子好多设计模式要去接受,其实挺容易遗忘的。可以结合生活的一些示例来加深记忆。
比如:
建造者模式
适配器模式
中介者模式
模版方法模式
享元模式
备忘录模式
... ...
体感
在这个阶段,大致对设计模式有了一个比较宏观的了解。知道很多模式的样子,但是缺少足够的理解和思考。比如:
更多单例模式可阅读《单例模式详解》。
更多建造者模式可阅读《建造者模式浅析》
... ...
至于为什么要这样?用在什么场景中?.... ? 缺少对模式真正在解决的问题的认识。
在这个阶段,我主要是带着更多的问题(为什么要这样做呢?)去巩固和学习,真正去理解和感受设计模式的味道。我主要通过如下几个方面来进行了。
看源码
可以从JDK、Spring、MyBatis、Guava、JUnit等入手去感受其使用到的一些模式,增加我们对设计模式在什么场景使用的认知和体感。
如:
饿汉式单例
JDK源码中的Runtime就是饿汉式单例。
枚举单例
Guava包的MoreExecutors类中,可以看到枚举单例的示例。
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()";
}
}
内部类单例
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,比如:
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来实现呢?此种场景,可能会创建多个实例,但是只有一个实例会返回。
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方法也是实现单例的,就记录下来。
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/公众号来加强设计模式的记忆。如:
回过头看,感觉当时写的真的好简单。
体感
这个阶段之后,我对设计模式的感受更深,也对模式真正解决的问题以及应用场景有了更多的了解,也会有更多的思考:比如:
... ...
模板方法模式
以前遇到过一个场景,我们有不同渠道去扣税,每个渠道的输入报文各不相同,但是,其大致的流程有类似性:
1、必要的账号有效性检查
2、个别场景特殊性的验证
3、插入扣税记录
4、调用扣税服务(如dubbo)
5、根据不同的调用结果,处理不一样的逻辑
... ... 其他流程,如发送MQ消息
如果每个渠道交易都写一遍,代码会产生冗余,还会产生其他各种不一致的问题。需要做的就是从业务特点出发,做如下几点改造:
简单的一些伪代码如下:
统一扣税对象
package com.wangmengjun.learning.designpattern.cases.template.v2;
import java.io.Serializable;
public class PayDetailRecord implements Serializable {
private static final long serialVersionUID = 8693597750287467725L;
... ...
}
模版
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;
}
}
回调函数
package com.wangmengjun.learning.designpattern.cases.template.v2;
public interface PayCallback {
void updatePayResult( PayContext payContext, PayDetailRecord record, int result);
}
简单示例:
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等参数。
public class LogUtils {
... ...
public static void info(LogRecord record) {
... ...
}
... ...
}
这时候,LogRecord的构建可以使用建造者模式。各应用可以灵活设值。
其实,还有很多设计模式的应用场景,这里不再详细展开,等后续其他文章中单独说明。
形成自己理解的体系
在这个阶段,需要继续沉淀,形成自己理解的体系,比如,对于创建型的设计模式,我的记忆如下:
... ...
不断补充知识
我们可以认为模式是特定问题解决的“套路”。随着业务不断的变化和发展,会有新的一些新的问题需要解决,在典型的GOF23种模式的基础上,也形成了许多其他的模式,作为补充,如:
通用方法
比如单例模式是否可以定义一个抽象类来实现,把创建类型交由子类实现,如:
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来存储,等等实现。
... ...
不知不觉已经写了好多内容。最后,再补充几点在误区和学习上吧。
误区
所以,要先关注业务核心问题,然后,如果问题和模式解决的问题类似,那么,可以使用模式的思想看是否能带来更好的设计。
学习
其实,当你真正对面向对象特性(封装、继承、抽象、多态)、对设计原则以及业务场景其在解决的问题有足够理解的时候,你可能已经不知不觉在项目中就使用设计模式。
暂时就写这么多吧,有兴趣的读者可以留言交流。