静态代码分析程序中设计模式的应用

219 阅读8分钟

大家好,我是王有志。一个分享硬核 Java 技术的金融摸鱼侠。

前面我通过两篇文章向大家分享了我们最近在做的静态代码分析程序(CA)的定位,能力,使用和设计:

今天呢, 我向各位汇报下在 CA 的设计过程中使用到了哪些设计模式,以及我们是如何运用这些设计模式的。我们在 CA 中主要应用了 3 中设计模式:责任链模式,建造者模式和工厂模式,下面我们聊一下责任链模式和工厂模式的应用。

Tips:CA 项目的地址:gitee.com/wyz-A2/Code…

责任链模式的应用

静态代码分析程序的流程设计》中提到,基于对 CA 项目的需求分析我们在设计主流程时选择了责任链模式,在责任链模式的应用上,我们并没有遵循原教旨主义,让处理节点既承担业务逻辑的功能,又承担链的功能。

我们先来看一下原教旨主义中责任链模式的实现。

定义一个处理节点接口,代码如下:

public interface BaseHandler {
  /**
    * 负责业务逻辑处理
    */
  void handle();

  /**
    * 设置下一个处理器
    * @param handler 处理器
    */
  void setNextHandler(BaseHandler handler);
}

再来实现两个处理节点,首先是项目下载处理节点,代码如下:

public class VcsDownloadHandler implements BaseHandler {

  private BaseHandler nextHandler;

  @Override
  public void handle() {
    // 这里实现业务逻辑处理

    // 这里负责调用下一个处理节点
    if (this.nextHandler != null) {
      this.nextHandler.handle();
    }
  }

  @Override
  public void setNextHandler(BaseHandler handler) {
    this.nextHandler = handler;
  }
}

接着是差异对比处理节点,代码如下:

public class VcsDifferentHandler implements BaseHandler {

  private BaseHandler nextHandler;

  @Override
  public void handle() {
    // 这里实现业务逻辑处理

    // 这里负责调用下一个处理节点
    if (this.nextHandler != null) {
      this.nextHandler.handle();
    }
  }

  @Override
  public void setNextHandler(BaseHandler handler) {
    this.nextHandler = handler;
  }
}

最后我们来使用这个责任链,代码如下:

public static void main(String[] args) {
  // 构建处理节点
  BaseHandler vcsDownloadHandler = new VcsDownloadHandler();
  BaseHandler vcsDifferentHandler = new VcsDifferentHandler();

  // 设置处理节点关联关系
  vcsDownloadHandler.setNextHandler(vcsDifferentHandler);

  // 调用责任链
  vcsDownloadHandler.handle();
}

上面就是一个非常简单的原教旨主义的责任链模式的应用。可以看到每个 BaseHandler 的实现类中,都要持有后继节点的实例,并且在自身实现的BaseHandler#handle方法的最后都会判断该处理节点是否有后继节点,如果有后继节点,则调用后继节点的BaseHandler#handle方法,这就是我前面所说的处理节点既承担业务逻辑的功能,又承担链的功能。

在 CA 的责任链设计中,我们剥离了处理节点的链的功能,即 CA 的处理节点只负责业务逻辑的实现,链的功能是单独实现的,链的代码如下:

public class CodeAnalysisChain {

  /**
    * 开始节点
    */
  private Node first;

  /**
    * 结束节点
    */
  private Node last;

  /**
    * 添加节点
    * @param node 处理节点
    */
  public void addNode(BaseNode node) {
    Node oldLast = this.last;
    Node newLast = new Node(null, node);
    this.last = newLast;
    if (oldLast == null) {
      this.first = newLast;
    } else {
      oldLast.next = newLast;
    }
  }

  /**
    * 处理
    * @param codeAnalysisContext 代码分析上下文
    */
  public void handle(CodeAnalysisContext codeAnalysisContext) {
    if (this.first != null) {
      first.handle(codeAnalysisContext);
    }
  }

  /**
    * 内部节点
    */ 
  private static class Node {

    private Node next;

    private final BaseNode node;

    public Node(Node next, BaseNode node) {
      this.next = next;
      this.node = node;
    }

    public void handle(CodeAnalysisContext codeAnalysisContext) {
      if (codeAnalysisContext.couldExecute()) {
        this.node.handle(codeAnalysisContext);
        if (this.next != null) {
          this.next.handle(codeAnalysisContext);
        }
      }
    }
  }
}

CA 中责任链的使用方式如下:

// 构建责任链节点
VcsDownloadNode vcsDownloadNode = new VcsDownloadNode();
VcsDifferentNode vcsDifferentNode = new VcsDifferentNode();

// 构建责任链
CodeAnalysisChain codeAnalysisChain = CodeAnalysisChain.builder()
.addNode(vcsDownloadNode)
.addNode(vcsDifferentNode)
.build();

// 调用责任链
codeAnalysisChain.handle(context);

这样做的原因是保证了处理节点的“纯粹性”,即处理节点只负责业务逻辑处理,而关于链的构建和调用则交由 CodeAnalysisChain 实现,另外,这样做我们可以在 CodeAnalysisChain 中统一责任链的状态控制,根据上一个处理节点的处理结果来决定责任链是否需要继续执行,而不是在每个处理节点中都额外的实现状态控制,代码可以参考CodeAnalysisChain$Node#handle方法(当然了,我们还没有完成状态控制的部分)。

Tips:上面所展示的 CodeAnalysisChain 和源码中的有所差别,这里我删除了 CodeAnalysisChain 的建造者。

工厂模式的应用

还记得我在《我们做了一款静态代码分析程序》中提到的关于 CA 的设想吗?CA 应该要能够支持多种版本控制软件,多种编程语言,多种项目构建方式。

在这种设想下,CA 需要根据项目的配置动态的组装不同版本控制软件,编程语言以及项目构建方式的处理逻辑。举个例子,假如说我们有两个项目:

  • 项目 A:通过 Git 进行版本控制,使用 Maven 构建的 Java 项目
  • 项目 B:通过 SVN 进行版本控制,使用 Gradle 构建的 Java 项目

对于这样的两个项目,CA 在主流程的逻辑处理上是一致的,如下:

差别体现在上图中虚线的处理节点中,针对于项目下载节点,差异对比节点和分支切换节点,项目 A 需要使用 Git 的指令来完成,项目 B 需要使用 SVN 的指令来完成,针对于项目编译节点,项目 A 需要使用 Maven 进行处理,项目 B 需要使用 Gradle 进行处理。

因此在 CA 的实现中,我们在每个节点中引入了处理器,不同的处理器负责处理不同类型的版本控制软件,编译工具和编程语言,而 CA 主流程中的处理节点只负责调用这些处理器相应的方法,例如,我们在处理项目下载的过程中,引入了版本控制处理器 VcsProcessor,定义如下:

public abstract class VcsProcessor extends BaseProcessor {

  protected final VcsConfig vcsConfig;

  protected VcsProcessor(VcsConfig vcsConfig) {
    this.vcsConfig = vcsConfig;
  }

  /**
   * 下载项目
   *
   * @param projectLocalPath 本地路径
   */
  public abstract void download(String projectLocalPath);

  /**
   * 切换分支
   *
   * @param branchName 分支名称
   */
  public abstract void checkout(String branchName);


  /**
   * 比较分支差异
   */
  public abstract VcsDifferent compare();
}

CA 中需要针对于不同的版本控制软件实现不同的版本控制软件处理器,目前对于 VcsProcessor 来说,CA 中只有一个实现类 GitProcessor,负责实现使用 Git 作为版本控制软件的项目的下载,分支切换和分支比较。

Tips:目前的 CA 中,大部分处理器只有一个实现类~~

引入了处理器后,不同的处理节点只需要负责根据配置的不同创建不同的处理器,并调用处理器的相应方法即可,这里来看下 VcsDownloadNode#handle 方法的实现,代码如下:

public void handle(CodeAnalysisContext context) {
  VcsConfig vcsConfig = context.getVcsConfig();
  VcsMapper vcsMapper = vcsConfig.getVcsMapper();
  
  // 构建 VCS 处理器
  VcsProcessor vcsProcessor = ProcessorBuilder.getVcsProcessor(context, vcsMapper, vcsConfig);
  
  // 下载项目到本地
  ProjectConfig projectConfig = context.getProjectConfig();
  vcsProcessor.download(projectConfig.getProjectLocalPath());
}

为了屏蔽不版本控制软件处理器的差异,我们又引入了 ProcessorBuilder,用于创建不同的处理器。在VcsDownloadNode#handle方法的实现中,首先调用ProcessorBuilder#getVcsProcessor方法创建了 VcsProcessor 实例,然后通过调用VcsProcessor#download方法实现项目下载的功能。

我们再具体的看下ProcessorBuilder#getVcsProcessor方法的实现,代码如下:

public class ProcessorBuilder {

    /**
   * 获取 VCS 处理器
   *
   * @param context   代码分析上下文
   * @param vcsMapper VCS 映射器
   * @param params    构造器参数
   * @return VCS 处理器
   */
    public static VcsProcessor getVcsProcessor(CodeAnalysisContext context, VcsMapper vcsMapper, Object... params) {
        String projectName = context.getProjectConfig().getProjectName();
        Class<? extends BaseProcessor> processorClass = vcsMapper.getVcsProcessor();
        return (VcsProcessor) getProcessor(projectName, processorClass, params);
    }

    private static BaseProcessor getProcessor(String projectName, Class<? extends BaseProcessor> processorClass, Object... params) {
        String key = key(projectName, processorClass);

        BaseProcessor baseProcessor = PROCESSOR_MAP.get(key);
        if (baseProcessor == null) {
            baseProcessor = buildProcessor(processorClass, params);
            PROCESSOR_MAP.put(key, baseProcessor);
        }
        return baseProcessor;
    }

    private static BaseProcessor buildProcessor(Class<? extends BaseProcessor> processorClass, Object... params) {
        try {
            Constructor<? extends BaseProcessor> constructor;
            if (params.length == 0) {
                constructor = processorClass.getDeclaredConstructor();
            } else {
                Class<?>[] parameterTypes = new Class<?>[params.length];
                for (int i = 0; i < params.length; i++) {
                    Object param = params[i];
                    parameterTypes[i] = param.getClass();
                }
                constructor = processorClass.getDeclaredConstructor(parameterTypes);
            }
            constructor.setAccessible(true);
            return constructor.newInstance(params);
        } catch (NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException e) {
            throw new RuntimeException("");
        }
    }
}

可以看到,我们在获取 VcsProcessor 的方法中,使用了一个映射器 VcsMapper 枚举类,它定义了不同版本控制软件与 VcsProcessor 的映射关系,代码如下:

public enum VcsMapper {

    GIT("Git", GitProcessor.class, UnixDifferentProcessor.class);

    private final String vcsType;

    private final Class<? extends VcsProcessor> vcsProcessor;

    private final Class<? extends VcsDifferentProcessor> vcsDifferentProcessor;

    VcsMapper(String vcsType, Class<? extends VcsProcessor> vcsProcessor, Class<? extends VcsDifferentProcessor> vcsDifferentProcessor) {
        this.vcsType = vcsType;
        this.vcsProcessor = vcsProcessor;
        this.vcsDifferentProcessor = vcsDifferentProcessor;
    }
}

上述的代码中,ProcessorBuilder 是工厂模式中的具体工厂,提供了获取和创建 VcsProcessor 实例的工厂方法ProcessorBuilder#getVcsProcessor,该方法根据版本控制软件的映射器 VcsMapper 枚举类来获取具体的 VcsProcessor 实现类,再通过反射的方式创建了 VcsProcessor 实例。

对于 VcsDownloadNode 来说,ProcessorBuilder 封装了 VcsProcessor 的创建过程,VcsDownloadNode 不需要知道创建的 VcsProcessor 的具体实现类,只需要根据自身虚要处理的逻辑调用 VcsProcessor 对应的方法即可;另外,我们在 CA 中也引入了VcsProcessor 的映射器 VcsMapper 枚举类,通过版本控制软件的类型映射到具体的 VcsProcessor 实现类上。

在 CA 需要引入其它的版本控制软件时,只需要在 CA 中实现它对应的处理器,并在映射器 VcsMapper 枚举类中添加映射关系即可,不需要对 CA 的主流程 VcsDownloadNode 做出修改,也不要需要对处理器工厂 ProcessorBuilder 做出修改。

这样,我们就可以把引入版本控制软件的复杂性控制在映射器 VcsMapper 枚举类和版本控制软件处理器 VcsProcessor 的实现中,而不会扩散到 CA 的主流程 VcsDownloadNode 和处理器工厂 ProcessorBuilder 里。

Tips:在 CA 的实现中 ProcessorBuilder 是复杂工厂,除了提供创建和获取版本控制软件处理器 VcsProcessor 的工厂方法外,还提供了版本控制软件差异解析处理器 VcsDifferentProcessor,语言文件差异处理器 LanguageFileProcessor,构建处理器 BuildProcessor 和语言文件解析处理器 LanguageParseProcessor 的工厂方法。

Tips:关于映射器 VcsMapper 枚举类,也可以选择通过数据库配置的形式实现~~

好了今天的内容就到这里了,下一篇文章中,我会和大家分享 CA 中使用的具体技术。我是分享硬核 Java 技术的金融摸鱼侠王有志,我们下回见!


尾图(无二维码).png