逸言

设计模式的一些变化

| Comments

经历了几次过度设计,我对一些设计模式开始抱有谨慎的戒惧。我希望能够合理地运用设计模式,而非为模式而模式。事实上,现在的我较少会有意识地运用设计模式;更多地是通过职责来驱动,以期获得合理的职责分配。之后,再通过辨别代码的坏味道,运用重构来改善设计。大多数情况下,这种方式对于设计而言,已经足够。不过,有时我们仍有必要根据具体场景,做出合理判断和决策。

我曾经遭遇的一个Story,是要实现一个Web Service,提供update的服务接口。需求要求对服务的Request进行验证。这一验证功能并非Story的核心功能,但其逻辑却比核心逻辑复杂数倍。它需要针对不同情况,遵循多个规则验证Request。这些验证逻辑如此的复杂,以至于我们可以在第一时间做出判断,它必须是单独的职责,绝对不适合放到Service对象中。

这种职责分离还不够。因为每一条验证规则,都可以视为一个单独的验证职责。若将每种验证规则封装为一个验证对象,就能很好地满足SRP。每个验证对象只需要做好自己的事情即可,结果只有两个,要么错误,要么通过。若当验证逻辑为一个整体,则可以理解为:只要违背了一条验证规则,出现错误,则可以视为验证不通过;否则将继续验证下一条规则。

乍一看来,这完全符合职责链模式的要求。我的Pair也完全同意了这一点。当我们按照这一思路进行编码时,我的疑惑随着步伐的前进,变得愈深,好像有什么东西在拽着自己,阻止我们前进。因为我们使用了Spring,故而需要将这个职责的链条通过依赖注入的方式拼接。为了迎合这种要求,标准的职责链模式需要微调。例如,我们写出这样的职责链超类:

public abstract class UpdateRequestValidator {
  private UpdateRequestValidator nextValidator;
  public UpdateRequestValidator(UpdateRequestValidator nextValidaor) {
      this.nextValidator = nextValidator;
  }
  
  public ValidateResult validate(UpdateRequest request) {
      ValidateResult result = doValidate();
      if (result.isError()) {
          return result;
      }
      return nextValidator.validate();
  }
  
  protected abstract ValidateResult doValidate();
}

如果仔细分析这段代码,可以发现我们是在运用装饰器模式的手法来实现职责链。这就让人感到奇怪了。由于我们自定义了构造函数,要求传入一个UpdateRequestValidator,似乎可以避免对nextValidator非空的判断。可是,链条的末尾又该放入哪个对象呢?为此,我们还定义了一个NullValidator,它什么都不做,并被要求放到职责链构造的最末尾。

够奇怪吧。这里我们对Spring的配置做出了假设,一厢情愿地要求NullValidator必须作为职责链的最后一个。可是,我们却未对这种假设做出任何约束。

与其如此别扭,还不如对设计做出调整。只要验证逻辑的职责分离与封装是合理的,我们未必一定要运用职责链模式(或者这种不伦不类的装饰器模式)。抛开这种约束后,每个Validator就可以变得更单纯,无需假设这个链条的组成;而是将这个链式逻辑转交给包含了UpdateRequestValidator列表对象的Validator对象,它可以被看成是Validator的合成对象:

public interface UpdateRequestValidator {
  ValidateResult validate(UpdateRequest request);
}
public class CustomerNotFoundValidator implements UpdateRequestValidator {
  @Override
  public ValidateResult validate(UpdateRequest request) {
      //do something;
  }
}
public class CompositeUpdateRequestValidator implements UpdateRequestValidator {
  private List<UpdateRequestValidator> validators;
  @Required
  public void setValidators(List<UpdateRequestValidator validators) {
      this.validators = validators;
  }
  @Override
  public ValidateResult validate(UpdateRequest request) {
      ValidateResult result;
      for (UpdateRequestValidator validator : validators) {
          result = validator.validate(request);
          if (result.isError()) {
              return result;
          }
      }
      return result;
  }
}

我觉得修改后的设计意图清晰许多了,并且,它对使用者没有了隐含的约束,在Spring的ApplicationContext配置文件中,也能够很好地将我们单独实现的Validator作为List的元素配置给CompositeUpdateRequestValidator。我喜欢这样的调整。

我对装饰器模式持同样态度。正如有人说道:装饰器模式的方式有些像洋葱。洋葱的味道虽好,不过在剥洋葱时,却要小心不要被呛出眼泪。在阅读Axon Framework的源代码时,我发现一种设计方式,可以避免装饰器模式的洋葱构造,却能达到同样的装饰效果。我将其成为Interceptor模式。

需求是希望对CommandHandler对象的命令请求进行包装(或装饰),例如为命令请求提供日志或事务功能。这些功能可以是组合的。传统的方式是运用装饰器模式,例如:

public interface CommandHandler {
    void handle();
}
public class LoggingCommand implements CommandHandler{
    private CommandHandler commandHandler;
    public LoggingCommand(CommandHandler commandHandler) {
        this.commandHandler = commandHandler;
    }

    @Override
    public void handle() {
        logInfo("begin execute command");

        commandHandler.handle();

        logInfo("end execute command");
    }
}
public class TransactionCommand implements CommandHandler {
    private CommandHandler commandHandler;
    public TransactionCommand(CommandHandler commandHandler) {
        this.commandHandler = commandHandler;
    }

    @Override
    public void handle() {
        myTransaction.beginTransaction();
        try {
            commandHandler.handle();
            myTransaction.commitTransaction();
        } catch (Exception ex) {
            myTransaction.rollbackTransaction();
        }
    }
}

假设有一个命令对象PlaceOrderCommandHandler,同时需要日志和事务功能,就需要如下构造方式:

CommandHandler command = new TransactionCommand(new LoggingCommand(new PlaceOrderCommandHandler()));

这种构造方式既复杂,也不容易使用Spring来注入。Axon的设计方式转变了设计视角,将这种对命令的包装,看做是对命令请求的一种拦截,这就好似AOP的pointcut一般。它定义了一个InterceptorChain,负责完成整个拦截器链条的组装,并将这些拦截职责通过Chain完成一层层的传递。设计如下:

public interface CommandHandlerInterceptor {
    void handle(InterceptorChain interceptorChain);
}
public class LoggingInterceptor implements CommandHandlerInterceptor {
    @Override
    public void handle(InterceptorChain interceptorChain) {
        info("LOG: begin");
        interceptorChain.proceed();
        info("LOG: end");
    }
}
public class TransactionInterceptor implements CommandHandlerInterceptor {
    @Override
    public void handle(InterceptorChain interceptorChain) {
        transaction.beginTransaction();
        try {
            interceptorChain.proceed();
            transaction.commitTransaction();
        } catch (Exception ex) {
            transaction.rollbackTransaction();
        }


    }
}
public class InterceptorChain {
    private Iterator<? extends  CommandHandlerInterceptor> interceptorChain;
    private CommandHandler handler;

    public void proceed() {
        if (interceptorChain.hasNext()) {
            interceptorChain.next().handle(this);
        }

        handler.handle();

    }

    @Required
    public void setHandler(CommandHandler handler) {
        this.handler = handler;
    }

    @Required
    public void setInterceptorChain(Iterator<? extends CommandHandlerInterceptor> interceptorChain) {
        this.interceptorChain = interceptorChain;
    }
}

在InterceptorChain类中,handler对象就是我们要包装(或者说拦截)的命令对象。注意,Interceptor对象的handle()方法,是将InterceptorChain对象作为参数传入的。于是就形成了如下的方法调用时序:

只要理解了InterceptorChain起到的委派传递作用,就能很好地理解这个设计。这种方式将之前装饰器模式那种洋葱式的构造转移到列表中,并通过迭代器进行迭代。同时,它将对CommandHandler以及相关拦截器对象的依赖转移到了外面,很好地支持IoC容器对依赖的注入,从而使得代码可以变得更简单,更灵活。

总之,当我们在运用设计模式时,一定要结合当前上下文,对设计模式做出合理的判断,甚至可以大胆地对GOF总结的设计模式进行大刀阔斧地挑战或修改。关键还在于把握设计模式的核心原则,并在“简单设计”的前提下,做出设计的决策。

Comments