设计模式实践(一)-框架源码中的常见设计模式

前言

设计面向对象比较困难,而设计可复用的面向对象就更加困难!
设计模式的不是为了提高代码的运行效率,而是提高开发效率!
设计模式使代码编写真正工程化,是软件工程的的基石脉络!
设计模式是软件开发的葵花宝典,天下武功,无坚不摧,唯快不破!

一、装饰器模式

通过组合的方式动态地给一个对象添加一些额外的职责或者行为

  • 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责,很契合设计模式原则之一开闭原则

  • 当对象族实现复杂度已经无法用继承来实现(eg:可能有大量独立的扩展,

  • 装饰类增加了系统的扩展性,对单元测试友好

为支持每一种组合将产生大量的子类,使得子类数目呈现组合叠加爆炸性增长)

  • 源码套路
X x=new X1(new X2(new X3()))......

UML类图

image

一层一层嵌套,"装饰器类" 持有目标对象的引用,具体方法执行委托给具体的目标对象子类形成了一连串"装饰器链",不断地增强功能

应用场景1

mybatis 二级缓存
  • 抽象组件实现类 (cache.impl包下)


    WechatIMG3.jpeg
  • 装饰器族(cache.decorators包下)

WechatIMG4.jpeg
  • build模式构建二级缓存
 Cache cache = new CacheBuilder(currentNamespace)
        .implementation(valueOrDefault(typeClass, PerpetualCache.class))
        .addDecorator(valueOrDefault(evictionClass, LruCache.class))
        .clearInterval(flushInterval)
        .size(size)
        .readWrite(readWrite)
        .blocking(blocking)
        .properties(props)
        .build();

  • 装饰器入口
  if (PerpetualCache.class.equals(cache.getClass())) {
      for (Class<? extends Cache> decorator : decorators) {
        cache = newCacheDecoratorInstance(decorator, cache);
        setCacheProperties(cache);
      }
      cache = setStandardDecorators(cache);
    } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
      cache = new LoggingCache(cache);
    }
 
  try {
      MetaObject metaCache = SystemMetaObject.forObject(cache);
      if (size != null && metaCache.hasSetter("size")) {
        metaCache.setValue("size", size);
      }
      if (clearInterval != null) {
        cache = new ScheduledCache(cache);
        ((ScheduledCache) cache).setClearInterval(clearInterval);
      }
      if (readWrite) {
        cache = new SerializedCache(cache);
      }
      cache = new LoggingCache(cache);
      cache = new SynchronizedCache(cache);
      if (blocking) {
        cache = new BlockingCache(cache);
      }
      return cache;
    } catch (Exception e) {
      throw new CacheException("Error building standard cache decorators.  Cause: " + e, e);
    }
四不四很符合X x=new X1(new X2(new X3()))......[呲牙]
  • 以FifoCache分析一下装饰器的使用
 public class FifoCache implements Cache {
  private final Cache delegate;
  public FifoCache(Cache delegate) {
    this.delegate = delegate;
    this.keyList = new LinkedList<Object>();
    this.size = 1024;
  }
//put get实际上都委托给了PerpetualCache来实现
  public void putObject(Object key, Object value) {
    cycleKeyList(key);
    delegate.putObject(key, value);//增强了回收策略
  }
  public Object getObject(Object key) {
    return delegate.getObject(key);
  }
//增强的方法,先回收最先进入的换成对象
 private void cycleKeyList(Object key) {
    keyList.addLast(key);
    if (keyList.size() > size) {
      Object oldestKey = keyList.removeFirst();
      delegate.removeObject(oldestKey);
    }
  }

Cache的设计上目标类和装饰器类实现了Cache接口,装饰器一个一个装饰器类串起来,层层包装目标类形成一个链

增强的功能一览

  • ScheduledCache:根据时间间隔清理缓存数据
  • LoggingCache:缓存命中率打印(debug)
  • LruCache:最近最少使用原则
  • SoftCache:jdk软引用策略,jvm内存不足时回收缓存对象
  • 等等.....

应用场景2

JDK IO流
FileInputStream in=new FileInputStream(new File ("hello.txt"));

BufferedInputStream inBuffered=new BufferedInputStream (in);

BufferedInputStream是一层装饰,增强了“缓冲区”的功能....

等等.....


二、模板设计模式

定义一个操作中算法的骨架或流程,充分利用"多态"使得子类可以不改变算法的结构即可重新定义实现

适用场景

完成一件事情,有固定的流程步骤比如说 1->2->3->4,但是每个步骤根据子类对象的不同,而实现细节不同,就可以在父类中定义不变的方法,把可变的方法通过子类回调来实现

关键字: 回调

UML类图

image
  • spring/mybatis/dubbo出现频率较高,一般表现为抽象类+protected+abstract方法组合
  • 代码套路
 public abstract class Abstractxxxxxx{
      
    public  void method{
         //校验逻辑
         //参数装配
//业务逻辑1、2、3
         doMethod();
         //异常处理
         //资源清理
    }
    //抽象方法由子类实现,父类回调...
    protected abstract void doMethod() ;
}

应用场景1

mybatis executor执行器

20151220224146815.jpg
  • BaseExecutor 模板抽象父类
  • SimpleExecutor 简单执行器类,常规的CURD
  • ReuseExecutor 复用Statement执行器
  • BatchExecutor 批量update执行器类
  • 抽象类BaseExecutor 查询模板方法
  
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    一级二级缓存处理逻辑
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      clearLocalCache();
    }
    List<E> list;
      queryStack++;
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
      if (list != null) {
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
        //真正的查询db入口
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
      }
   todo 一些清理方法,缓存,事务,连接关闭等等
   查询db方法入口
 private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
      todo一堆前置方法 校验,缓存(不可变方法)
      doQuery是抽象方法,可变方法交给具体的子类去实现
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
      todo 一堆后置方法....(不可变方法)

//protected abstract 暗示着需要子类去实现
 protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)
     
update/delete/insert套路同上
  • 实现类SimpleExecutor,真正的执行器
public class SimpleExecutor extends BaseExecutor { 
  //真正查询db的入口实现
  public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
      Configuration configuration = ms.getConfiguration();
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      stmt = prepareStatement(handler, ms.getStatementLog());
      return handler.<E>query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }

方法调用,根据ExecutorType类型不同选择不同的实现类

 sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
 sqlSession = sqlSessionFactory.openSession(ExecutorType.SIMPLE);

Executor选择器入口

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
     //根据参数选择执行器实现 -> 策略模式
    if (ExecutorType.BATCH == executorType) {
      executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
      executor = new ReuseExecutor(this, transaction);
    } else {
      executor = new SimpleExecutor(this, transaction);
    }
    if (cacheEnabled) {
      //上文装饰器的包装入口 -> 装饰器模式
      executor = new CachingExecutor(executor);
    }
   //插件拦截器链的入口-> 责任链模式
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

应用场景2

消息中间件 消费端-consumer处理消息
  • 消费端子类实现类消费逻辑
 private class MyHandle extends AbstractHandle {
        //子类方法实现父类,在抽象父类模板方法回调之!
        public void doHandle(String data) {
            //消费解析消息 todo...
        }
    }
  • consumer抽象类模板方法
public abstract class AbstractHandle implements IHandle{
    public void handle(message msg)  {
        //todo 前置处理省略
            try {
                //抽象方法由子类实现后,回调...
                doHandle(data);
            } catch (  Exception ex) {
               //..todo
            }
        // 后置业务逻辑 todo 清理,日志,异常捕获等等
    }

三、策略模式

动态的改变对象的行为,实现某一个功能有多种算法或者策略,多种不同解决方案动态切换,起到改变对象行为的效果,一般会结合模板方法模式配合使用

  • UML类图


应用举例1

jdk线程池拒绝策略

线程池构造方法,其中RejectedExecutionHandler是拒绝策略的接口

 public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) 

策略接口

public interface RejectedExecutionHandler {
    void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}

策略实现类



策略实现之一:线程池满了,主线程执行

 public static class CallerRunsPolicy implements RejectedExecutionHandler {
        public CallerRunsPolicy() { }
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                r.run();
            }
        }
    }

要实现自己的拒绝策略只需要实现拒绝接口即可

  • netty线程池拒绝策略
package org.jboss.netty.handler.execution;
private static final class NewThreadRunsPolicy implements RejectedExecutionHandler {
        NewThreadRunsPolicy() {
            super();
        }

        public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
            try {
                final Thread t = new Thread(r, "Temporary task executor");
                t.start();
            } catch (Throwable e) { }
        }
    }
  • dubbo线程模型线程池拒绝策略
package com.alibaba.dubbo.common.threadpool.support;
public class AbortPolicyWithReport extends ThreadPoolExecutor.AbortPolicy {
    
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        String msg = String.format("Thread pool is EXHAUSTED!");
        logger.warn(msg);
        throw new RejectedExecutionException(msg);
    }
}

应用举例2

消息中间件的消费模型
  • 客户端根据实际情况选择消费策略算法
    客户端依赖抽象类,实现类依赖的抽象,一定程度上契合了依赖倒转原则客户端和实现类通过抽象类关联在一起。
 AbstractHandle handle = new MyHandle();
 handle.handle()

并行和串行子类实现handle()策略方法,从而对消息有不同的消费策略
增强其他的消费策略,实现抽象方法即可,开闭自如


四、外观模式

定义了一个高层接口,为一堆子系统接口提供一个一致的entry入口,供客户端调用

引入外观角色之后,用户只需要直接与外观角色交互,用户与子系统之间的复杂关系由外观角色来实现,从而降低了系统的耦合度。



可以看到经过外观模式改造后的子系统对于客户端来说,封装了内部负载的实现逻辑,对客户端友好

  • UML类图


  • 子系统:职责单一,易于维护
  • 门面:充当了客户类与子系统类之间的“第三者”,对外隐藏了很多细节,对客户的来说"最少知道",比较契合迪米特法则。

应用举例

SLF4J

SLF4J = Simple Logging Facade for Java

  • slf4j相当于一层门面facade,日志框架的抽象,提供了对日志子系统(log4j,logback,jdk-logging)的入口

工厂模式获取当前类的日志对象,对于具体的实现,客户端是不知道的,实际使用是绑定具体实现类。
获取日志子系统实现类通过类加载机制,按照加载顺序,第一个被加载到的子日志系统就绑定到slf4j。
子类加载和寻通过maven pom.xml配置

private final Logger logger = LoggerFactory.getLogger(this.getClass());


如图所示,门面slf4j-api
log4j:根据桥接slf4j-log4j包适配log4j ,输出由最终log4j.jar实现类执行。
logback:直接实现了slf4j的接口,不需要桥接包的适配过程
子系统通过类加载机制来绑定具体的日志子系统,"先到先得"

/*
很重要,类加载器加载路径,slf4j日志子系统都要有同名的包类
根据类加载器双亲委托机制,第一个加载到的日志子系统就优先使用
*/
private static String STATIC_LOGGER_BINDER_PATH = "org/slf4j/impl/StaticLoggerBinder.class";

  private static Set<URL> findPossibleStaticLoggerBinderPathSet() {
   
    Set<URL> staticLoggerBinderPathSet = new LinkedHashSet<URL>();
    try {
    //获取类加载器
      ClassLoader loggerFactoryClassLoader = LoggerFactory.class.getClassLoader();
      Enumeration<URL> paths;
      if (loggerFactoryClassLoader == null) {
        paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH);
      } else {
        paths = loggerFactoryClassLoader.getResources(STATIC_LOGGER_BINDER_PATH);
      }
     
  • logback.jar包同名类



观察者-事件监听模式

察者模式定义了对象之间的一对多的依赖关系,这样,当"一"的一方状态发生变化时,它所依赖的"多"的一方都会收到通知并触发相关操作

  • 关系图谱


    image.png
  • UML类图


  • event-object:事件状态对象,作为监听器的参数参与事件交互
  • event0source:具体的事件源,被监听的对象
  • event listener:事件监听器,由事件源出发监听事件,引起响应变化。

事件监听把事件源和监听器的的依赖关系彻底解耦
公众号订阅,订阅报纸期刊,数据库触发器等等都有观察者模式的"味道"
基于事件驱动机制的系统或语言,比如 js、netty,kafka等亦如此。
java-swing中的应用比较多 button点击事件 ....

应用场景1

cuodao-kafka-consumer事件监听
  • kafka循环pull数据 即为"事件源",该事件源是一个线程
public class PullTask extends Thread {
  • kafka-consumer状态变化 包装为"事件"
/**
 * 消费端工作者状态发生变化的事件
 * @author cuodao
 */
public class TaskStatusEvent extends EventObject {
    private static final long serialVersionUID = 1298884648955658019L;

    private final TaskStatus oldStatus;

    private final TaskStatus newStatus;
  • TaskStatus 事件添加对应监听器加以侦听其状态变化
   PullTask() {
            this.setDaemon(true);
            //添加监听器
            this.addListener(myListener());
        }
  • 事件状态变化后引起的事件回调,修改consumer的全局状态
        private TaskStatusListener myListener() {
            return new TaskStatusListener() {
                @Override
                public void statusListener(TaskStatusEvent e) {
                 
                    }

六、适配器模式

将一个类的接口转换成客户希望的另外一个接口

  • UMl类图(类适配器)


  • adpter也可以是抽象为接口,适配由子类实现
  • 对象适配器类图类似,adpter和adptee关系由继承->关联,也就是委托
    业务代码中,面向适配器开发可以拓展新功能,这里适配器模式也有一点装饰器的意思,只不过"动机"不同!一个是单纯的增强,一个是补偿

应用场景

mybatis 日志框架适配
  • target顶级日志接口
package org.apache.ibatis.logging;

/**
 * @author Clinton Begin
 */
public interface Log {

  boolean isDebugEnabled();
  void error(String s, Throwable e);
  debug
  tarce
  warn
  .....
}
  • log4j适配器类,采用了对象适配-委托给"org.apache.log4j.Logger"
package org.apache.ibatis.logging.log4j;
import org.apache.ibatis.logging.Log;
import org.apache.log4j.Logger;

/**
 * @author Eduardo Macarron
 */
public class Log4jImpl implements Log {
  private Logger log;
  public void error(String s, Throwable e) {
    log.log(FQCN, Level.ERROR, s, e);
  }
 

其他日志适配器类也一样,继承"org.apache.ibatis.logging.Log"类,里面持有对第三方日志框架的日志记录类的引用

  • 客户端即加载 LogFactory类的时候,首先会去根据配置文件动态确定使用哪个第三方日志框架
 <settings>
        <setting name="logImpl" value="LOG4J" />
    </settings>

解析xml配置文件的构造器代码截取

  Class<? extends Log> logImpl = (Class<? extends Log>)resolveClass(props.getProperty("logImpl"));
  configuration.setLogImpl(logImpl);

logfactory日志工厂构造获取具体的日志实现类


public final class LogFactory 
  public static final String MARKER = "MYBATIS";

  private static Constructor<? extends Log> logConstructor;

  static {
    tryImplementation(new Runnable() {
      @Override
      public void run() {
        useSlf4jLogging();
      }
    });
    tryImplementation(new Runnable() {
      @Override
      public void run() {
        useCommonsLogging();
      }
    });
    
    tryImplementation(new Runnable() {
      @Override
      public void run() {
        useLog4JLogging();
      }
    });
  .....等等
 
private static void setImplementation(Class<? extends Log> implClass) {
    try {
      Constructor<? extends Log> candidate = implClass.getConstructor(String.class);
      Log log = candidate.newInstance(LogFactory.class.getName());
     
      }
      logConstructor = candidate;
    } catch (Throwable t) {
    
    }
  }

mybatis没有自己的日志系统,依赖于第三方实现,通过配置文件参数根据logfactory来适配对应的第三方日志系统(log4j,jdk-log,commonslogging)过程略,可参照slf4j日志适配模式


代理模式

为其他对象提供一种代理以控制对这个对象的访问,可能是框架源码使用频率最高的设计模式!

  • 应用场景:rpc框架客户端(httpinvoker/hessian/rmi),Aop,拦截器,mybatis动态生成mapper接口实现类,事务、连接池框架

  • 静态代理

  • 动态代理(通过对象反射动态生成代理类 jdk/cglib/asm/javassist)

实际应用以动态代理居多!动态代理本质上也是静态代理,只不过是以字节码增强,类加载器根据目标target类二进制文件,动态生成其代理代码,并载入jvm方法区,类名多以xxx$xxxx.class

  • UML类图


  • 协调调用者和被调用者,在一定程度上降低了系统的耦合度

应用场景1

dubbo-consumer
  • 解析dubbo自定义标签
<dubbo:reference id="remoteService"interface="com.xxx.remoteService"/>
public class DubboNamespaceHandler extends NamespaceHandlerSupport {

    public void init() {
        //服务消费者标签bean
        registerBeanDefinitionParser("reference", new DubboBeanDefinitionParser(ReferenceBean.class, false));
        //服务提供者标签bean
        registerBeanDefinitionParser("service", new DubboBeanDefinitionParser(ServiceBean.class, true));
      
    }
/**
 * ReferenceFactoryBean
 * 
 * @author william.liangf  duubo作者 梁飞
 * @export
   解析dubbo自定义标签 实现了InitializingBean
   初始化标签信息的时候,向注册中心订阅服务,并生成consumer接口的动态代理类
 */
public class ReferenceBean<T> extends ReferenceConfig<T> implements  InitializingBean,x,x,x {
  public void afterPropertiesSet() throws Exception {
        //todo....... 
     createProxy()       
 }
private T createProxy(Map<String, String> map) {
        //todo.....
     return (T) proxyFactory.getProxy(invoker);  
 }
  • dubbo动态代理工厂基于SPI思想,默认由"javassist"实现,
    关于 SPI插件机制我有空再给大家做个技术分享专题
/**
 * ProxyFactory. (API/SPI, Singleton, ThreadSafe)
 * 
 * @author william.liangf
 */
@SPI("javassist")
public interface ProxyFactory {
  • 生成动态代理的实现类
    1、jdk动态代理
    2、javassist



    消费端远程调用dubbo接口,客户端执行类实际上是其代理类。
    dubbo 为这一类公共rpc接口客户端生成动态代理,契合面向切面编程的思想

基于netty的rpc框架实现是国庆节的作业,大家应该都比较熟悉了,dubbo的consumer类似,这里细节不再铺开。


三克油

23种设计模式没有结束,同学们还需要继续努力添砖加瓦!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容