zoukankan      html  css  js  c++  java
  • 业务代码“五宗罪”:为什么业务代码看起来总是不够清晰直观

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


    引子

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

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

    “五宗罪”

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

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

    • 业务语义: 这段代码到底完成的是什么业务意图?为什么要做这件事 ?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 ,不易定位和排查。

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


  • 相关阅读:
    windows服务等获取文件路径文件目录方法
    推荐几款很棒的 JavaScript 表单美化和验证插件
    在网站制作中随时可用的10个 HTML5 代码片段
    SQLServer数据排序
    SQL Join的一些总结
    3.14(链表练习)
    链表
    基数排序
    hdu-1198(并查集)
    hdu-1207(规律推导)
  • 原文地址:https://www.cnblogs.com/lovesqcc/p/14575821.html
Copyright © 2011-2022 走看看