行为型模式
问题
在一些场景下,我们需要把执行的方法、函数(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 模式的核心在于:把方法、函数封装成一个对象,并且存储这个对象,用于后续的“异步化、重试、管理”等等。