Loading... > 复制粘贴是最容易产生重复代码的地方,所以,**一个最直白的建议就是,不要使用复制粘贴。真正应该做的是,先提取出函数**,然后,在需要的地方调用这个函数,把公共的部分先统一掉。 短函数常常能让编译器的优化功能运转更良好,**因为短函数可以更容易地被缓存**。 # 一、重复的结构 ```cpp public: void sendBook() { try { service_.sendBook(); } catch (Throwable t) { this.notification_.send(new SendFailure(t))); throw t; } } public: void sendChapter() { try { service_.sendChapter(); } catch (Throwable t) { notification_.send(new SendFailure(t))); throw t; } } public: void startTranslation() { try { service_.startTranslation(); } catch (Throwable t) { notification_.send(new SendFailure(t))); throw t; } } ``` 这三段函数业务的背景是:一个系统要把作品的相关信息发送给翻译引擎。所以,结合着代码,我们就不难理解它们的含义,**sendBook 是把作品信息发出去,sendChapter 就是把章节发送出去,而 startTranslation 则是启动翻译。** 我们可以看到,虽然这三个函数调用的业务代码不同,但它们的结构是一致的,其基本流程可以理解为:**调用业务函数;如果出错,发通知**。 上面三段代码如何进行重构,将业务抽象出来,封装一个函数将公共的内容放到里面 ```cpp private: void executeTask(const Runnable& runnable) { try { runnable.run(); } catch (Throwable t) { notification_.send(new SendFailure(t))); throw t; } } public: void sendBook() { executeTask(service_::sendBook); } public: void sendChapter() { executeTask(service_::sendChapter); } public: void startTranslation() { executeTask(service_::startTranslation); } ``` 理解到这一点,我们就容易发现结构上的相似之处。比如在上面的例子中,发送作品信息、发送章节、启动翻译之所以看上去是三件不同的事,只是因为它们的动词不同,但是除了这几个动词之外的其它部分是相同的,所以,**它们在结构上是重复的**。 Note: `当有catch这种异常场景时应该如何重构的技巧` # 二、if else表达式造成的代码重复 > 只要你看到 if 语句出现,而且 if 和 else 的代码块长得又比较像,多半就是出现了这个坏味道。如果你不想所有人都来玩“找茬”游戏,赶紧消灭它。 ```cpp if (user.isEditor()) { service.editChapter(chapterId, title, content, true); } else { service.editChapter(chapterId, title, content, false); } ``` if 选择的一定是两段不同的业务处理。但只要你稍微看一下,就会发现,if 和 else 两段代码几乎是一模一样的。在经过仔细地“找茬”之后,才能发现,原来是最后一个参数不一样。 只有参数不同,是不是和前面说的重复代码是如出一辙的?**没错,这其实也是一种重复代码**。 解决办法: 写代码要有表达性。把意图准确地表达出来,是写代码过程中非常重要的一环。显然,这里的 if 判断区分的是参数,而非动作。所以,我们可以把这段代码稍微调整一下,会让代码看上去更容易理解: ```cpp boolean approved = user.isEditor(); service.editChapter(chapterId, title, content, approved); ``` Note: 改进1: 这里我把 user.isEditor() 判断的结果赋值给了一个 approved 的变量,而不是直接作为一个参数传给 editChapter,这么做也是为了提高这段代码的可读性。因为 editChapter 最后一个参数表示的是这个章节是否审核通过。**通过引入 approved 变量,我们可以清楚地看到,一个章节审核是否通过的判断条件是“用户是否是一个编辑”,这种写法会让代码更清晰**。 改进2: 如果将来审核通过的条件改变了,变化的点全都在 approved 的这个变量的赋值上面。如果你追求更有表达性的做法,甚至可以提取一个函数出来,这样,就把变化都放到这个函数里了,就像下面这样: ```cpp boolean approved = isApproved(user); service.editChapter(chapterId, title, content, approved); private: boolean isApproved(final User user) { return user.isEditor(); } ``` 改进3: 其实也可以将两个业务分别使用两个不同的函数来命名,这样就可以避免使用bool类型变量作为函数形参 ```cpp boolean approved = isApproved(user); if (approved) { EditChapter(chapterId, title, content); } else { NotEditChapter(chapterId, title, content); } private boolean isApproved(final User user) { return user.isEditor(); } ``` # 三、何时应该把代码放进一个独立的函数中 case1: 代码的长度,当一个函数的代码的长度应该能在一横屏中显示 case2:从复用的角度考虑,认为只要用过不止一次的代码,就应该单独放进一个函数 case3:个人认为最合理的应该是,将意图与实现分开,**如果你需要花时间浏览一段代码才能弄清它到底在干什么,那么就应该将其提炼到一个函数中,并根据它所做的事为其命名**。 Tips: 可以反向来判断是否真的需要去提炼函数: 当提炼完函数时,发现并不能找出一个更有意义的名字来命名,可以再把它内联回去。 # 四、长函数的解决方法 ## 1、提炼函数 ### 1.1、创造一个新的函数 > 要想一个好的名字,提炼的过程中也会有好的名字出现 将待提炼的代码从源函数复制到新建的目标函数中,检查提炼出的函数是否有访问不到的`变量`。若是,则以参数的形式传递给新的函数。 case1:如果这个变量不只在新的函数中使用,则作为参数传递给新的函数就行。 case2:如果这个变量只在新的函数中使用,则将提炼部分声明之外的声明也加到新的函数中。 ### 1.2、仔细检查提炼出的函数 ## 2、移动语句 > 如果重复代码只是类似而不是完全相同,请首先尝试试用移动语句重组代码顺序,把相似的部分放在一起以便提炼。 ## 3、函数上移 > 当类中有两个函数出现代码重复时,就会面临修改其中一个而导致另一个未被修改的风险 总结: 开发过程中导致代码重复的原因: > 1 代码结构不合理导致同一个实现散落各处由于初期代码结构设计不合理导致后续功能实现无法快速找到已有实现,或者找到了但是不好引用已有实现。改进:初期设计代码逻辑合理,对于不合理的地方要及时重构 防止演变成原因2 2 为了稳定性,不动老逻辑 拷贝一份。由于对于业务的不熟悉和对自己代码能力的不信任不敢重构导致。改进通过微重构进行多次迭代小改进慢慢优化 3 写的时候为了快 由于时间紧张或者能力问题无法识别出的坏代码。改进提升能力 在软件开发里,有一个重要的原则叫做 Don't Repeat Yourself(不要重复自己,简称 DRY) > 在一个系统中,每一处知识都必须有单一、明确、权威地表述。 Every piece of knowledge must have a single, unambiguous, authoritative representation within a system. 最后修改:2025 年 07 月 01 日 © 允许规范转载 打赏 赞赏作者 支付宝微信 赞 如果觉得我的文章对你有用,请随意赞赏