掘金 后端 ( ) • 2024-06-26 15:36

函数代码开发的技巧

1. 函数编写的原则

1.1 短小

函数方法需要尽量短小,可能很多开发者都知道这个准则。函数写的尽量短,主要的好处就是让维护者能够看清代码的全貌,快速的理解开发者的意图。要想将函数写得尽量短小,开发者在编写代码需要时刻提醒自己以下两点:

  • 20行以内为佳,尽量一屏能看完
  • if / else / while 代码块理应只该有一行代码,2层以内的嵌套结构为佳

举个《代码整洁之道》这本书中的一个例子,看看作者是如何一步步缩短函数长度的。如下:

public static String testableHtml(PageData pageData, boolean includeSuiteSetup) throws Exception {
  WikiPage wikiPage = pageData.getWikiPage();
  StringBuffer buffer = new StringBuffer();
  if (pageData.hasAttribute("Test")) {
    if (includeSuiteSetup) {
      WikiPage suiteSetup =
      PageCrawlerImpl.getInheritedPage(
              SuiteResponder.SUITE_SETUP_NAME, wikiPage
      );
      if (suiteSetup != null) {
      WikiPagePath pagePath =
        suiteSetup.getPageCrawler().getFullPath(suiteSetup);
      String pagePathName = PathParser.render(pagePath);
      buffer.append("!include -setup .")
            .append(pagePathName)
            .append("\n");
      }
    }
    WikiPage setup =
      PageCrawlerImpl.getInheritedPage("SetUp", wikiPage);
    if (setup != null) {
      WikiPagePath setupPath =
        wikiPage.getPageCrawler().getFullPath(setup);
      String setupPathName = PathParser.render(setupPath);
      buffer.append("!include -setup .")
            .append(setupPathName)
            .append("\n");
    }
  }
  buffer.append(pageData.getContent());
  if (pageData.hasAttribute("Test")) {
    WikiPage teardown =
      PageCrawlerImpl.getInheritedPage("TearDown", wikiPage);
    if (teardown != null) {
      WikiPagePath tearDownPath =
        wikiPage.getPageCrawler().getFullPath(teardown);
      String tearDownPathName = PathParser.render(tearDownPath);
      buffer.append("\n")
            .append("!include -teardown .")
            .append(tearDownPathName)
            .append("\n");
    }
    if (includeSuiteSetup) {
      WikiPage suiteTeardown =
        PageCrawlerImpl.getInheritedPage(
                SuiteResponder.SUITE_TEARDOWN_NAME,
                wikiPage
        );
      if (suiteTeardown != null) {
        WikiPagePath pagePath =
          suiteTeardown.getPageCrawler().getFullPath (suiteTeardown);
        String pagePathName = PathParser.render(pagePath);
        buffer.append("!include -teardown .")
              .append(pagePathName)
              .append("\n");
      }
    }
  }
  pageData.setContent(buffer.toString());
  return pageData.getHtml();
}

在上面的代码中,有太多不同层级的抽象、奇怪的字符串、函数调用,混以双重嵌套、用标识来控制的 if 语句等,令人头疼。只要做几个简单的方法抽离和重命名操作,加上一点点重构,就能在接下来的几行代码中搞定:

public static String renderPageWithSetupsAndTeardowns(PageData pageData, boolean isSuite) throws Exception {
  boolean isTestPage = pageData.hasAttribute("Test");
  if (isTestPage) {
    WikiPage testPage = pageData.getWikiPage();
    StringBuffer newPageContent = new StringBuffer();
    includeSetupPages(testPage, newPageContent, isSuite);
    newPageContent.append(pageData.getContent());
    includeTeardownPages(testPage, newPageContent, isSuite);
    pageData.setContent(newPageContent.toString());
  }
  return pageData.getHtml();
}

这段代码比第一个例子清晰多了,你大概能明白,这个函数要把一些设置和拆解页放入一个测试页面,再渲染为 HTML。

但这样优化还不够整洁,作者说,「函数的第一规则是要短小。第二条规则是还要更短小。」

再次重构后,代码终于一目了然:

public static String renderPageWithSetupsAndTeardowns(PageData pageData, boolean isSuite) throws Exception {
  if (isTestPage(pageData))
    includeSetupAndTeardownPages(pageData, isSuite);
  return pageData.getHtml();
}

1.2 只干一件事

只干一件事对于java开发者来说再熟悉不过了,因为java开发原则中就有一条单一职责原则,说的就是每个类在设计之初职责必须单一,不能承担过多不属于这个类的事情,这个同样适用于函数方法。我们实际工作中很多同学会写的一类代码,附在下文:

public boolean changeStatus(String uuid, Req req) { 
    // 查询 
    Acct acct = vccsAcctToolService.getAcctsCacheByUUid(uuid); 
    if (acct != null) { 
        req.setContractNo(acct.getContrNbr()); 
    } 
    // 更新标记  
    vccsAcctToolService.updateContrSupplySign(uuid); 
    return true; 
}

上述代码并不完全是项目中的代码,意在说明函数只干一件事的重要性。代码中的入参req已经发生了变更,再往下透传的时候contractNo已不再是最原始的参数值。

函数功能应尽量纯粹,不要掺杂入参对象的随意更改。

1.3 3个以内参数为佳

最理想的参数数量是零(零参数函数),其次是一(单参数函数),再次是二(双参数函数),应尽量避免三(三参数函数)。

  • 阅读者的角度,参数越多,函数越不容易读懂,理解也更困难。
  • 测试者的角度,参数越多,组合出来的测试用例也是指数级增长。

就像下面"画圆"的例子,可以压缩参数的个数,函数也更好理解。

Circle makeCircle(double x, double y, double radius);
Circle makeCircle(Point center, double radius);

2. 注释的使用

当初我们热衷于比较谁写的注释更多,认为注释多就是好的编程实践。然而,回顾过去的代码,我们可能会发现那些过度依赖注释的代码实际上暴露了维护的难题。实际上,写注释的初衷是为了帮助读者理解函数的目的和运作方式。然而,随着时间的推移,我们认识到真正的优秀代码应该追求自解释性,即函数名称和代码结构本身就能清晰地传达其意图。好的注释应该简洁、准确,并仅用于解释那些无法通过代码直接理解的部分。因此,高质量的代码应当努力通过清晰的函数命名和易于理解的逻辑结构来减少不必要的注释。 好的注释大致有以下几种:

  • 法律信息
  • 提供信息的注释(时间格式...)
  • 对意图的解释
  • 警告
  • TODO
  • 公共 API 你愿意看到这个:
// Check to see if the employee is eligible for full benefits
if ((employee.flags & HOURLY_FLAG) &&(employee.age > 65))

还是这个?

if (employee.isEligibleForFullBenefits())

显然,第二种方法代码的可读性更好,也不需要添加额外的注释。

结语:合理的注释是让读者更好的读懂代码,非必要的注释只会徒增阅读的负担。尽量不要用注释来解释代码,让代码自解释。 个人推荐:公共代码需要编写Javadoc注释,可以参考jdk底层类中的编写。

3. 异常函数方法

代码中的异常处理其实也是单独的一个函数方法,它也遵循只干一件事的原则。想要将代码中的try/catch代码块优化的很整洁好看,通常可以参照以下两点:

  • 抽离try和catch代码块的主体部分。
  • 错误处理就是一件事。try就是这个函数的开头,catch/finally后面不要有其他内容。 参考书中给的一段示例代码:
public void delete(Page page) {
    try {
        deletePageAndAllReferences(page);
    } catch (Exception e) {
        logError(e);
    }
}
private void deletePageAndAllReferences(Page page) throws Exception {
    deletePage(page);
    registry.deleteReference(page.name);
    configKeys.deleteKey(page.name.makeKey());
}
private void logError(Exception e) {
    logger.log(e.getMessage());
}  

在前面的示例中,delete 函数的主要关注点在于错误处理,这使得它的主要逻辑容易被忽视。而 deletePageAndAllReferences 函数则专注于完整地删除一个页面,错误处理的部分相对独立且可以被视为辅助逻辑。这种明确的职责区隔让代码变得更加清晰和易于理解,也方便了后续的修改和维护。 对于是否使用异常替代返回码来处理错误,这确实是一个需要根据具体场景和团队习惯来权衡的问题。不同的开发者和团队可能会有不同的偏好和考量。然而,无论选择哪种方式,关键在于写出的代码应当易于理解和维护。在异常处理函数中,我们还需要特别注意两点原则:首先,要确保异常的处理逻辑是恰当的,避免在异常处理中引入新的问题;其次,要尽量减少不必要的异常抛出,避免对调用者造成不必要的困扰。总的来说,选择适合自己的错误处理方式,确保代码的质量和可维护性才是最重要的。 另外,还有两点原则也是在异常处理函数中需要注意的:

  • 不返回null值
  • 不传递null值

4. 格式运用的小技巧

格式的最大好处就是可以提高可读性,方便维护和拓展。代码的格式可以分为: 垂直格式

  • 概念间垂直方向上的间隔(空一行)
  • 垂直方向上的靠近(紧密联系的代码放在一起,不要隔开)
  • 垂直距离
  • 变量声明:靠近其使用位置
  • 实体变量:类的顶部
  • 相关函数:调用者放在被调用者的上方(靠近) 横向格式
  • 根据是否紧密相关进行:
  • 隔离(等号两边加空格)
  • 靠近:(乘号两边不加空格)
  • 空范围:将; 换行

这里推荐安装eclipse code formatter插件来统一开发者的代码书写格式。