前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >如何学好设计模式?你值得拥有

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

原创
作者头像
孟君
发布2022-07-22 07:10:14
4310
发布2022-07-22 07:10:14
举报

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

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

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

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

学习设计模式的“门槛”

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

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

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

学习

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

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

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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 学习设计模式的“门槛”
  • 阶段一、熟悉
  • 阶段二、进阶
  • 阶段三、实践
  • 阶段四、沉淀
  • 学习设计模式的总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档