如何解决模板式的冗余代码问题?

作者:微信小助手

发布时间:2021-04-01T08:17:48

作者 | 李立坤
当项目中在使用到诸如 Elasticsearch 的中间件时,客户端对不同数据模型的 CRUD 操作存在着大量模版式的冗余代码,每次有新的业务数据需要 Elasticsearch 的管理时都会重写类似的 CRUD 逻辑,这些 CRUD 代码除了数据模型不同,通用功能的代码逻辑几乎一样。显然,在这种情况下,我们完全可以抽取出通用功能的代码,将其定义成一个模版。当接入具体的业务数据时,只需要进行模版实例化的代码书写,把因业务不同的数据模型嵌入到模版中,从而避免重复书写功能相同的代码,最终达到提高开发效率,降低开发成本的目的。

在实际的项目开发中,如何解决模板式的冗余代码?

首先,考虑到 Java 语言以及 ElasticSerarch 在项目开发中的普及性,我选择基于 Java 语言使用 Elasticsearch 的技术场景来澄清以及解决模板式的代码冗余问题。

其次,为了使得问题的澄清和解决更具象化,我以简单的用户信息和订单信息为例,来阐述澄清和解决问题的具体过程。

那么,我们先定义一下简单用户信息和订单信息的数据模型:

/** * 简单用户信息数据模型。 */public class UserInfo {    private String id; // ID    private String nickName; // 昵称    private Integer age; // 年龄    private String introduction; // 简介    private String signature; // 签名}
/** * 简单订单信息数据模型。 */@Datapublic class OrderInfo {    private String  id;     private String productId; // 产品 ID    private Integer productNum; // 产品数据量    private Integer status; // 订单状态    private String remark; // 备注}
1什么是模板式的冗余代码?

接下来,我们澄清一下模版式的冗余代码问题。

当用户信息和订单信息接受 Elasticsearch 的管理时,直接使用 Elasticsearch 客户端 API 创建索引的代码如下:

/** * ES 创建用户信息文档。 */public ServiceResponse<Boolean> create(UserInfo userInfo) {    // 参数检查逻辑。    String userInfoId = userInfo.getId();    if (StringUtils.isEmpty(userInfoId)) {        return ServiceResponse.FAIL_RESPONSE;    }    // 创建索引请求。    IndexRequest indexRequest = new IndexRequest("user_info_index", "user_info_type", userInfoId);    String documentData = JSON.toJSONString(userInfo);    indexRequest.source(documentData, XContentType.JSON);    indexRequest.opType(DocWriteRequest.OpType.INDEX);    // 调用客户端 API。    try {        InitESRestClient.getClient().index(indexRequest, RequestOptions.DEFAULT);    } catch (Exception e) {        log.error("error when save document:{}.", JSON.toJSONString(userInfo), e);        return ServiceResponse.FAIL_RESPONSE;    }    return ServiceResponse.SUCCESS_RESPONSE;}
/** * ES 创建订单信息文档。 */public ServiceResponse<Boolean> create(OrderInfo orderInfo) {    // 参数检查逻辑。    String orderInfoId = orderInfo.getId();    if (StringUtils.isEmpty(orderInfoId)) {        return ServiceResponse.FAIL_RESPONSE;    }    // 创建索引请求。    IndexRequest indexRequest = new IndexRequest("order_info_index", "order_info_type", orderInfoId);    String documentData = JSON.toJSONString(orderInfo);    indexRequest.source(documentData, XContentType.JSON);    indexRequest.opType(DocWriteRequest.OpType.INDEX);    // 调用客户端 API。    try {        InitESRestClient.getClient().index(indexRequest, RequestOptions.DEFAULT);    } catch (Exception e) {        log.error("error when save document:{}.", JSON.toJSONString(orderInfo), e);        return ServiceResponse.FAIL_RESPONSE;    }    return ServiceResponse.SUCCESS_RESPONSE;}

为了让大家更清晰地关注到代码中的重点,这里说明一下代码上的约定:

  1. ServiceResponse 是一个通用的业务对象,用来表达业务逻辑执行成功或失败的信息;

  2. InitESRestClient.getClient() 用来获取默认的 Elasticsearch 集群客户端;

  3. 其他未特别说明的都是标准的 Java 语言和 Elasticsearch 客户端 API。

从上面的代码我们可以看出,无论对于用户信息的索引创建还是订单信息的索引创建,我们无可避免地要写参数检查、创建索引请求、调用客户端 API 的相同逻辑。这些逻辑的代码看着似乎都是不一样的,但整体的逻辑都是相同的,就像是一个执行流程的模板,只是模板实例化时传递的参数不一样而已。

从代码冗余的角度讲,我们完全没有必要,对用户信息和订单信息索引创建都写一份逻辑相似的代码,即使他们看上去并不是一摸一样。像上面这样,看上去并不是一摸一样的代码,但是存在着一样的处理逻辑,使用着相同形式的代码,我们就叫做模板式的冗余代码。

2定义模板

从用户信息与订单信息的索引创建过程中,我们发现模板式的冗余代码主要由两部分构成。

一个是不变的部分,即对任意的数据模型,这些部分的处理都是一样的,主要有以下几点:

  1. 对数据中的唯一标识做合法性检查,即通用参数的检查逻辑;

  2. 构建 Elasticsearch API 的 IndexRequest,即构建索引创建请求;

  3. 获取 Elasticsearch 的集群客户端发送索引创建请求并进行异常处理,即通用的发送请求并进行异常处理的逻辑。

另一个是变化的部分,这些部分都是根据不同的数据模型而变化的,主要有以下几点:

  1. 传入参数不同,用户信息传入的是 UserInfo,而订单信息是 OrderInfo;

  2. 数据配置不同,用户信息进入的 user_info_index 索引的 user_info_type,而订单信息进入的是 order_info_index 索引的 order_info_type;

  3. 集群选择可能不同,这里用户信息和订单信息都采用的默认的集群,但实际项目开发中可能用户信息在集群 1,订单信息在集群 2。

所以,我们想要避免模板式的冗余代码,那么必然要将上述变化的部分和不变的部分定义在一个模板里,这样相似的代码只会存在于模板内。然而,上述不变的部分我们可以直接定义在模板内,而变化的部分我们怎么在模板内抽象表达呢?下面我就基于 Java 语言以 Elasticsearch 索引创建的过程为例具体定义一个包含上述不变和变化部分的模板。

方法定义

首先我们在模板中定义创建索引的方法,在 Java 语言中想要实现上述模板的功能,最好的语言组件自然是 Java 中的抽象类了,同时这里为了在模板中描述方法中变化的参数,即 UserInfo、OrderInfo 等数据模型,我们必然要使用到 Java 中的泛型参数在抽象类对数据模型进行抽象统一。所以我们初步定义的包含创建索引功能的抽象类就是下面这个样子了:

/** * 模板的定义。 * DOC 泛型参数的定义是对模板中变化部分的抽象, * 对接受 Elasticsearch 管理的数据进行统一表示。 */public class AbstractBaseESServiceImpl<DOC> {  /**   * 在模板中提供创建索引的功能,该功能可对任意数据模型生效。   */  public ServiceResponse<Boolean> create(DOC doc) {}}

上述代码基于 Java 中的抽象类和泛型参数对模板做了一个初步的定义,该模板暂时只包含创建索引的功能,但是具体的创建索引的方法并没有实现,为什么呢?因为我们要想实现上述创建索引的参数检查,创建索引请求,发送请求的逻辑,我们必须先知道数据模型中那些通用字段需要做参数检查,该数据要去那个索引和那个类型,该数据要进入那个集群这些信息,显然我们仅仅根据 Doc 类型的 doc 参数是无法获取这些信息的。

元数据初始化

那么如何获取上述所述的信息呢?这时我们就可以利用 Java 语言强大的元语言编程能力,在对象初始化的时候,获取到泛型参数实例化后实际数据模型的元数据。当有了这些准备好的元数据后,就可以在方法运行时,根据实际参数和元数据获取到相关的信息从而完成创建索引的逻辑。所以,接下来我们要做的就是如何准备元数据可以获取到上述那些通用字段需要做参数检查,数据要进入那个索引和那个类型的信息。

在 Java 语言中,元数据信息都是存放在 Class 类型的对象中,所以我们可以在对象初始化的时候先获取到上述 DOC 泛型参数所表示的数据模型的 Class 对象,代码如下:

/** * DOC 数据模型的 Class 对象。 */private Class<DOC> docClass;/** * 初始化元数据。 */public AbstractBaseESServiceImpl() {    this.docClass = this.getDocClass();}/** * 获取 DOC 数据模型的元数据。 * 这里主要是获取子类泛型参数实例化后实际数据类型的 Class 对象。 */private Class<DOC> getDocClass() {    Type genericSuperclass = this.getClass().getGenericSuperclass();    Type docType = ((ParameterizedType) genericSuperclass).getActualTypeArguments()[0];    if (!(docType instanceof ParameterizedType)) {        return (Class<DOC>) docType;      }     return (Class<DOC>)((ParameterizedType)docType).getRawType();}

上述代码都是基于 Java 语言反射机制的实现,具体 API 这里就不细说了。大概思路就是在对象初始化时,使获取到元数据,即在模板中获取到实际数据类型的元数据,为后面获取具体变化的部分信息做准备。变化部分的信息主要包括两方面,一是哪些通用字段需要做参数校验,二是数据进入哪个 Elasticsearch 索引和类型。

 通用字段元数据的准备

这里我们先解决获取通用字段元数据信息的问题,从而为通用字段的参数校验做准备。Java 语言中,我们可以通过注解,在各个组件中添加一些标记信息。自然而然地,我们可以自定义注解在实际数据模型定义中标记那些通用字段,根据该标记信息我们可以获取到需要做参数校验地通用字段的元数据信息。

例如上述我们要求任何接受 Elastic 管理的数据都要定义唯一标识,且要求唯一标识是不为空的字符串类型。

如何实现这个需求呢?这时候我们就可发挥 Java 中注解的强大功能了,比如我们自定义一个 DocID 的注解,该注解只能加在 Java 类的字段上,而且必须是不为空的字符串类型。

那么,我们先自定义一个 DocID 的注解,代码如下:

/** * 定义 DOC 数据模型中的唯一标识字段。 * 该注解用于在实际的数据模型中标记 Elasticsearch 文档的唯一标识。 */@Target({ElementType.FIELD})@Retention(RetentionPolicy.RUNTIME)public @interface DocID{}

显然,该注解的处理逻辑必然是要在模板对象即上述抽象类对象初始化时存在,也就是说,我们要在上述抽象类初始化时,根据数据模型的元数据获取到标记了该注解的字段的元数据,从而为后面通用参数的校验做准备,这段逻辑具体的代码如下:

/** * 标识字段元数据,用于处理唯一标识相关逻辑,比如参数检查。 */protected final Field idField;/** * 初始化元数据。 */public AbstractBaseESServiceImpl() {    this.docClass = this.getDocClass();    this.idField = this.getIDField();    this.idField.setAccessible(true);}/** * 获取文档的 ID 字段元数据。 * 要求用户在使用时,必须通过自定义注解 DocID,告诉我们实际数据模型 * 哪个字段是 ID 字段,从而获取到 ID 字段的具体值,用于通用参数的逻辑检查以及和唯一标识 * 相关的逻辑。 */private Field getIDField() {    Field[] declaredFields = this.docClass.getDeclaredFields();    Field idField = null;    for (Field declaredField : declaredFields) {        DocID docID =declaredField.getAnnotation(DocID.class);        if (null == docID) {            continue;        }        idField = declaredField;        break;    }    // 要求数据模型中必须定义 ID 字段。    if (null == idField) {        throw new DocIDUndefineException();    }    // 要求 ID 字段的类型为字符串。    if (idField.getType() != String.class) {        throw new DocIDNotStringException();    }    return idField;}