业务代码“五宗罪”:为什么业务代码看起来总是不够清晰直观

好代码不仅仅关乎风格和习惯,更关乎对技术和设计的理解。


引子

写出若干行好代码,并不比做出一个好的设计方案更加容易。我个人认为,代码的最高境界是清晰、简洁、设计优雅。能够做到这一点的业务代码,实在是极少。很多开发人员是逻辑感不错,但表达水平糟糕,而程序设计是一项逻辑、设计与表达结合的活动,三者缺一不可,而业界目前仍然更注重逻辑和技术层面(面试考察的主要偏重于此)。

很多程序员在其开发生涯中接触到的大多是业务系统,写下来的也是业务代码。然而,放眼望去,很多业务代码看上去很不够直观清晰,很难一眼就能理解代码说的是什么。 为什么会这样? 本文探讨业务代码的“五宗罪”。

“五宗罪”

缺乏清晰的业务语义和顶层视图

这是很多很多业务代码的通病。 放眼望去,只有一段段字符的“飞流直下三千尺”,很难清晰直观地看出其业务语义和顶层视图。

  • 业务语义: 这段代码到底完成的是什么业务意图?为什么要做这件事 ?Explain what and why , not merely how ;
  • 顶层视图: 整个业务流程包括哪些步骤,能否在十行代码里一眼就能看清楚,比如 STEP1—STEP10 。原则上,入口方法就应当只包括十行代码,就是这若干个步骤的业务语义;而每个业务语义都是一个子函数。

大多数业务代码无非是做如下事情:

  • VCRU:即校验数据(Validate)、查询数据(Retrieve),插入数据( Create ),更新数据(Update) 。通过这四件事的组合来实现业务意图,其中尤以 R 和 U 居多;
  • Send/Receive: 即发送消息(Send)、接收消息(Receive)。

是否能够用声明式的可编排的方式来清晰地展示业务意图呢?

要想写出清晰直观的业务代码,有若干建议:

  • 遵循“设计先行”原则,从设计上把握整体和全局;
  • 遵循“自顶向下”和“意图导航”的程序设计和编写法则;
  • 高层展现语义,底层呈现细节;
  • 业务关注点抽离成业务组件,做成可编排的业务流程。

业务中掺杂技术细节

为什么业务代码总是显得不够清晰 ? 因为开发人员总喜欢把业务与技术细节掺杂在一起。如下所示:

要改善这样的代码,需要将技术细节提取成可复用的基础库,而业务则可写成声明式的。

先创建一个 StreamUtil 工具类

public class StreamUtil {
  public static <T,R> List<R> map(List<T> data, Function<T, R> mapFunc) {
    if (CollectionUtil.isEmpty(data)) { return new ArrayList();  }
    return data.stream().map(mapFunc).collect(Collectors.toList());
  }

  public static <T> List<T> filter(List<T> data, Predicate<T> filterFunc) {
    if (CollectionUtil.isEmpty(data)) { return new ArrayList();  }
    return data.stream().filter(filterFunc).collect(Collectors.toList());
  }

  public static <T,R> List<R> filterAndMap(List<T> data, Predicate<T> filterFunc, Function<T, R> mapFunc) {
    if (CollectionUtil.isEmpty(data)) { return new ArrayList();  }
    return data.stream().filter(filterFunc).map(mapFunc).collect(Collectors.toList());
  }
}

以上就可以写成如下方式,实现了类声明式编程。

rules = StreamUtil.filter(rules, rule -> BaselineUtils.matchPlatform(rule, platforms));
baseLineRules = StreamUtil.filter(rules, rule -> rule.getFamily() == BaselineRule.FAMILY_SYSTEM);

多行代码糅合在一行

如下代码所示:


response.setData(result.stream().map(

                    container -> ImageContainerDto.toDto(container, hostService.findById(container.getAgentId())))

                    .collect(Collectors.toList()));

这行代码将多个动作揉和在一起,显得不够清晰:

  • hostService.findById(container.getAgentId()) 是一个依赖 IO 的调用;

  • result.stream().map(...).collect(...) 是流的转换;

  • response.setData 是一个赋值的过程。

可以写成如下,更加清晰:


List<ImageContainerDto> imageContainers = StreamUtil.map(result, this::convert);
response.setData(imageContainers);

public ImageContainerDto convert(Container c) {
    Host host = hostService.findById(container.getAgentId());
    return ImageContainerDto.toDto(container, host);
}

此外,这行代码可能潜藏性能风险。当 result 条数很多时,每次都去查一遍 DB 获取 host ,性能会降低,且 IO 与 流操作(CPU 操作)混合在一起,是不好的做法。应当将 IO 操作和 CPU 操作分离,IO 部分做成批量并发的。进一步地,写成:

List<String> agentIds = StreamUtils.map(result, Container::getAgentId);
List<Host> hosts = hostService.findBatchHosts(agentIds);
Map<String,Host> hostMap = buildMap(hosts);
 
List<ImageContainerDto> imageContainers = StreamUtils.map(result, container -> ImageContainerDto.toDto(container, hostMap.get(agentId)));
 
response.setData(imageContainers);

多行揉和到一行,往往潜藏 NPE 。如下代码所示 如果 findImageIds 方法返回 null 时, addAll 方法会报 NPE。

configTrustImages.addAll(imageClient.findImageIds(ImageQueryParam.builder().imageIds(imageIds).build()));

应当写成:

ImageQuery query = ImageQueryParam.builder().imageIds(imageIds).build();
Set<String> imageIds = imageClient.findImageIds(query);  // 如果这个方法返回空列表而不是 null ,那就没问题。
if (CollectionUtils.isEmpty(imageIds)) {
    configTrustImages.add(imageIds);

重复模板代码

业务中往往充斥着不少重复代码,其原因在于:

  • 第一个人没有意识到其潜在的可复用性,没有良好地抽象出来;后面的人则只好 CVM(Copy-Paste-Modify)。
  • 没有将(技术和业务)关注点分离出来;许多关注点混杂在一起,难以复用。

要避免重复模板代码,有若干建议:

  • 分离通用和差异部分;
  • 对于通用部分,考虑可复用性和可扩展性;
  • 考虑模板方法模式和函数式编程来解耦通用和差异;
  • 放在 Service 层, 而非 Controller 层。

多重条件语句

多重条件语句,也是导致业务代码“不堪卒读”的重要原因之一。通常是因为:

  • 写代码的人没有仔细理顺其中的逻辑,按照思维中的逻辑顺着写下来了;
  • 多个业务逐渐累积上去,而初始又缺乏设计,导致后面的人效仿而堆砌。

如何解耦多重条件语句呢?

  • 使用 Map 结构替代 Switch ;
  • 使用 if-return 的卫述句避免头重脚轻的 if-else ,尽早返回 ;
  • 使用策略模式分离大段的功能相似的业务逻辑;
  • 重新理顺整个的逻辑,理解关键点,持续小步重构,用更清晰的方式表达出来。

小结

可以看到,第一个人写下的代码是很重要的。如果第一个人没有很好地设计和抽象,而是写成了逻辑流,那么后面的人就会效仿,逐渐形成“代码堆砌”。可谓是“始作俑者”。要远离这些“罪过”,怎么办呢?

代码即设计。

  • 代码应当能够凸显出业务语义和设计意图,而不是需要人通过代码去推断出来;
  • 代码的业务部分与技术细节应当分离,避免业务被技术细节淹没;技术部分是可复用的;
  • 将原子的业务关注点分离出来;原子的业务关注点是可以复用和扩展的;
  • 多个动作的语句,拆成多行;放在一行不利于后续改动,且容易潜藏 BUG ,不易定位和排查。

有一个很简单的代码技巧,却非常管用: 时刻注意抽离出可测试的子函数,避免混杂在主流程里。


原文地址:https://www.cnblogs.com/lovesqcc/p/14575821.html