设计模式-Command

249 阅读4分钟

行为型模式

问题

在一些场景下,我们需要把执行的方法、函数(function)保存下来,比如在调用外部系统接口的时候,希望能够延时执行或者重试。

在这种情况下,就需要使用到“命令模式”,把方法封装成一个对象,从而可以把函数当做对象来传递和使用。

命令模式用的最核心的实现手段,是将函数封装成对象。

组成

“命令模式”的组成 UML 如下所示:

图 1. 命令模式的组成

图 1 中各个成员介绍:

  • Command:命令对象的通用接口或抽象类
  • ConcreteCommand:具体的命令对象,实现 Command 的 execute 方法,持有一个 Receiver 对象
  • Receiver:知道如何实施和执行一个请求相关的操作,任何类都可以成为一个 Receiver
  • Client:创建 Command 对象,并指定他的 Receiver
  • Invoker:要求 Command 执行 execute 方法

Client 创建一个 Command 对象并指定它的 Receiver 对象,某个 Invoker 存储该 Command 对象,并通过调用 Command 的 execute 方法来 执行/提交 一个请求。因为 Invoker 已经存储了 Command 对象,所以 Invoker 可以重试 Command。

在图1 中,Client 和 Invoker 被分开了,两者可以是同一个对象。我们并不关心 Receiver 的信息,只需要知道它能被 Command 接收即可,在需要的时候,通过替换 Receiver 的具体实现,从而实现了“命令和接收者之间的解耦”。

应用场景

因为 Command 模式的主要实现方式是把函数封装成对象,所以它的应用场景(要解决的问题)都是“需要把函数参数化,从而方便管理”的问题,比如延时执行、重复执行、撤销、记录执行记录等等:

  • 需要在不同的时刻,指定、排列或执行请求,一个 Command 对象拥有与初始请求无关的生命周期,如果 Receiver 也与 Client 无关,那么可以在需要的任意时刻,调用 Command 对象的 execute 方法。
  • 支持取消操作,Invoker 可以在执行 Command 的 execute 方法前记录下 Receiver 的状态,并由 Command 提供 unexecute 方法,在 Receiver 上撤销 execute 方法的影响。
  • 组合多个操作为一个更高阶的操作,比如一个事务就封装了对数据的一组变动,command 自身可以作为一个事务,包含更多的操作,或者多个 command 作为事务内的操作。

示例代码

在工作中,我们的服务需要调用外部服务和接收外部服务的回调,常规实现方式是,定义一个 client,提供一个外部调用的方法,方法接收参数,在方法中使用 restTemplate 或者 httpClient 执行请求:

public Response<Result> batchInsert(BatchInsertForm batchInsertForm) {
    // 接口地址
    String url = generateUrl(getBatchUrl());
    // 封装请求
    HttpEntity<BatchInsertForm> entity = new HttpEntity<>(batchInsertForm, headers());
    // 执行
    String res = doRequest(url, entity);
    // 解析出结果
    return JsonUtils.read(res, Response.class, Result.class);
}

这种实现方式存在的一些问题如下:

  • 如果调用失败,怎么重试这个请求?
  • 如果调用失败,怎么查看请求 body 和响应的异常?打印日志?
  • 如果要统计该接口的成功率,怎么记录?

这些都需要把执行该请求的方法参数化,把每次请求(参数、结果等)都记录下来,这就需要使用到 Command 模式。

为此,我们组的实现方式是,定义一个 OutgoingRequest对象用于封装对外部系统发起的请求,定义一个 IncomingRequest对象用于封装外部系统的回调,并分别定义一个 RequestExecutor来执行 request 对象。

OutgoingRequest类的简化代码如下:

public abstract class OutgoingRequest {

    private final RequestId requestId;
    private ExecuteStatus executeStatus;
    private Integer executeCount;
    private final String url;
    private final Map<String, String> headers;
    private final Map<String, String> params;
    private String body;
    private final ZonedDateTime createdAt;
    private ZonedDateTime updatedAt;

    protected Object execute(OkHttpClient okHttpClient) {
        if (this.executeStatus != ExecuteStatus.PROCESSING) {
            throw new IllegalStateException();
        }
        touch();
        try {
            this.executeCount++;
            Request request = buildRequest();
            
            log.info("outgoingRequest execute params: {}, body:{}, headers:{}",
                    JsonUtils.write(params), JsonUtils.write(body), JsonUtils.write(headers));
            try (Response response = okHttpClient.newCall(request).execute()) {
                if (response.isSuccessful()) {
                    Object result = onSuccess(response);
                    this.executeStatus = ExecuteStatus.SUCCESS;
                    return result;
                } else {
                    this.executeStatus = ExecuteStatus.EXECUTION_FAILED;
                    throw new HttpExecutionException(response.code(), response.body() != null ? response.body().string() : null);
                }
            }
        } catch (Exception e) {
            log.error("execute request error, requestId:{}", requestId.getId(), e);
            this.executeStatus = ExecuteStatus.EXECUTION_FAILED;
            RuntimeException wrapException = onException(e);
            if (null != wrapException) {
                throw wrapException;
            } else {
                throw new RuntimeException(e);
            }
        }
    }

    protected abstract Object onSuccess(Response response);

    protected abstract RuntimeException onException(Exception e);
}

OutgoingRequestExecutor类的简化代码如下:

public class OutgoingRequestExecutor {

    private final OutgoingRequestRepository outgoingRequestRepository;
    private final OkHttpClient okHttpClient;

    public OutgoingRequestExecutor(
            OkHttpClient okHttpClient,
            OutgoingRequestRepository outgoingRequestRepository) {
        this.okHttpClient = okHttpClient;
        this.outgoingRequestRepository = outgoingRequestRepository;
    }

    public Object submit(OutgoingRequest request) {
        return doProcess(request);
    }


    private Object doProcess(OutgoingRequest request) {
        try {
            return request.execute(okHttpClient);
        } finally {
            outgoingRequestRepository.save(request);
        }
    }
}

在上面的示例代码中:

  • OutgoingRequest是一个被对象化的外部请求,包含了执行请求所需的所有参数:url、params、headers、body 等等
  • OutgoingRequestExecutor就是 UML 中的 invoker,提交和保存 request 后,request 调用自己的 execute 方法
  • okHttpClient 是 UML 中的 Receiver,request 在 execute 方法中通过它来执行请求

在 doProcess 方法中,还会保存 request 对象,这样就可以查询请求记录、请求的参数和结果、重试请求等等。

小结

Command 模式的核心在于:把方法、函数封装成一个对象,并且存储这个对象,用于后续的“异步化、重试、管理”等等。