Loading... > 从不同的抽象层次理解单一职责 # 一、长函数的产生 > 不过,限制函数长度,是一种简单粗暴的解决方案。最重要的是你要知道,长函数本身是一个结果,如果不理解长函数产生的原因,还是很难写出整洁的代码。接下来,我们就来看看长函数是怎么产生的。 如何确定该提炼哪一段代码:寻找注释 ## 1、多长的函数才算“长”? 那就是不要超过一个显示屏的垂直高度。比如,在我的电脑上,如果要让一个函数的代码完整地显示在 IDE 中,那最大代码行数不能超过 50。这个说法我觉得挺有道理的。因为超过一屏之后,在阅读代码的时候,为了串联前后的代码逻辑,就可能需要频繁地上下滚动屏幕,阅读体验不好不说,还容易出错 ## 2、**以性能为由** 不过,这个观点在今天是站不住的。性能优化不应该是写代码的第一考量。 一方面,一门有活力的程序设计语言本身是不断优化的,无论是编译器,还是运行时,性能都会越来越好;另一方面,可维护性比性能优化要优先考虑,当性能不足以满足需要时,我们再来做相应的测量,找到焦点,进行特定的优化。这比在写代码时就考虑所谓性能要更能锁定焦点,优化才是有意义的。 现代编程语言几乎已经完全免除了进程内的函数调用开销。 ## 3、平铺直叙 一种最常见的原因也会把代码写长,那就是写代码平铺直叙,把自己想到的一点点罗列出来。 ```cpp //这段代码来自java public void executeTask() { ObjectMapper mapper = new ObjectMapper(); CloseableHttpClient client = HttpClients.createDefault(); List<Chapter> chapters = this.chapterService.getUntranslatedChapters(); for (Chapter chapter : chapters) { // Send Chapter SendChapterRequest sendChapterRequest = new SendChapterRequest(); sendChapterRequest.setTitle(chapter.getTitle()); sendChapterRequest.setContent(chapter.getContent()); HttpPost sendChapterPost = new HttpPost(sendChapterUrl); CloseableHttpResponse sendChapterHttpResponse = null; String chapterId = null; try { String sendChapterRequestText = mapper.writeValueAsString(sendChapterRequest); sendChapterPost.setEntity(new StringEntity(sendChapterRequestText)); sendChapterHttpResponse = client.execute(sendChapterPost); HttpEntity sendChapterEntity = sendChapterPost.getEntity(); SendChapterResponse sendChapterResponse = mapper.readValue(sendChapterEntity.getContent(), SendChapterResponse.class); chapterId = sendChapterResponse.getChapterId(); } catch (IOException e) { throw new RuntimeException(e); } finally { try { if (sendChapterHttpResponse != null) { sendChapterHttpResponse.close(); } } catch (IOException e) { // ignore } } // Translate Chapter HttpPost translateChapterPost = new HttpPost(translateChapterUrl); CloseableHttpResponse translateChapterHttpResponse = null; try { TranslateChapterRequest translateChapterRequest = new TranslateChapterRequest(); translateChapterRequest.setChapterId(chapterId); String translateChapterRequestText = mapper.writeValueAsString(translateChapterRequest); translateChapterPost.setEntity(new StringEntity(translateChapterRequestText)); translateChapterHttpResponse = client.execute(translateChapterPost); HttpEntity translateChapterEntity = translateChapterHttpResponse.getEntity(); TranslateChapterResponse translateChapterResponse = mapper.readValue(translateChapterEntity.getContent(), TranslateChapterResponse.class); if (!translateChapterResponse.isSuccess()) { logger.warn("Fail to start translate: {}", chapterId); } } catch (IOException e) { throw new RuntimeException(e); } finally { if (translateChapterHttpResponse != null) { try { translateChapterHttpResponse.close(); } catch (IOException e) { // ignore } } } } ``` 主要原因就是把前面所说的逻辑全部平铺直叙地摆在那里了,这里既有业务处理的逻辑,比如,把章节发送给翻译引擎,然后,启动翻译过程;又有处理的细节,比如,把对象转成 JSON,然后,通过 HTTP 客户端发送出去。 从这段代码中,我们可以看到平铺直叙的代码存在的两个典型问题: 把多个业务处理流程放在一个函数里实现; 把不同层面的细节放到一个函数里实现。 重构后: ```cpp public void executeTask() { ObjectMapper mapper = new ObjectMapper(); CloseableHttpClient client = HttpClients.createDefault(); List<Chapter> chapters = this.chapterService.getUntranslatedChapters(); for (Chapter chapter : chapters) { String chapterId = sendChapter(mapper, client, chapter); translateChapter(mapper, client, chapterId); } } ``` `将一个长函数拆分为不同层面的短函数, 方法可以参考后面的”函数语句放在同一抽象层级“` 函数中间实现的细节 ```cpp private String sendChapter(final ObjectMapper mapper, final CloseableHttpClient client, final Chapter chapter) { SendChapterRequest request = asSendChapterRequest(chapter); CloseableHttpResponse response = null; String chapterId = null; try { HttpPost post = sendChapterRequest(mapper, request); response = client.execute(post); chapterId = asChapterId(mapper, post); } catch (IOException e) { throw new RuntimeException(e); } finally { try { if (response != null) { response.close(); } } catch (IOException e) { // ignore } } return chapterId; } private HttpPost sendChapterRequest(final ObjectMapper mapper, final SendChapterRequest sendChapterRequest) throws JsonProcessingException, UnsupportedEncodingException { HttpPost post = new HttpPost(sendChapterUrl); String requestText = mapper.writeValueAsString(sendChapterRequest); post.setEntity(new StringEntity(requestText)); return post; } private String asChapterId(final ObjectMapper mapper, final HttpPost sendChapterPost) throws IOException { String chapterId; HttpEntity entity = sendChapterPost.getEntity(); SendChapterResponse response = mapper.readValue(entity.getContent(), SendChapterResponse.class); chapterId = response.getChapterId(); return chapterId; } private SendChapterRequest asSendChapterRequest(final Chapter chapter) { SendChapterRequest request = new SendChapterRequest(); request.setTitle(chapter.getTitle()); request.setContent(chapter.getContent()); return request } ``` 我们只用了最简单的提取函数这个重构手法,就把一个大函数拆分成了若干的小函数。 **长函数往往还隐含着一个命名问题**。如果你看修改后的 sendChapter,其中的变量命名明显比之前要短,理解的成本也相应地会降低。因为变量都是在这个短小的上下文里,也就不会产生那么多的命名冲突,变量名当然就可以写短一些。 Note: for (Chapter chapter : chapters) 一般for循环内部分函数都可以重新提取为一个新的函数 再举一个类似的例子: ```cpp // 重构前的代码 public List<String> appendSalts(List<String> passwords) { if (passwords == null || passwords.isEmpty()) { return Collections.emptyList(); } List<String> passwordsWithSalt = new ArrayList<>(); for (String password : passwords) { if (password == null) continue; if (password.length() < 8) { // ... } else { // ... } } return passwordsWithSalt; } ``` 重构后: ```cpp // 重构后的代码:将部分逻辑抽成函数 public List<String> appendSalts(List<String> passwords) { if (passwords == null || passwords.isEmpty()) { return Collections.emptyList(); } List<String> passwordsWithSalt = new ArrayList<>(); for (String password : passwords) { if (password == null) continue; passwordsWithSalt.add(appendSalt(password)); } return passwordsWithSalt; } private String appendSalt(String password) { String passwordWithSalt = password; if (password.length() < 8) { // ... } else { // ... } return passwordWithSalt; } ``` ## 4、一次加一点 有时,一段代码一开始的时候并不长,就像下面这段代码,它根据返回的错误进行相应地错误处理: ```cpp if (code == 400 || code == 401) { // 做一些错误处理 } ``` 然后,新的需求来了,增加了新的错误码,它就变成了这个样子: ```cpp if (code == 400 || code == 401 || code == 402) { // 做一些错误处理 } ``` **任何代码都经不起这种无意识的累积,每个人都没做错,但最终的结果很糟糕。** 代码变长根本是一个无意识的问题,写代码的人没有觉得自己把代码破坏了。但只要你认识到长函数是一个坏味道,后面的许多问题就自然而然地会被发掘出来,至于解决方案,你已经看到了,大部分情况下,就是拆分成各种小函数。 # 二、分解条件表达式 > 代码嵌套层次过深往往是因为 if-else、switch-case、for 循环过度嵌套导致的。 对于条件逻辑,将每个分支条件分解成新函数还可以带来更多好处,可以突出条件逻辑,更清楚的表明每个分支的作用,并且突出每个分支的原因。 ## 1、这种方式并不能减少循环的次数,但是可以提高代码的可读性 > 对条件判断和每个条件分支运用提炼函数的手法 ```cpp void ApdTimerController::sfpInstalledCallback(const std::string& sfp) { std::string connector = connectorFacade_.getByChild(sfp); if (**sfpFacade_.isPresent(sfpFacade_.getByPort(connector)) && connectorFacade_.isAdministrativeStateUnlocked(connector)**) { startTimer(connector); } } bool ApdTimerController::isSfpPresent(const std::string& connector) { return sfpFacade_.isPresent(sfpFacade_.getByPort(connector)); } bool ApdTimerController::isUnlocked(const std::string& connector) { return connectorFacade_.isAdministrativeStateUnlocked(connector); } ``` 也可以使用解释性变量来解释复杂表达式: ```cpp void ApdTimerController::sfpInstalledCallback(const std::string& sfp) { std::string connector = connectorFacade_.getByChild(sfp); bool isDetectedStatus = sfpFacade_.isPresent(sfpFacade_.getByPort(connector)) && connectorFacade_.isAdministrativeStateUnlocked(connector); if (isDetectedStatus) { startTimer(connector); } } ``` 可以参考如何减少圈复杂度的介绍! # 三、代码分割成更小的单元块 > 善于将大块的复杂逻辑提炼成类或者函数,屏蔽掉细节,让阅读代码的人不至于迷失在细节中,这样能极大地提高代码的可读性。 重构前,在 invest() 函数中,最开始的那段关于时间处理的代码,是不是很难看懂? ```cpp // 重构前的代码 public: void invest(int userId, int financialProductId) { Calendar calendar = Calendar.getInstance(); calendar.setTime(date); calendar.set(Calendar.DATE, (calendar.get(Calendar.DATE) + 1)); if (calendar.get(Calendar.DAY_OF_MONTH) == 1) { return; } //... } ``` 解决办法: 提炼函数,将逻辑抽象成一个函数 ```cpp // 重构后的代码:提炼函数之后逻辑更加清晰 public: void invest(int userId, int financialProductId) { if (isLastDayOfMonth(new Date())) { return; } //... } public: boolean isLastDayOfMonth(Date date) { Calendar calendar = Calendar.getInstance(); calendar.setTime(date); calendar.set(Calendar.DATE, (calendar.get(Calendar.DATE) + 1)); if (calendar.get(Calendar.DAY_OF_MONTH) == 1) { return true; } return false; } ``` 重构之后,我们将这部分逻辑抽象成一个函数,**并且命名为 isLastDayOfMonth,从名字就能清晰地了解它的功能**,判断今天是不是当月的最后一天。这里,我们就是通过将复杂的逻辑代码提炼成函数,大大提高了代码的可读性。 Note: 只有代码逻辑比较复杂的时候,我们其实才建议提炼类或者函数。**毕竟如果提炼出的函数只包含两三行代码,在阅读代码的时候,还得跳过去看一下,这样反倒增加了阅读成本**。 ## 1、函数设计要职责单一 > 单一职责原则的时候,针对的是类、模块这样的应用对象。实际上,对于函数的设计来说,更要满足单一职责原则。相对于类和模块,函数的粒度比较小,代码行数少。 ```cpp public: boolean checkUserIfExisting(String telephone, String username, String email) { if (!StringUtils.isBlank(telephone)) { User user = userRepo.selectUserByTelephone(telephone); return user != null; } if (!StringUtils.isBlank(username)) { User user = userRepo.selectUserByUsername(username); return user != null; } if (!StringUtils.isBlank(email)) { User user = userRepo.selectUserByEmail(email); return user != null; } return false; } // 拆分成三个函数 public: boolean checkUserIfExistingByTelephone(String telephone); boolean checkUserIfExistingByUsername(String username); boolean checkUserIfExistingByEmail(String email); ``` 拆成三个函数。 ## 2、移除过深的嵌套层次 `长函数的脚本检测方法`: 比如,在 Java 中,我们就可以把代码行的约束加到 CheckStyle 的配置文件中,就像下面这样 ```cpp <module name="MethodLength"> <property name="tokens" value="METHOD_DEF"/> <property name="max" value="20"/> <property name="countEmpty" value="false"/> </module> ``` 最后修改:2025 年 07 月 01 日 © 允许规范转载 打赏 赞赏作者 支付宝微信 赞 如果觉得我的文章对你有用,请随意赞赏