如何学好设计模式?你值得拥有

2022-11-21 20:30:44 浏览数 (1)

前段时间和一个好哥们聚餐,他提到了我好久前准备的设计模式札记,问:写得怎么样了?答曰:大概有一半多点。因为项目跟进,已经很长时间基本没有更新。哥们笑着说:那你要继续哈哈。

谈起设计模式,其实有好多可以讲的。比如:之前面试过程中,对较多候选人问过一些设计模式的问题。有很多同学貌似学习设计模式有一个误区:就是努力地去记住模式长啥样?但是却忽视了模式其所解决的问题是什么。

本文主要是对如何学好设计模式做一个简单的阐述,也算是一个设计模式爱好者对自己学习设计模式的学习回顾、心得分享吧。主要包括如下几个部分:

  • 学习设计模式的“门槛“ -- 简述学习设计模式之前我们最好具备哪些知识
  • 学习设计模式的不同阶段 -- 简述个人在熟悉、进阶、实践、沉淀四个阶段所做的一些事情
  • 学习设计模式的小结 -- 简述我们学习设计模式的“误区”等

学习设计模式的“门槛”

个人而言,学习设计模式之前最好已经有一定的知识储备,主要体现在如下三个方面:

  • 具备一定的面向对象的知识
  • 懂点统一建模语言(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种模式的组合。

所以,要先关注业务核心问题,然后,如果问题和模式解决的问题类似,那么,可以使用模式的思想看是否能带来更好的设计。

学习

  • 设计模式的学习是一个循序渐进的过程,每次回顾看,因为业务场景接触的增加,都会有不一样的体感
  • 保持好奇心,多和别人一起交流。大家不同的点,可能是对自己知识的有效补充
  • 有时候可以翻翻优秀源代码,可以增加自己对设计模式的理解和模式在场景中的应用,遇到好的模式应用,要记录下来。好记性不如烂笔头。

其实,当你真正对面向对象特性(封装、继承、抽象、多态)、对设计原则以及业务场景其在解决的问题有足够理解的时候,你可能已经不知不觉在项目中就使用设计模式。

暂时就写这么多吧,有兴趣的读者可以留言交流。

0 人点赞