重构改善既有代码的设计序言原著作者和我——也就是写这篇阅读笔记的人的观点并不完全相同。比如作者认为只要函数名取得好就不需要注释甚至以此认为只要是需要注释的代码就可以封装成一个函数哪怕只有一行代码我却认为英文的函数名再好也不如一行中文注释来得直接间接层过多反而会导致可读性降低。因此此笔记中含有部分观点是书中观点与我个人开发经验结合而来如有需要可阅读原著。原著是基于Java后端的强OO场景而如果是对于其他开发场景下比如前端书中很多理念不必教条式搬抄比如原著中认为基本类型偏执和重复代码、过大的类等一样具有重构的必要性但是在前端开发中至少在我的前端开发生涯中很少直接修改类的代码面对基本类型偏执带来的问题也有其他办法解决而不是直接解决基本类型偏执重构前须知如果你给代码新增特性时感受到困难就应该重构了重构前应该检查你是否有一个可靠的测试机制这些机制必须有自我检验能力是否有自我检测能力是指测试机制自己判断实际输出与预期输出是否一致一致则表明通过不一致则列出问题清单这些测试机制大概不会影响你重构但能够减少BUG率重构的几种常用手法抽离方法将代码中一段逻辑抽离成单独的方法注意参数和返回值设计移动方法将方法移动到更合适的位置移到更贴合的类下或是更匹配的目录下多态替换条件属性私有化状态/策略模式替换类型如果有字段是表明实例是某种类型时并且在不同类型下会有不同逻辑时可以考虑使用状态模式或者策略模式重构技术是以微小的步伐改变程序每一步都应该安排测试如果出现了问题你可以很快排查出来从复杂冗余的代码中将一个个逻辑提取出来测试保证和提取之前一致对提取出来的代码进行微小的修改可以先从修改变量名开始测试确保没有破坏任何东西重复3到4步对提取出来的逻辑进行修改测试微小修改步骤可以包括修改变量名逻辑在提取前和提取后命名可能会不同移除临时变量Replace Temp with Query声明之后就没修改过的变量或者修改过一次但没有设置初值的变量临时变量可以直接用查询查询函数替换当然这可能会导致一些额外的性能开支第一章 重构原则核心原则重构永远是一个成本收益的权衡不是一个绝对的命令只有当维护重写方法的成本超过了重构的成本时才值得重构下定义重构指在不改变软件可观测行为的前提下提高代码可读性和可维护性的行为重构的目的代码更容易被理解并且后续的修改成本更低。既不是提升系统效率也不是减少代码量容易混淆的是优化但是优化的目的是提高组件的执行速度而这往往会提高抽象程度导致代码更不容易被理解重构的难题数据库大多数商用程序都与其背后的数据库结构紧密耦合即使再小心的将系统分层数据库结构的改变还是需要迁移所有数据。对于非对象数据库可以在对象模型和数据库模型之间使用一个分隔层当某个模型发生变化时无需修改另一个模型只需要修改上述的分隔层就行对于对象数据库某些数据库提供自动迁移功能。修改公共组件、方法、接口对于这些公共的对象的修改要么确保本次修改是向下兼容的也就是修改之后的对象仍能被其调用者正常调用且逻辑正常要么确保所有调用者都在你的控制之下也就是说你能够找出所有调用了这些对象的代码并且能够修改。通常来说项目内地公共对象都是可以被找出并修改的但考虑到团队代码所有权政策或者会被其他项目使用那么就需要注意此项。设计层面的改动如果前期架构或者进行高层设计时没有考虑到某些需求后续想要改动这些牵一发而动全身的设计将是一件非常困难的事情这也说明了架构与高层设计的重要性。何时不该重构重写更简单如果将某段代码重写一遍比重构更简单那么就应该重写而不是重构重写的一个重要判断就是现有代码根本不能正常稳定运行大多数情况下都会出现BUG。重构之前应该确保大部分情况下都能正常工作临近期限重构的一个最重要的好处是后续修改起来更加简单但如果已经临近期限那么这个好处只能在期限过后体现出来为此浪费一定的时间是不理智的。如果临近期限时想要重构却没有时间说明你早就该重构了重构与设计重构和设计互补事先做好设计能够节省返工的高昂成本但是想要一步到位的找出最佳的解决方案需要极深的功力如果功力不足则必然会有各种的小漏洞而重构正好可以解决这些漏洞。重构的另一个好处就是不必在设计时承受太大的压力有一些小问题也可以在重构时修改只要确保设计是合理的就行。但重构仍不可能完全取代设计重构只是多了一个方法来应付变化带来的风险开发者仍需在设计时思考潜在的变化仍然需要一个灵活的设计只是降低了设计的难度将设计的马拉松变成了设计与重构的接力赛需要平衡设计部分和重构部分的难度不至于设计太过简单导致重构困难就行。重构与性能不少人都会混淆重构和优化经常有人问出这个问题“你这样重构能让系统提升多少效率”。然而在重构时为了使程序易于理解开发者常会做出一些影响性能的事情。这是一个非常重要的问题开发者不应该在性能和可维护性上做选择而是对两者做权衡。另外重构本身虽然可能使软件更慢但是也使得软件的性能优化更容易。之所以让人容易混淆这两个概念或许是因为很多时候局部的优化代码以及不能满足效率要求时往往要对整个模块甚至整个软件进行重构。如果开发的系统对性能要求极高比如实时系统那么在设计时就应该做好预算给每个模块甚至每个功能分配一定的资源开发者在开发时绝对不允许超过自己模块的预算。对于性能要求不是特别严重的系统可以先找出系统中大半的时间都消耗在了什么地方然后着重优化这些功能。有数据显示一个软件80%的时间都消耗在20%的代码上所以首先开发者需要有一个度量工具来监控程序的运行找出哪些吃性能的功能点然后优化他们再次度量如果没能提高性能应该撤销本次修改如此重复。第二章 可能需要重构的代码重复代码最简单的情况就是同一个类或同一个组件内部的两个方法中使用了相同的一段表达式此时只需要将这段表达式提炼成一个方法然后引用即可。最常见的情况是两个互为兄弟的子类或同一个功能模块下的子组件包含相同的表达式。那么需要将提炼出来的代码推入超类中或者放到模块下的一个utils文件中。如果两段代码只是相似不是相同仍可以使用模板模式将其相似部分提炼出来封装成一个类或一个方法在使用时使用子类修改或对方法结果或参数进行修改。如果两个互不相干的类或者组件存在相同的代码需要非常明确这两个地方用的是同样的逻辑并且一个地方修改另一个地方也要跟着修改时才将其提炼到一个类中或提炼成一个公共方法否则最好保持原样。如果开发者仅仅只能确定当前两个地方用的是相同的逻辑贸然将其提炼成公共方法反而会提高代码耦合度。过长函数间接层带来的全部好处——解释能力、共享能力、选择能力全都是由小型函数支持的。因此开发者应该积极地将函数分解成小型函数当然还是要权衡添加这个间接层是否值得。需不需要分解并不直接取决于函数的长度而在于注释或者函数名解释“做什么”和代码上的“如何做”之间的语义距离。有一个简单快速的判断方法寻找注释。如果函数内的某一行注释用于解释下方的多行代码那么或许就可以将其提炼成一个小型函数。过大的类如果想利用单个类做太多是的事情就会出现太多的示例变量完全可以将其中彼此相关的一些变量整合到一个子类中。比如一个常见的类User其中可能包含了用户名、账号、密码、身份证号、姓名、公司、职业等许多变量可以这些变量封装到对应的子类或者账号信息、身份信息、工作信息三个类中发散式修改可维护性要求软件是易于修改的需求变更也是软件开发中常见的事。开发者希望的是在某次需求变更时只需要跳到某一处关键代码修改这一段代码就能完成而不是改变了某一段代码然后再去修改依赖于这段代码的另外两段代码再去修改依赖于这两段代码的其他代码。如果出现了这种发散式修改的代码那么这段代码乃至于依赖这段代码的其他部分甚至整个系统都至少是值得被重构的。当然由需求变更引发的修改方向是不可控的即使你这次重构好了也无法保证下次需求变更是否又要重构。所以在设计时、开发时、重构时就应该考虑到系统或者模块可能的变更方向在确认需求变更时也要跟客户或领导确认变更的成本和代价当然这就不在重构的内容中了。霰弹时修改还有一种典型的情况当某一个需求变更时须在在许多段代码中做一些小修改这也是值得重构的依恋情结对象的核心在于将数据和操作数据的方法打包到一起组件也是类似的。但是有时候一个对象会严重依赖于另一个对象从另一个对象中调用一长串的取值方法甚至于调用其他类的数据比调用这个类本身的数据还要多那么这个类、调用的这些数据、方法都应该是被重构的比如讲这些被调用的方法放到调用的这个类中或者提炼成公共方法数据泥团指一些数据总是成群结队地出现在一起那么这些数据就可能是一个数据泥团。当然这只是数据泥团表现最直接的一个现象, 如果这些数据还满足[天然绑定、同生同灭、同属一个业务实体]这才算数据泥团针对数据泥团可以尝试将这些数据提炼成一个对象比较典型的有分页器的分页数据current、pagiSize、total于是这些数据便组成了一个pagination的对象。将数据泥团组成对象还有一个好处就是精简参数不再需要把这N多个字段一一传过去只需传一个对象就行了。基本类型偏执指将一些具有较强业务属性的数据声明为基本数据类型 比如String phone 13800138000而电话号码就是一个具有较强业务的字段它极有可能需要校验、脱敏、格式化等操作如果使用裸string的话意味着而更推荐的做法是将这些数据定义为一个类或者一个对象比如这里的电话号码可以改写成export type PhoneNumber string; // 2. 把所有和手机号相关的规则打包成一个对象还是你原来的逻辑只是换个写法 export const PhoneNumber { // 核心类型守卫 验证TS会自动缩小类型 isValid: (val: unknown): val is PhoneNumber { return typeof val string /^1[3-9]\d{9}$/.test(val); }, // 你原来的验证方法保留兼容旧代码 validate: (val: string) PhoneNumber.isValid(val), // 脱敏原来可能散在各处现在收过来 mask: (phone: PhoneNumber) phone.replace(/^(\d{3})\d{4}(\d{4})$/, $1****$2), // 格式化原来可能散在各处现在收过来 format: (phone: PhoneNumber) phone.replace(/(\d{3})(\d{4})(\d{4})/, $1 $2 $3), // 核心安全创建函数专门处理接口数据 // 从接口拿到数据后用这个函数转一下非法数据直接返回默认值不会污染业务逻辑 safeCreate: (val: unknown, fallback: PhoneNumber ): PhoneNumber { return PhoneNumber.isValid(val) ? val : fallback; } };这样手机号本身仍是一个字符串可以直接传给接口如果将PhoneNumber声明为一个class类的话就需要使用.value获取值Switch语句在面向对象程序中少用switch多用多态如果只是单一位置或者小范围的条件分支或者使用多态明显不划算的也可以用其他轻量重构手法比如使用明确函数代替控制分支的类型参数平行继承体系平行继承体系是指当你企图为某个继承体系添加一个子类时因为设计的原因你不得不给另一个继承体系也添加一个子类。这也是霰弹式修改的一种造成这种现象的原因是两个体系因为某些原因被强关联了而解决平行继承体系的思路也是对两个体系进行解耦冗余的类不仅仅是class类还包括对象、接口、组件等。代码中声明的每一个类、每一个组件、每一个对象都是需要开发者理解和维护的于是就有了一个理解和维护的代价如果某一个对象提供的价值比不上其所需的代价那么就应该被干掉。注意这里的冗余与否不应该与 对象被使用的的次数挂钩也就是说有些类有些组件可能整个项目只有一个地方在用那也不能直接推断其是冗余的有些类为了在更多地方被使用导致里面加了很多关联性不大的功能那么即使它被多处代码使用也应该被重构夸夸其谈未来性冗余设计“这个功能现在不做后面也会做”、“这个地方这么写就省了后面重构的麻烦”很多项目因为这两句话就无端增加了很多冗余的设计须知大多数高明的设计是并不直观的不直观就意味着开发者在阅读时需要投入的理解成本更高所以冗余设计和冗余的类一样都是根据其代价与价值进行权衡的。而为了未来的某个功能而进行的设计除非能够确定这个设计百分百会在后续的迭代中起到大作用否则轻易不要做这些设计暂时字段项目中总是存在很多的字段它们在项目的生命周期中99%的时间都是空这类字段就称为暂时字段。比如组件中的visible、比如类中的remark等。并不是所有暂时字段都需要重构或者说暂时字段不可能被完全消灭只要有交互就一定有暂时字段。重构也只能尽量减少暂时字段或者是这个字段的生命周期和其主体的生命周期一致。举例说明组件A中有一个弹窗弹窗里是有data1到data3三个状态变量如果直接将这个弹窗的内容写到组件A的代码中组件A就会因此新增四个状态visible、data1、data2、data3如果这些状态变量是根据某个id动态获取的那还需要加一个id的状态那么这几个状态在组件A的声明周期中可能90%的时间都是null这就是典型的暂时字段。 重构思路其中visible和id的状态无可避免肯定是在组件A内部定义但是弹窗内容可以单独声明一个组件B这样data1、data2、data3三个变量以及其动态获取的逻辑都可以放到这个组件B中data1~data3的生命周期和组件B的生命周期基本一致这样暂时字段就只剩下了visible和id两个状态消息链与中间人定义对象A请求对象B对象B请求对象C对象C请求对象D......这就是消息链。实际代码中可能看到的就是一长串的临时变量或者类似a.getB().getC().getD().doSomething()的代码修改了链中的某个对象结构或者对象之间的关联关系发生变化很可能会影响链上的其他对象。封装与委托是面向对象的核心优势当对象A被调用时自己”不干实事“而是”委托转发/外包“给其他对象实现当这样的委托过多时就产生了中间人。消息链和中间人是面对同一种情况链式调用的两个极端一个完全不封装或者封装不足那就是原始的链式调用形成了消息链。另一个是过度封装形成了中间人。两种方式都存在一些缺点消息链耦合于对象导航结构如果其中某个节点不再关联下一个节点那么所有使用这个消息链的代码都需要修改。中间人耦合于中间层接口只要中间人提供的签名没有变那么在调用方就不需要修改但是当中间人委托了太多可能多达几十上百个的方法给其他类后任何一个受委托的类修改了签名都需要修改这个中间人。这是个无限循环的问题重构的艺术就在于从中寻找最佳的平衡点维度消息链 (Message Chains)中间人 (Middle Man)客户端视角知道太多对象关系知道太少真实对象耦合方向耦合于对象导航结构耦合于中间层接口问题本质封装不足封装过度重构手法增加封装减少封装重构结果产生中间人变回消息链案例让我们用一个最常见的 用户 - 订单 - 商品 场景来完整演示这个循环阶段 1消息链坏味道// 客户端代码 const order user.getOrder(orderId); const product order.getProduct(); const productName product.getName(); // 等价于 const productName user.getOrder(orderId).getProduct().getName();问题如果未来订单不再直接持有商品而是通过orderItem关联所有客户端都要修改。阶段 2委托// 在Order类中添加委托方法 class Order { getProductName() { return this.product.getName(); } } // 客户端代码简化为 const order user.getOrder(orderId); const productName order.getProductName();结果消除了消息链但在 Order 类中增加了一个中间人方法。阶段 3继续委托// 在User类中再添加一层委托 class User { getOrderProductName(orderId: number) { return this.getOrder(orderId).getProductName(); } } // 客户端代码进一步简化为 const productName user.getOrderProductName(orderId);结果客户端代码变得极其简洁但 User 类现在变成了一个纯粹的中间人。阶段 4中间人坏味道出现当 User 类中积累了几十个这样的纯转发方法时class User { getOrderProductName(orderId) { ... } getOrderProductPrice(orderId) { ... } getOrderProductStock(orderId) { ... } getOrderShippingAddress(orderId) { ... } getOrderPaymentStatus(orderId) { ... } // 还有20个类似的方法 }问题User 类严重膨胀变成了一个什么都不干的转发器任何 Order 或 Product 的接口变化都会导致 User 类修改。阶段5移除中间人// 删除User类中的所有纯转发方法 // 客户端代码回到阶段2 const order user.getOrder(orderId); const productName order.getProductName(); const productPrice order.getProductPrice();结果消除了中间人坏味道回到了一个更合理的平衡点。这个案例中我们面对的消息链没有修改所以最终阶段5的结果和阶段2的几乎一致以至于看重这个案例就会想为什么到了阶段2还不停为什么要多走阶段3~5这几步但是实际上我们不会一开始就知道这个消息链最终会成长为什么样也许最开始就写成了阶段2的样子但是后续迭代过程中消息链变长了于是需要再次封装委托也许委托得太多了又得解除封装所以说这是一个死循环随着消息链的不断增长而循环衡量与重构技巧重构技巧无论是消息链还是中间人其根本原因仍然是耦合一个是耦合于导航结构一个耦合于中间层面对耦合的重构核心就一句话把耦合放到不易变化的位置并不是所有消息链都需要被重构也不是所有委托都是中间人即便需要重构也只是力争让结果更接近平衡点而不是直接解决所以使用一套决策方案来评估程序是否需要重构评估变化频率如果对象间的导航结构经常变化→ 倾向于使用委托减少客户端修改如果对象的行为接口经常变化→ 倾向于使用消息链减少中间层修改统计客户端数量如果一条消息链被多个客户端使用 → 值得封装成一个委托方法如果一个委托方法只被一个客户端使用 → 应该内联到客户端避免中间人判断方法价值如果委托方法除了转发之外还有额外逻辑如参数验证、错误处理、缓存→ 保留委托如果委托方法只是纯转发没有任何附加值 → 删除委托考虑迪米特法则的边界迪米特法则只和你的直接朋友说话但 直接朋友 的定义是主观的过度遵守迪米特法则会导致系统中充满中间人狎昵关系狎昵关系是指两个类过度亲密简单来说就是一个类依赖了另一个类的私有成员。狎昵关系也是源于耦合导致的问题也就是一个类耦合于另一个类的内部实现细节。按照封装的思想一个封装结果类/组件/模块等对于其他对象来说应该是一个黑盒子只有其内部明确暴露出来的成员是可以被其他对象访问的而狎昵关系则是破坏了这个思想使用了类中没有明确暴露出去的成员举例说明class Order { private status: OrderStatus; // 私有状态 // 公共接口只能通过这个方法获取状态 getStatus(): OrderStatus { return this.status; } } // ✅ 正常调用依赖公共接口 const status order.getStatus(); // ❌ 狎昵关系直接访问私有属性哪怕只是读 const status order.status;异曲同工的类两个函数做同样的事不完美的第三方库第三方库通常只提供了一个通用性对于一个具体的或者比较特殊的业务需求可能就无法完全满足需求但是开发者不可能为了库不满足的那10%甚至2%的需求而自力更生地重新开发一个类或组件更多地可能是在这个第三方库的基础上进行扩展而不当的扩展方式很可能就会滋生出其他问题从而需要重构。第三方库本身不需要重构需要重构的是不当的扩展方式很多人发现不完美的库之后会下意识地想到fork这个库但是这个方案可以说是相当错误的除非真的山穷水尽否则永远不要fork一个第三方库。fork第三方库后将要面对无法升级无法获得BUG修复和新功能需要自己维护这个fork团队其他成员也需要学习你这个fork典型的不当扩展方式不扩展 》重复代码const { data } useQuery([users], fetchUsers); // 这段代码会在20个列表页中完全复制粘贴 const tableData data?.map(user ({ ...user, key: user.id, // Table要求必须有key createdAt: moment(user.createdAt).format(YYYY-MM-DD HH:mm), // 日期格式化 updatedAt: moment(user.updatedAt).format(YYYY-MM-DD HH:mm) })); return Table dataSource{tableData} columns{columns} /;修改原型链、私有成员 》 狎昵关系import { Form } from antd; Form.prototype.getFieldError function(name: string) { return this.getFieldError(name)?.[0]; };这样就造成了狎昵关系而且会影响到全局直接修改库源码 》 霰弹式修改这会导致库无法升级或者库升级后使用的地方各种报错造成霰弹式修改库参数硬编码 》 魔法值案例未二次封装的axios为了适配库而写出又长又复杂的函数 》过长函数案例ECharts的配置项方法面对不完美库的正确思路阅读最新的官方文档查看是否已经提供了所需功能很多时候只是没找到或者版本未更新。如果确实没有那么应该在库和实际业务之间添加一个防腐层扩展层/适配层将库中不好的或者你不想要的设计阻挡在这一层后续无论是库更新或者全局需求更新都只需要修改这一处代码即可数据类指那种声明了一个类或者一个结构里面只有数据而没有实际的行为方法这违反了面向对象中“数据与行为绑定”。实际上像DTO、VO、Response等以及前端大量使用的interface其实都是数据类但是并不是所有数据类都需要重构DTO、VO、Response等本身就是一个存粹数据传输的对象这些类声明为数据类完全没问题而前端由于其本身的特性拿到的数据大都就是一个JSON对象或者本身就是一个静态数据使用interface声明数据类也没有问题。所以判断一个数据类是否需要重构可以从三个方面考虑这个类有自己的业务规则和生命周期吗有超过 3 个地方在操作这个类的数据吗这些操作逻辑如果修改需要改多个地方吗如果这三点同时满足那么就是需要被重构的被拒绝的遗赠被拒绝的遗赠refused bequest是指子类从父类继承了一个方法但是并没有使用这个方法。这同样是一个需要权衡的问题而且90%的情况下都不值得重构案例class Animal { public void eat() { /* ... */ } public void walk() { System.out.println(动物用腿走路); } } class Jellyfish extends Animal { public void walk() { /* ... */ } } class Dog extends Animal { public void bark() { /* ... */ } } class Fish extends Animal { public void swim() { /* ... */ } }这时Fish继承于Animal也就继承了其walk方法但是如果调用该方法Fish.walk()就有问题因为鱼不会走路。这是一个最初设计的问题Animal中的方法至少应该确保所有Animal调用都没有问题但是这时完全没有必要重构直接在Fish中重写walk即可class Fish extends Animal { public void swim() { /* ... */ }; public void walk() { System.out.println(鱼不会走路); } }如果此时又新增了一个方法sleepclass Animal { public void eat() { /* ... */ } public void walk() { System.out.println(动物用腿走路); } public void sleep() {动物需要睡觉}; }这时又出现了一个问题Jellyfish水母不会睡觉但是也继承到了新增的sleep方法这同样可以靠重写解决但暴露了一个问题每次新增方法时都需要检查所有子类这看似是一个比较大的值得重构的问题但是其实不一定还是需要衡量Animal类是否频繁修改子类是否很多。请牢记重构的核心原则如果说在衡量过后仍觉得重构更好那么重构方式如下传统的重构方式面对这个问题传统的重构方式就就是从问题的源头解决问题既然一切的问题都是源于Animal中被声明了不应属于Animal的方法那么就将这些方法提取出来比如额外声明一个LandAnimal的类并将walk方法放到这个类中class Animal { public void eat() { /* ... */ }; } class LandAnimal extends Animal { public void walk() { System.out.println(陆生动物用腿走路); } } class Dog extends LandAnimal { public void bark() { /* ... */ }; } class Fish extends Animal { public void swim() { /* ... */ }; }这样就解决了问题并且足够简单直接但是很容易产生其他问题类爆炸、复杂继承关系推荐的方法委托不再使用继承关系而是使用委托class Animal { public void eat() { /* ... */ }; public void walk() { System.out.println(动物用腿走路); } } class Dog { private animal new Animal(); public void walk() { animal.walk(); } public void bark() { /* ... */ }; } class Fish { public void swim() { /* ... */ }; }过多的注释虽然《重构》作者用代码解释一切无需注释的思想在汉语环境下行不通但是不得不肯定过多的注释仍然是个问题如果在几百行的注释中间夹着一行有用的注释谁也不能保证自己能够一眼发现它第三章 构建测试体系虽然《重构》中本章节的内容都是面向java或者面向后端单体应用并且其中提到的JUnit测试工具等都已经过时但是其核心思想却是不分前后端也不分时代一直适用的。核心思想重构的前提必须要有测试在重构前搭建好测试的代码这样才能避免是在重构而不是在引入新的BUG测试是自动验证的测试程序应该直接告诉你通过或者不通过而不是由人工判断测试应该自动化所有测试程序应该运行一键运行测试应该是细粒度的一个测试程序只测试一个小功能这样测试不通过时才能快速找到问题所在重构的标准流程为你要重构的代码写测试运行测试确保它们全部通过小步重构每改一行代码就运行一次测试所有重构完成后再运行一次所有测试总结《重构》其实总共十五章但是全书灵魂就是在第三章代码的坏味道对应笔记第二章可能需要重构的代码后续章节则介绍了各个代码坏味道的重构手法然而由于时代变化其中很多方法都已经过时而且部分重构方法我已在笔记第二章进行了简单介绍可以说至此已经涵盖了全书80%的价值感兴趣的仍可以去拜读原著感受下那个时代的开发。
《重构:改善既有代码的设计》阅读笔记
发布时间:2026/5/27 5:50:22
重构改善既有代码的设计序言原著作者和我——也就是写这篇阅读笔记的人的观点并不完全相同。比如作者认为只要函数名取得好就不需要注释甚至以此认为只要是需要注释的代码就可以封装成一个函数哪怕只有一行代码我却认为英文的函数名再好也不如一行中文注释来得直接间接层过多反而会导致可读性降低。因此此笔记中含有部分观点是书中观点与我个人开发经验结合而来如有需要可阅读原著。原著是基于Java后端的强OO场景而如果是对于其他开发场景下比如前端书中很多理念不必教条式搬抄比如原著中认为基本类型偏执和重复代码、过大的类等一样具有重构的必要性但是在前端开发中至少在我的前端开发生涯中很少直接修改类的代码面对基本类型偏执带来的问题也有其他办法解决而不是直接解决基本类型偏执重构前须知如果你给代码新增特性时感受到困难就应该重构了重构前应该检查你是否有一个可靠的测试机制这些机制必须有自我检验能力是否有自我检测能力是指测试机制自己判断实际输出与预期输出是否一致一致则表明通过不一致则列出问题清单这些测试机制大概不会影响你重构但能够减少BUG率重构的几种常用手法抽离方法将代码中一段逻辑抽离成单独的方法注意参数和返回值设计移动方法将方法移动到更合适的位置移到更贴合的类下或是更匹配的目录下多态替换条件属性私有化状态/策略模式替换类型如果有字段是表明实例是某种类型时并且在不同类型下会有不同逻辑时可以考虑使用状态模式或者策略模式重构技术是以微小的步伐改变程序每一步都应该安排测试如果出现了问题你可以很快排查出来从复杂冗余的代码中将一个个逻辑提取出来测试保证和提取之前一致对提取出来的代码进行微小的修改可以先从修改变量名开始测试确保没有破坏任何东西重复3到4步对提取出来的逻辑进行修改测试微小修改步骤可以包括修改变量名逻辑在提取前和提取后命名可能会不同移除临时变量Replace Temp with Query声明之后就没修改过的变量或者修改过一次但没有设置初值的变量临时变量可以直接用查询查询函数替换当然这可能会导致一些额外的性能开支第一章 重构原则核心原则重构永远是一个成本收益的权衡不是一个绝对的命令只有当维护重写方法的成本超过了重构的成本时才值得重构下定义重构指在不改变软件可观测行为的前提下提高代码可读性和可维护性的行为重构的目的代码更容易被理解并且后续的修改成本更低。既不是提升系统效率也不是减少代码量容易混淆的是优化但是优化的目的是提高组件的执行速度而这往往会提高抽象程度导致代码更不容易被理解重构的难题数据库大多数商用程序都与其背后的数据库结构紧密耦合即使再小心的将系统分层数据库结构的改变还是需要迁移所有数据。对于非对象数据库可以在对象模型和数据库模型之间使用一个分隔层当某个模型发生变化时无需修改另一个模型只需要修改上述的分隔层就行对于对象数据库某些数据库提供自动迁移功能。修改公共组件、方法、接口对于这些公共的对象的修改要么确保本次修改是向下兼容的也就是修改之后的对象仍能被其调用者正常调用且逻辑正常要么确保所有调用者都在你的控制之下也就是说你能够找出所有调用了这些对象的代码并且能够修改。通常来说项目内地公共对象都是可以被找出并修改的但考虑到团队代码所有权政策或者会被其他项目使用那么就需要注意此项。设计层面的改动如果前期架构或者进行高层设计时没有考虑到某些需求后续想要改动这些牵一发而动全身的设计将是一件非常困难的事情这也说明了架构与高层设计的重要性。何时不该重构重写更简单如果将某段代码重写一遍比重构更简单那么就应该重写而不是重构重写的一个重要判断就是现有代码根本不能正常稳定运行大多数情况下都会出现BUG。重构之前应该确保大部分情况下都能正常工作临近期限重构的一个最重要的好处是后续修改起来更加简单但如果已经临近期限那么这个好处只能在期限过后体现出来为此浪费一定的时间是不理智的。如果临近期限时想要重构却没有时间说明你早就该重构了重构与设计重构和设计互补事先做好设计能够节省返工的高昂成本但是想要一步到位的找出最佳的解决方案需要极深的功力如果功力不足则必然会有各种的小漏洞而重构正好可以解决这些漏洞。重构的另一个好处就是不必在设计时承受太大的压力有一些小问题也可以在重构时修改只要确保设计是合理的就行。但重构仍不可能完全取代设计重构只是多了一个方法来应付变化带来的风险开发者仍需在设计时思考潜在的变化仍然需要一个灵活的设计只是降低了设计的难度将设计的马拉松变成了设计与重构的接力赛需要平衡设计部分和重构部分的难度不至于设计太过简单导致重构困难就行。重构与性能不少人都会混淆重构和优化经常有人问出这个问题“你这样重构能让系统提升多少效率”。然而在重构时为了使程序易于理解开发者常会做出一些影响性能的事情。这是一个非常重要的问题开发者不应该在性能和可维护性上做选择而是对两者做权衡。另外重构本身虽然可能使软件更慢但是也使得软件的性能优化更容易。之所以让人容易混淆这两个概念或许是因为很多时候局部的优化代码以及不能满足效率要求时往往要对整个模块甚至整个软件进行重构。如果开发的系统对性能要求极高比如实时系统那么在设计时就应该做好预算给每个模块甚至每个功能分配一定的资源开发者在开发时绝对不允许超过自己模块的预算。对于性能要求不是特别严重的系统可以先找出系统中大半的时间都消耗在了什么地方然后着重优化这些功能。有数据显示一个软件80%的时间都消耗在20%的代码上所以首先开发者需要有一个度量工具来监控程序的运行找出哪些吃性能的功能点然后优化他们再次度量如果没能提高性能应该撤销本次修改如此重复。第二章 可能需要重构的代码重复代码最简单的情况就是同一个类或同一个组件内部的两个方法中使用了相同的一段表达式此时只需要将这段表达式提炼成一个方法然后引用即可。最常见的情况是两个互为兄弟的子类或同一个功能模块下的子组件包含相同的表达式。那么需要将提炼出来的代码推入超类中或者放到模块下的一个utils文件中。如果两段代码只是相似不是相同仍可以使用模板模式将其相似部分提炼出来封装成一个类或一个方法在使用时使用子类修改或对方法结果或参数进行修改。如果两个互不相干的类或者组件存在相同的代码需要非常明确这两个地方用的是同样的逻辑并且一个地方修改另一个地方也要跟着修改时才将其提炼到一个类中或提炼成一个公共方法否则最好保持原样。如果开发者仅仅只能确定当前两个地方用的是相同的逻辑贸然将其提炼成公共方法反而会提高代码耦合度。过长函数间接层带来的全部好处——解释能力、共享能力、选择能力全都是由小型函数支持的。因此开发者应该积极地将函数分解成小型函数当然还是要权衡添加这个间接层是否值得。需不需要分解并不直接取决于函数的长度而在于注释或者函数名解释“做什么”和代码上的“如何做”之间的语义距离。有一个简单快速的判断方法寻找注释。如果函数内的某一行注释用于解释下方的多行代码那么或许就可以将其提炼成一个小型函数。过大的类如果想利用单个类做太多是的事情就会出现太多的示例变量完全可以将其中彼此相关的一些变量整合到一个子类中。比如一个常见的类User其中可能包含了用户名、账号、密码、身份证号、姓名、公司、职业等许多变量可以这些变量封装到对应的子类或者账号信息、身份信息、工作信息三个类中发散式修改可维护性要求软件是易于修改的需求变更也是软件开发中常见的事。开发者希望的是在某次需求变更时只需要跳到某一处关键代码修改这一段代码就能完成而不是改变了某一段代码然后再去修改依赖于这段代码的另外两段代码再去修改依赖于这两段代码的其他代码。如果出现了这种发散式修改的代码那么这段代码乃至于依赖这段代码的其他部分甚至整个系统都至少是值得被重构的。当然由需求变更引发的修改方向是不可控的即使你这次重构好了也无法保证下次需求变更是否又要重构。所以在设计时、开发时、重构时就应该考虑到系统或者模块可能的变更方向在确认需求变更时也要跟客户或领导确认变更的成本和代价当然这就不在重构的内容中了。霰弹时修改还有一种典型的情况当某一个需求变更时须在在许多段代码中做一些小修改这也是值得重构的依恋情结对象的核心在于将数据和操作数据的方法打包到一起组件也是类似的。但是有时候一个对象会严重依赖于另一个对象从另一个对象中调用一长串的取值方法甚至于调用其他类的数据比调用这个类本身的数据还要多那么这个类、调用的这些数据、方法都应该是被重构的比如讲这些被调用的方法放到调用的这个类中或者提炼成公共方法数据泥团指一些数据总是成群结队地出现在一起那么这些数据就可能是一个数据泥团。当然这只是数据泥团表现最直接的一个现象, 如果这些数据还满足[天然绑定、同生同灭、同属一个业务实体]这才算数据泥团针对数据泥团可以尝试将这些数据提炼成一个对象比较典型的有分页器的分页数据current、pagiSize、total于是这些数据便组成了一个pagination的对象。将数据泥团组成对象还有一个好处就是精简参数不再需要把这N多个字段一一传过去只需传一个对象就行了。基本类型偏执指将一些具有较强业务属性的数据声明为基本数据类型 比如String phone 13800138000而电话号码就是一个具有较强业务的字段它极有可能需要校验、脱敏、格式化等操作如果使用裸string的话意味着而更推荐的做法是将这些数据定义为一个类或者一个对象比如这里的电话号码可以改写成export type PhoneNumber string; // 2. 把所有和手机号相关的规则打包成一个对象还是你原来的逻辑只是换个写法 export const PhoneNumber { // 核心类型守卫 验证TS会自动缩小类型 isValid: (val: unknown): val is PhoneNumber { return typeof val string /^1[3-9]\d{9}$/.test(val); }, // 你原来的验证方法保留兼容旧代码 validate: (val: string) PhoneNumber.isValid(val), // 脱敏原来可能散在各处现在收过来 mask: (phone: PhoneNumber) phone.replace(/^(\d{3})\d{4}(\d{4})$/, $1****$2), // 格式化原来可能散在各处现在收过来 format: (phone: PhoneNumber) phone.replace(/(\d{3})(\d{4})(\d{4})/, $1 $2 $3), // 核心安全创建函数专门处理接口数据 // 从接口拿到数据后用这个函数转一下非法数据直接返回默认值不会污染业务逻辑 safeCreate: (val: unknown, fallback: PhoneNumber ): PhoneNumber { return PhoneNumber.isValid(val) ? val : fallback; } };这样手机号本身仍是一个字符串可以直接传给接口如果将PhoneNumber声明为一个class类的话就需要使用.value获取值Switch语句在面向对象程序中少用switch多用多态如果只是单一位置或者小范围的条件分支或者使用多态明显不划算的也可以用其他轻量重构手法比如使用明确函数代替控制分支的类型参数平行继承体系平行继承体系是指当你企图为某个继承体系添加一个子类时因为设计的原因你不得不给另一个继承体系也添加一个子类。这也是霰弹式修改的一种造成这种现象的原因是两个体系因为某些原因被强关联了而解决平行继承体系的思路也是对两个体系进行解耦冗余的类不仅仅是class类还包括对象、接口、组件等。代码中声明的每一个类、每一个组件、每一个对象都是需要开发者理解和维护的于是就有了一个理解和维护的代价如果某一个对象提供的价值比不上其所需的代价那么就应该被干掉。注意这里的冗余与否不应该与 对象被使用的的次数挂钩也就是说有些类有些组件可能整个项目只有一个地方在用那也不能直接推断其是冗余的有些类为了在更多地方被使用导致里面加了很多关联性不大的功能那么即使它被多处代码使用也应该被重构夸夸其谈未来性冗余设计“这个功能现在不做后面也会做”、“这个地方这么写就省了后面重构的麻烦”很多项目因为这两句话就无端增加了很多冗余的设计须知大多数高明的设计是并不直观的不直观就意味着开发者在阅读时需要投入的理解成本更高所以冗余设计和冗余的类一样都是根据其代价与价值进行权衡的。而为了未来的某个功能而进行的设计除非能够确定这个设计百分百会在后续的迭代中起到大作用否则轻易不要做这些设计暂时字段项目中总是存在很多的字段它们在项目的生命周期中99%的时间都是空这类字段就称为暂时字段。比如组件中的visible、比如类中的remark等。并不是所有暂时字段都需要重构或者说暂时字段不可能被完全消灭只要有交互就一定有暂时字段。重构也只能尽量减少暂时字段或者是这个字段的生命周期和其主体的生命周期一致。举例说明组件A中有一个弹窗弹窗里是有data1到data3三个状态变量如果直接将这个弹窗的内容写到组件A的代码中组件A就会因此新增四个状态visible、data1、data2、data3如果这些状态变量是根据某个id动态获取的那还需要加一个id的状态那么这几个状态在组件A的声明周期中可能90%的时间都是null这就是典型的暂时字段。 重构思路其中visible和id的状态无可避免肯定是在组件A内部定义但是弹窗内容可以单独声明一个组件B这样data1、data2、data3三个变量以及其动态获取的逻辑都可以放到这个组件B中data1~data3的生命周期和组件B的生命周期基本一致这样暂时字段就只剩下了visible和id两个状态消息链与中间人定义对象A请求对象B对象B请求对象C对象C请求对象D......这就是消息链。实际代码中可能看到的就是一长串的临时变量或者类似a.getB().getC().getD().doSomething()的代码修改了链中的某个对象结构或者对象之间的关联关系发生变化很可能会影响链上的其他对象。封装与委托是面向对象的核心优势当对象A被调用时自己”不干实事“而是”委托转发/外包“给其他对象实现当这样的委托过多时就产生了中间人。消息链和中间人是面对同一种情况链式调用的两个极端一个完全不封装或者封装不足那就是原始的链式调用形成了消息链。另一个是过度封装形成了中间人。两种方式都存在一些缺点消息链耦合于对象导航结构如果其中某个节点不再关联下一个节点那么所有使用这个消息链的代码都需要修改。中间人耦合于中间层接口只要中间人提供的签名没有变那么在调用方就不需要修改但是当中间人委托了太多可能多达几十上百个的方法给其他类后任何一个受委托的类修改了签名都需要修改这个中间人。这是个无限循环的问题重构的艺术就在于从中寻找最佳的平衡点维度消息链 (Message Chains)中间人 (Middle Man)客户端视角知道太多对象关系知道太少真实对象耦合方向耦合于对象导航结构耦合于中间层接口问题本质封装不足封装过度重构手法增加封装减少封装重构结果产生中间人变回消息链案例让我们用一个最常见的 用户 - 订单 - 商品 场景来完整演示这个循环阶段 1消息链坏味道// 客户端代码 const order user.getOrder(orderId); const product order.getProduct(); const productName product.getName(); // 等价于 const productName user.getOrder(orderId).getProduct().getName();问题如果未来订单不再直接持有商品而是通过orderItem关联所有客户端都要修改。阶段 2委托// 在Order类中添加委托方法 class Order { getProductName() { return this.product.getName(); } } // 客户端代码简化为 const order user.getOrder(orderId); const productName order.getProductName();结果消除了消息链但在 Order 类中增加了一个中间人方法。阶段 3继续委托// 在User类中再添加一层委托 class User { getOrderProductName(orderId: number) { return this.getOrder(orderId).getProductName(); } } // 客户端代码进一步简化为 const productName user.getOrderProductName(orderId);结果客户端代码变得极其简洁但 User 类现在变成了一个纯粹的中间人。阶段 4中间人坏味道出现当 User 类中积累了几十个这样的纯转发方法时class User { getOrderProductName(orderId) { ... } getOrderProductPrice(orderId) { ... } getOrderProductStock(orderId) { ... } getOrderShippingAddress(orderId) { ... } getOrderPaymentStatus(orderId) { ... } // 还有20个类似的方法 }问题User 类严重膨胀变成了一个什么都不干的转发器任何 Order 或 Product 的接口变化都会导致 User 类修改。阶段5移除中间人// 删除User类中的所有纯转发方法 // 客户端代码回到阶段2 const order user.getOrder(orderId); const productName order.getProductName(); const productPrice order.getProductPrice();结果消除了中间人坏味道回到了一个更合理的平衡点。这个案例中我们面对的消息链没有修改所以最终阶段5的结果和阶段2的几乎一致以至于看重这个案例就会想为什么到了阶段2还不停为什么要多走阶段3~5这几步但是实际上我们不会一开始就知道这个消息链最终会成长为什么样也许最开始就写成了阶段2的样子但是后续迭代过程中消息链变长了于是需要再次封装委托也许委托得太多了又得解除封装所以说这是一个死循环随着消息链的不断增长而循环衡量与重构技巧重构技巧无论是消息链还是中间人其根本原因仍然是耦合一个是耦合于导航结构一个耦合于中间层面对耦合的重构核心就一句话把耦合放到不易变化的位置并不是所有消息链都需要被重构也不是所有委托都是中间人即便需要重构也只是力争让结果更接近平衡点而不是直接解决所以使用一套决策方案来评估程序是否需要重构评估变化频率如果对象间的导航结构经常变化→ 倾向于使用委托减少客户端修改如果对象的行为接口经常变化→ 倾向于使用消息链减少中间层修改统计客户端数量如果一条消息链被多个客户端使用 → 值得封装成一个委托方法如果一个委托方法只被一个客户端使用 → 应该内联到客户端避免中间人判断方法价值如果委托方法除了转发之外还有额外逻辑如参数验证、错误处理、缓存→ 保留委托如果委托方法只是纯转发没有任何附加值 → 删除委托考虑迪米特法则的边界迪米特法则只和你的直接朋友说话但 直接朋友 的定义是主观的过度遵守迪米特法则会导致系统中充满中间人狎昵关系狎昵关系是指两个类过度亲密简单来说就是一个类依赖了另一个类的私有成员。狎昵关系也是源于耦合导致的问题也就是一个类耦合于另一个类的内部实现细节。按照封装的思想一个封装结果类/组件/模块等对于其他对象来说应该是一个黑盒子只有其内部明确暴露出来的成员是可以被其他对象访问的而狎昵关系则是破坏了这个思想使用了类中没有明确暴露出去的成员举例说明class Order { private status: OrderStatus; // 私有状态 // 公共接口只能通过这个方法获取状态 getStatus(): OrderStatus { return this.status; } } // ✅ 正常调用依赖公共接口 const status order.getStatus(); // ❌ 狎昵关系直接访问私有属性哪怕只是读 const status order.status;异曲同工的类两个函数做同样的事不完美的第三方库第三方库通常只提供了一个通用性对于一个具体的或者比较特殊的业务需求可能就无法完全满足需求但是开发者不可能为了库不满足的那10%甚至2%的需求而自力更生地重新开发一个类或组件更多地可能是在这个第三方库的基础上进行扩展而不当的扩展方式很可能就会滋生出其他问题从而需要重构。第三方库本身不需要重构需要重构的是不当的扩展方式很多人发现不完美的库之后会下意识地想到fork这个库但是这个方案可以说是相当错误的除非真的山穷水尽否则永远不要fork一个第三方库。fork第三方库后将要面对无法升级无法获得BUG修复和新功能需要自己维护这个fork团队其他成员也需要学习你这个fork典型的不当扩展方式不扩展 》重复代码const { data } useQuery([users], fetchUsers); // 这段代码会在20个列表页中完全复制粘贴 const tableData data?.map(user ({ ...user, key: user.id, // Table要求必须有key createdAt: moment(user.createdAt).format(YYYY-MM-DD HH:mm), // 日期格式化 updatedAt: moment(user.updatedAt).format(YYYY-MM-DD HH:mm) })); return Table dataSource{tableData} columns{columns} /;修改原型链、私有成员 》 狎昵关系import { Form } from antd; Form.prototype.getFieldError function(name: string) { return this.getFieldError(name)?.[0]; };这样就造成了狎昵关系而且会影响到全局直接修改库源码 》 霰弹式修改这会导致库无法升级或者库升级后使用的地方各种报错造成霰弹式修改库参数硬编码 》 魔法值案例未二次封装的axios为了适配库而写出又长又复杂的函数 》过长函数案例ECharts的配置项方法面对不完美库的正确思路阅读最新的官方文档查看是否已经提供了所需功能很多时候只是没找到或者版本未更新。如果确实没有那么应该在库和实际业务之间添加一个防腐层扩展层/适配层将库中不好的或者你不想要的设计阻挡在这一层后续无论是库更新或者全局需求更新都只需要修改这一处代码即可数据类指那种声明了一个类或者一个结构里面只有数据而没有实际的行为方法这违反了面向对象中“数据与行为绑定”。实际上像DTO、VO、Response等以及前端大量使用的interface其实都是数据类但是并不是所有数据类都需要重构DTO、VO、Response等本身就是一个存粹数据传输的对象这些类声明为数据类完全没问题而前端由于其本身的特性拿到的数据大都就是一个JSON对象或者本身就是一个静态数据使用interface声明数据类也没有问题。所以判断一个数据类是否需要重构可以从三个方面考虑这个类有自己的业务规则和生命周期吗有超过 3 个地方在操作这个类的数据吗这些操作逻辑如果修改需要改多个地方吗如果这三点同时满足那么就是需要被重构的被拒绝的遗赠被拒绝的遗赠refused bequest是指子类从父类继承了一个方法但是并没有使用这个方法。这同样是一个需要权衡的问题而且90%的情况下都不值得重构案例class Animal { public void eat() { /* ... */ } public void walk() { System.out.println(动物用腿走路); } } class Jellyfish extends Animal { public void walk() { /* ... */ } } class Dog extends Animal { public void bark() { /* ... */ } } class Fish extends Animal { public void swim() { /* ... */ } }这时Fish继承于Animal也就继承了其walk方法但是如果调用该方法Fish.walk()就有问题因为鱼不会走路。这是一个最初设计的问题Animal中的方法至少应该确保所有Animal调用都没有问题但是这时完全没有必要重构直接在Fish中重写walk即可class Fish extends Animal { public void swim() { /* ... */ }; public void walk() { System.out.println(鱼不会走路); } }如果此时又新增了一个方法sleepclass Animal { public void eat() { /* ... */ } public void walk() { System.out.println(动物用腿走路); } public void sleep() {动物需要睡觉}; }这时又出现了一个问题Jellyfish水母不会睡觉但是也继承到了新增的sleep方法这同样可以靠重写解决但暴露了一个问题每次新增方法时都需要检查所有子类这看似是一个比较大的值得重构的问题但是其实不一定还是需要衡量Animal类是否频繁修改子类是否很多。请牢记重构的核心原则如果说在衡量过后仍觉得重构更好那么重构方式如下传统的重构方式面对这个问题传统的重构方式就就是从问题的源头解决问题既然一切的问题都是源于Animal中被声明了不应属于Animal的方法那么就将这些方法提取出来比如额外声明一个LandAnimal的类并将walk方法放到这个类中class Animal { public void eat() { /* ... */ }; } class LandAnimal extends Animal { public void walk() { System.out.println(陆生动物用腿走路); } } class Dog extends LandAnimal { public void bark() { /* ... */ }; } class Fish extends Animal { public void swim() { /* ... */ }; }这样就解决了问题并且足够简单直接但是很容易产生其他问题类爆炸、复杂继承关系推荐的方法委托不再使用继承关系而是使用委托class Animal { public void eat() { /* ... */ }; public void walk() { System.out.println(动物用腿走路); } } class Dog { private animal new Animal(); public void walk() { animal.walk(); } public void bark() { /* ... */ }; } class Fish { public void swim() { /* ... */ }; }过多的注释虽然《重构》作者用代码解释一切无需注释的思想在汉语环境下行不通但是不得不肯定过多的注释仍然是个问题如果在几百行的注释中间夹着一行有用的注释谁也不能保证自己能够一眼发现它第三章 构建测试体系虽然《重构》中本章节的内容都是面向java或者面向后端单体应用并且其中提到的JUnit测试工具等都已经过时但是其核心思想却是不分前后端也不分时代一直适用的。核心思想重构的前提必须要有测试在重构前搭建好测试的代码这样才能避免是在重构而不是在引入新的BUG测试是自动验证的测试程序应该直接告诉你通过或者不通过而不是由人工判断测试应该自动化所有测试程序应该运行一键运行测试应该是细粒度的一个测试程序只测试一个小功能这样测试不通过时才能快速找到问题所在重构的标准流程为你要重构的代码写测试运行测试确保它们全部通过小步重构每改一行代码就运行一次测试所有重构完成后再运行一次所有测试总结《重构》其实总共十五章但是全书灵魂就是在第三章代码的坏味道对应笔记第二章可能需要重构的代码后续章节则介绍了各个代码坏味道的重构手法然而由于时代变化其中很多方法都已经过时而且部分重构方法我已在笔记第二章进行了简单介绍可以说至此已经涵盖了全书80%的价值感兴趣的仍可以去拜读原著感受下那个时代的开发。