多工程下编写可复用代码的意识和技术
背景
代码可复用性,大多数开发者都耳熟能详:它是提升软件工程效率的利器。不过,能够贯彻实现可复用性原则,并不是件容易的事情。
有四个系统,A,B,C 均依赖 S 的服务。今天 CR 了两位同学的项目代码,均涉及 A,B,C 三个系统。发现:有一个“从 JSON 字符串中取特定字段的值”的相似功能的代码段在 A,B,C 多次出现,且指定字段都是写死的。
这样导致的问题是什么呢? 读者很容易想到了。相似的代码,会在不同的系统里多次出现;且每个同学为了取不同的字段,很可能都会写一段类似的代码。可想而知,这会有多少重复的工作量!多少重复的代码!重复的代码又会持续增加后续的维护成本。
如下代码所示,有两个问题:
- 写死了只能取 BIZ_ORDER_EXTEND 的内层嵌套字段;
- 这段代码放在某个上游应用里,没法被其他工程复用。
public static String extract(Map<String, Object> extra, String extKey) {
if (MapUtils.isEmpty(extra)) {
return null;
}
try {
String bizExtend = (String) extra.get(BizConstants.BIZ_ORDER_EXTEND);
JSONObject jsonObject = JSONObject.parseObject(bizExtend);
return jsonObject.getString(extKey);
} catch (Exception e) {
logger.warn("extract failed, extKey={}, extra={}", extKey, extra, e);
return null;
}
}
原则与方法
事不过二
有个“事不过三”的原则:写第一遍,是因为新增的必须要写;写第二遍相似的代码,是因为两段逻辑可能有一点差异;写第三遍相似的代码,就不可接受了。
事实上,更严格的原则是:“事不过二”。当你发现要写第二遍时,就要意识到,这是重复的代码,需要将第一个抽离成更加通用的形式,方便复用。
公共模块
这里有两种场景。
如果相似的逻辑需要在同一个工程里反复使用,那就要在这个工程里添加一个工具类。对代码可维护性比较重视的开发者基本能做到这一点。如果不能做到这一点,先想办法做到这一点。
如果相似的逻辑需要在多个工程里反复使用呢?作为优秀的开发者,需要敏锐意识到这一点,并思考解决方案。
如果只是一段业务无关的代码,可以放在依赖工程的 API jar 里的工具类,上游的业务方都可以复用;如果是一段业务相关的代码,需要考虑开放一个通用的服务接口来提供。
对于上述情况而言,应该将这段取 json 串的字段值的逻辑放在 S 的 api jar 的工具类中。
通用形式
当打算提供一个够业务通用的工具类时,就需要考虑做到多通用的程度。 比如从 JSON 串中取指定字段的值:JSON 字符串是可变的,指定字段也是可变的。因此,必然存在两个入参。对于大部分业务场景,这样足够用了。少部分业务场景,由于可能存在嵌套的 JSON 串,可能要获取内层嵌套的字符串。
如果要做成一个通用的 JSON 字符串解析包,则需要考虑更多。比如在一个复杂嵌套的 JSON 串中,所要获取的字段可能在很深的层次。这时候,需要创建一种语法,来指明所取的字段位于 JSON 串中的位置。
通常,可以使用通用解析包来实现一个业务上够用的相对简单的通用工具类。
示例
比如有个 json 串如下。希望从中解析出指定字段的值。
{"IS_MEMBER":"true","PRICE":{"originAmount":4990,"totalAmount":4990},"BIZ_ORDER_EXTEND":"{\"CART_INFO\":\"fromRetail\"}"}
通常情况下,实现业务功能,并不需要非常通用的 json 解析能力。因此,我可以只提供两层的解析能力。比如定义两个基本函数 extract 和 extractString 。因为 String 出现频次非常高,因此提供一个函数用来获取字符串值。
基础函数
通常,先实现一个通用的基础函数,这个函数要足够灵活而健壮。 如下代码所示:
/**
* 从 Map 中析取指定字段的值
* @param extra map
* @param topField 顶层字段
* @param nestField 顶层字段嵌套下的二级字段
*
* eg.
* Map{"IS_MEMBER":"true","PRICE":{"originAmount":4990,"totalAmount":4990},"BIZ_ORDER_EXTEND":"{\"CART_INFO\":\"fromRetail\"}"}
* PRICE 是顶层字段, totalAmount 是二级字段
*/
public static Object extract(Map<String,Object> extra, String topField, String nestField) {
if (extra == null || extra.isEmpty() || StringUtils.isBlank(topField)) {
return null;
}
if (StringUtils.isBlank(nestField)) {
return extra.get(topField);
}
return extract(toString(extra.get(topField)), nestField);
}
/**
* 从 JSON 串中析取指定字段的值
* @param json json 串
* @param topField 顶层字段
* @param nestField 顶层字段嵌套下的二级字段
* @param isTopFieldString 顶层字段是否是一个 json 形式的字符串,比如 BIZ_ORDER_EXTEND
*
* eg.
* {"IS_MEMBER":"true","PRICE":{"originAmount":4990,"totalAmount":4990},"BIZ_ORDER_EXTEND":"{\"CART_INFO\":\"fromRetail\"}"}
* PRICE 是顶层字段, totalAmount 是二级字段
*/
public static Object extract(String json, String topField, String nestField, boolean isTopFieldString) {
try {
if (StringUtils.isBlank(json) || StringUtils.isBlank(topField)) {
return null;
}
if (StringUtils.isBlank(nestField)) {
return JsonPath.read(json, "$."+topField);
}
if (isTopFieldString) {
return extractWhenTopFieldIsString(json, topField, nestField);
}
return JsonPath.read(json, "$." +topField + "."+nestField);
} catch (Exception ex) {
// add log
try {
// 这里做个兜底,应对 isTopFieldString 传错的情形
return extractWhenTopFieldIsString(json, topField, nestField);
} catch (Exception e) {
// add log
}
return null;
}
}
private static Object extractWhenTopFieldIsString(String json, String field, String nestField) {
Object obj = JsonPath.read(json, "$."+field);
return JsonPath.read(obj.toString(), "$." + nestField);
}
API####
写工具类的意义在于让更多人方便的使用,提升效率。通用灵活的基础函数,使用起来通常不便利。此时,可以采用重载函数来提供更友好的工具。
为了排版清晰,可以将具有同一方法名的放在一起。
public static Object extract(String json, String field) {
return extract(json,field, null, false);
}
public static Object extract(String json, String field, String nestField) {
return extract(json,field, nestField, false);
}
private static Object extractWhenTopFieldIsString(String json, String field, String nestField) {
Object obj = JsonPath.read(json, "$."+field);
return JsonPath.read(obj.toString(), "$." + nestField);
}
public static String extractString(String json, String field) {
return extractString(json,field, null, false);
}
public static Object extractString(String json, String field, String nestField) {
return extract(json,field, nestField, false);
}
public static String extractString(String json, String field, String nestField, boolean isTopFieldString) {
Object obj = extract(json, field, nestField, isTopFieldString);
return toString(obj);
}
还可以根据常用的业务情形,提供有针对性的接口:
private static final String BIZ_ORDER_ATTRIBUTE = "BIZ_ORDER_ATTRIBUTE";
private static final String BIZ_ITEM_ATTRIBUTE = "BIZ_ITEM_ATTRIBUTE";
private static JsonPath BIZ_ORDER_ATTRIBUTE_PATH = JsonPath.compile("$."+BIZ_ORDER_ATTRIBUTE);
private static JsonPath BIZ_ITEM_ATTRIBUTE_PATH = JsonPath.compile("$."+BIZ_ITEM_ATTRIBUTE);
public static String extractItemBizAttributeString(String json, String nestField) {
return extractString(BIZ_ITEM_ATTRIBUTE_PATH.read(json), nestField);
}
public static Object extractItemBizAttribute(String json, String nestField) {
return extract(BIZ_ITEM_ATTRIBUTE_PATH.read(json), nestField);
}
public static String extractOrderBizAttributeString(String json, String nestField) {
return extractString(BIZ_ORDER_ATTRIBUTE_PATH.read(json), nestField);
}
public static Object extractOrderBizAttribute(String json, String nestField) {
return extract(BIZ_ORDER_ATTRIBUTE_PATH.read(json), nestField);
}
单测
写个单测:
public class JsonUtilTest {
String json = "{\"IS_MEMBER\":\"true\",\"PRICE\":{\"originAmount\":4990,\"totalAmount\":4990},\"BIZ_ORDER_ATTRIBUTE\":\"{\\\"CART_INFO\\\":\\\"fromRetail\\\"}\", \"BIZ_ITEM_ATTRIBUTE\":\"{\\\"IS_OUTSIDE\\\":\\\"true\\\"}\"}";
@Test
public void testExtract() {
Assert.assertEquals("", JsonUtils.extractString(null, "IS_MEMBER"));
Assert.assertEquals("", JsonUtils.extractString(json, null));
Assert.assertEquals("true", JsonUtils.extractString(json, "IS_MEMBER"));
Assert.assertEquals("4990", JsonUtils.extractString(json, "PRICE", "totalAmount", false));
Assert.assertEquals("fromRetail", JsonUtils.extractString(json, "BIZ_ORDER_ATTRIBUTE", "CART_INFO", true));
Assert.assertEquals("fromRetail", JsonUtils.extractString(json, "BIZ_ORDER_ATTRIBUTE", "CART_INFO"));
}
@Test
public void testExtractOrderBizAttributeString() {
Assert.assertEquals("fromRetail", JsonUtils.extractOrderBizAttributeString(json, "CART_INFO"));
}
@Test
public void testExtractItemBizAttributeString() {
Assert.assertEquals("true", JsonUtils.extractItemBizAttributeString(json, "IS_OUTSIDE"));
}
}
设计反思
真的应该怪罪写业务的同学没有考虑复用性吗 ? 进一步思考下,这个锅可以甩给开启 JSON 扩展字段的设计和实现者。
既然创建了JSON 扩展字段,也应该考虑到从 JSON 中解析指定字段的代码需求,一并解决了才是。因此,可以说,设计者也有考虑不周之处。
小结
代码的可复用性,是代码质量的一个重要的评定标准。程序员编写程序,并不仅仅是为了完成业务功能,还应该让自己的成果尽可能被其他人复用,产生更大的放大效果。
对于个人来说,可以增进对通用能力的技艺和领悟;对于他人来说,则提升了他人的工作效率,扩散了自己的影响力。一举两得。