重构之改善既有代码的设计(一)

重构是对软件内部结构的调整,旨在提高可理解性并降低修改成本。在添加功能、修复错误或代码审查时需要重构。坏味道如神秘命名、重复代码、过长函数等是重构的信号。重构有助于提高代码质量,节省调试时间,减少变更风险,并提升团队效率。
摘要由CSDN通过智能技术生成

重构之改善既有代码的设计(二)

1.1 何为重构,为何重构

第一个定义是名词形式:

重构(名词):对软件内部结构的一种调整,目的是在不改变「软件可察行为」前提下,提高其可理解性,降低修改成本。

「重构」的另一个用法是动词形式:

重构(动词):使用一系列重构准则手法,在不改变「软件可察行为」前提 下,调整其结构。

  1. 改进软件设计,使软件更易被理解。

    ps:重构是一种经济适用行为,而非道德使然,如果它不能让我们更快更好的开发,那么它是毫无意义。

  2. 重构对个体程序员的意义是提高ROI。

    1. 更快速的定位问题,节省调试时间。
    2. 最小化变更风险,提高代码质量,减少修复事故的时间。
    3. 得到程序员同行的认可,更好的发展机会。
  3. 重构对整个研发团队的意义是战斗力的提升。

image-20230202212809100

1.2 什么时候需要重构?

三次法则;

添加功能更时重构;

修补错误时重构;

复审代码时重构;

  1. Code review : 在给别人code review时嗅出坏味道,在不失礼貌的前提下提出建议。
  2. 每次 commit 代码时: 每一次经你之手提交的代码都应该比之前更加干净。
  3. 当你接手一个异常难读的项目时: 说服项目组将重构作为一项需求任务来做。
  4. 当迭代效率低于预期时: 将重构当作一个项任务专门来做,必要的时候停下来迭代需求。

重构过程中关于两顶帽子的比喻:

使用重构技术开发软件时,你把自己的时间分配给两种截然不同的行为:「添加新功能」和「重构」。

添加新功能时,你不应该修改既有代码,只管添加新功能。重构时你就不能再添加功能,只管改进程序结构。

软件开发过程中,你可能会发现自己经常变换帽子。首先你会尝试添加新功能,然后觉得把程序结构改一下,功能的添加会容易得多。于是你换一顶帽 子,做一会儿重构工作。接着重复该过程。整个过程或许只花十分钟,但无论何时你都应该清楚自己戴的是哪一顶帽子。

二 代码/架构的坏味道

何时重构?书中告诉了你一些迹象,它会指出「这里有一个可使用重构解决的问题」。

详细可参考:导图,篇幅所限,下面举例了其中15条,重点在介绍这种“坏味道”,相应的应对方法可以参考:https://www.itzhai.com/articles/bad-code-small.html

2.1 Mysterious Name(神秘命名)

好的名字能节省未来用在猜谜上的大把时间。

image-20230201100250812

源代码:

function getPrice(order) {
  const a = order.quantity * order.itemPrice
  const b = Math.max(0, order.quantity - 500) * order.itemPrice * 0.05
  const c = Math.min(basePrice * 0.1, 100)
  return a - b + c
}

改进:

function getPrice(order) {
  // 获取基础价格
  const basePrice = order.quantity * order.itemPrice
  // 获取折扣
  const quantityDiscount = Math.max(0, order.quantity - 500) * order.itemPrice * 0.05
  // 获取运费
  const shipping = Math.min(basePrice * 0.1, 100)
  // 计算价格
  return basePrice - quantityDiscount + shipping
}

2.2 Duplicated Code(重复的代码)

“如果你在一个以上的地点看到相同的代码结构,那么可以肯定:设法将它们合而为一,程序会变得更好。一旦有重复代码存在,阅读这些重复的代码时你就必须加倍仔细,留意其间细微的差异。如果要修改重复代码,你必须找出所有的副本来修改。”

2.3 Long Method(过长函数)

“据我们的经验,活得最长、最好的程序,其中的函数都比较短。初次接触到这种代码库的程序员常常会觉得“计算都没有发生”——程序里满是无穷无尽的委托调用。但和这样的程序共处几年之后,你就会明白这些小函数的价值所在。间接性带来的好处——更好的阐释力、更易于分享、更多的选择——都是由小函数来支持的。”

2.4 Large Class(过大类)

image-20230131175055336

2.5 Long Parameter List(过长参数列)

“刚开始学习编程的时候,老师教我们:把函数所需的所有东西都以参数的形式传递进去。这可以理解,因为除此之外就只能选择全局数据,而全局数据很快就会变成邪恶的东西。但过长的参数列表本身也经常令人迷惑。”

public class LongParameterListExample {
    public void processData(String name, String address, int age, String gender, String occupation, String phoneNumber, String email) {
        // process data here
    }
}

上面的代码定义了一个方法processData,它有7个参数。在这个例子中,我们可以发现参数列非常长,不利于程序的可读性和可维护性。这是一个典型的过长参数列的例子。

2.6 Divergent Change(发散式变化)

指一个类受多种变化的影响。

你发现你想要修改一个函数,却必须要同时修改许多不相关的函数。例如,当你想要添加一个新的产品类型时,你需要同步修改对产品进行查找、显示、排序的函数。

2.7 Shotgun Surgery(霰弹式修改)

多种变化引发多个类相应的修改。

任何修改都需要在许多不同类上做小幅度修改。

可能原因:一个单一的职责被拆分成大量的类。

image-20230131193630774

注意霰弹式修改 与 发散式变化 区别 : 发散式变化是在一个类受多种变化影响, 每种变化修改的方法不同, 霰弹式修改是 一种变化引发修改多个类中的代码。

2.8 Feature Envy(依恋情结)

函数大量地使用了另外类的数据。这种情况下最好将此函数移动到那个类中。

函数对某个class的兴趣高过对自己所处之 class的兴趣。无数次经验里,我们看到某个函数为了计算某值,从另一个对象那儿调用几乎一半以上的取值函数。 影响:数据和行为不在一处,修改不可控。 解决方案:让数据和行为在一起,通过 Extract Method(提炼函数)和Move Method(搬移函数)的方法来处理,这函数到该去的地方。

2.9 Data Clumps(数据泥团)

数据泥团指的是经常一起出现的数据,比如每个方法的参数几乎相同,处理方式与过长参数列的处理方式相同,用Introduce Parameter Object(引入参数对象)将参数封装成对象。

2.10 Primitive Obsession(基本型别偏执)

写代码时总喜欢用基本类型来当参数,而不喜欢用对象。当要修改需求和扩展功能时,复杂度就增加了。

2.11 Lazy Element(冗赘的元素)

去除多层不必要的包装。

如:方法a中包的是b,b包的是c,c包的是d。但是bc只是基于某种考虑的纯粹包装,而从未有其他变化,这时可以让a直接包d,bc就去掉吧。

class Customer {
  private String name;
  private String address;
  private String city;
  private String state;
  private String zip;
  private String phone;
  private String email;

  public Customer(String name, String address, String city, String state, 
                  String zip, String phone, String email) {
    this.name = name;
    this.address = address;
    this.city = city;
    this.state = state;
    this.zip = zip;
    this.phone = phone;
    this.email = email;
  }

  public String getEmail() {
    return email;
  }
}

class Order {
  private Customer customer;
  private int total;

  public Order(Customer customer, int total) {
    this.customer = customer;
    this.total = total;
  }

  public String getCustomerEmail() {
    return customer.getEmail();
  }
}

在上面的代码中,我们定义了一个Order类和一个Customer类,其中Order类知道Customer类的详细信息,但仅使用Customer的电子邮件。这是一个冗赘的元素,因为只需要知道用户的电子邮件,但是却存储了大量未使用的数据。在这种情况下,重构可能会改为:

class CustomerEmail {
  private String email;

  public CustomerEmail(String email) {
    this.email = email;
  }

  public String getEmail() {
    return email;
  }
}

class Order {
  private CustomerEmail customerEmail;
  private int total;

  public Order(CustomerEmail customerEmail, int total) {
    this.customerEmail = customerEmail;
    this.total = total;
  }

  public String getCustomerEmail() {
    return customerEmail.getEmail();
  }
}

现在,我们只存储所需的信息,而不是冗赘的信息,这样可以使代码更简洁。

2.12 Message Chains(过长的消息链)

向一个对象请求另一个对象,然后再向后者请求另一个对象,然后再请求另一个对象……

未充分的考虑数据结构的读取场景,导致在需要使用某些数据的时候无法简单的获得其引用,或者为了使用某个字段,需要了解一堆中间封装的数据结构。

a.b.c.d.e()

2.13 Middle Man(中间人)

对象的基本特征之一就是封装——对外部世界隐藏其内部细节。封装往往伴随着委托。比如,你问主管是否有时间参加一个会议,他就把这个消息“委托”给他的记事簿,然后才能回答你。很好,你没必要知道这位主管到底使用传统记事簿还是使用电子记事簿抑或是秘书来记录自己的约会。

但是人们可能过度运用委托。你也许会看到某个类的接口有一半的函数都委托给其他类,这样就是过度运用。这时应该使用移除中间人,直接和真正负责的对象打交道。如果这样“不干实事”的函数只有少数几个,可以运用内联函数把它们放进调用端。如果这些中间人还有其他行为,可以运用以委托取代超类或者以委托取代子类把它变成真正的对象,这样你既可以扩展原对象的行为,又不必负担那么多的委托动作。

2.14 Refused Bequest(被拒绝的遗赠)

子类应该继承超类的函数和数据。但如果它们不想或不需要继承,又该怎么办呢?它们得到所有礼物,却只从中挑选几样来玩!

按传统说法,这就意味着继承体系设计错误。你需要为这个子类新建一个兄弟类,再运用函数下移和字段下移把所有用不到的函数下推给那个兄弟。这样一来,超类就只持有所有子类共享的东西。你常常会听到这样的建 议:所有超类都应该是抽象(abstract)的。

2.15 注释(Comments)

image-20230131195507602
  1. 废话注释;
  2. 与代码逻辑不一致的注释;
  3. 尽量让提炼的函数和精炼易懂的命名减少注释的必要;

参考资料

https://refactoringguru.cn/

速看笔记版

https://www.itzhai.com/articles/refactoring-cheat-sheet.html

https://www.itzhai.com/articles/bad-code-small.html

《重构》笔记—坏代码的味道与处理

坏味道与重构手法速查表

  • 6
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
第1章 重构,第一个案例 1 1.1 起点 1 1.2 重构的第一步 7 1.3 分解并重组statement() 8 1.4 运用多态取代与价格相关的条件逻辑 34 1.5 结语 52 第2章 重构原则 53 2.1 何谓重构 53 2.2 为何重构 55 2.3 何时重构 57 2.4 怎么对经理说 60 2.5 重构的难题 62 2.6 重构设计 66 2.7 重构与性能 69 2.8 重构起源何处 71 第3章 代码的坏味道 75 3.1 Duplicated Code(重复代码) 76 3.2 Long Method(过长函数) 76 3.3 Large Class(过大的类) 78 3.4 Long Parameter List(过长参数列) 78 3.5 Divergent Change(发散式变化) 79 3.6 Shotgun Surgery(霰弹式修改) 80 3.7 Feature Envy(依恋情结) 80 3.8 Data Clumps(数据泥团) 81 3.9 Primitive Obsession(基本类型偏执) 81 3.10 Switch Statements(switch惊悚现身) 82 3.11 Parallel InheritanceHierarchies(平行继承体系) 83 3.12 Lazy Class(冗赘类) 83 3.13 Speculative Generality(夸夸其谈未来性) 83 3.14 Temporary Field(令人迷惑的暂时字段) 84 3.15 Message Chains(过度耦合的消息链) 84 3.16 Middle Man(中间人) 85 3.17 Inappropriate Intimacy(狎昵关系) 85 3.18 Alternative Classes with Different Interfaces(异曲同工的类) 85 3.19 Incomplete Library Class(不完美的库类) 86 3.20 Data Class(纯稚的数据类) 86 3.21 Refused Bequest(被拒绝的遗赠) 87 3.22 Comments(过多的注释) 87 第4章 构筑测试体系 89 4.1 自测试代码的价值 89 4.2 JUnit测试框架 91 4.3 添加更多测试 97 第5章 重构列表 103 5.1 重构的记录格式 103 5.2 寻找引用点 105 5.3 这些重构手法有多成熟 106 第6章 重新组织函数 109 6.1 Extract Method(提炼函数) 110 6.2 Inline Method(内联函数) 117 6.3 Inline Temp(内联临时变量) 119 6.4 Replace Temp with Query(以查询取代临时变量) 120 6.5 Introduce Explaining Variable(引入解释性变量) 124 6.6 Split Temporary Variable(分解临时变量) 128 6.7 Remove Assignments to Parameters(移除对参数的赋值) 131 6.8 Replace Method with Method Object(以函数对象取代函数) 135 6.9 Substitute Algorithm(替换算法) 139 第7章 在对象之间搬移特性 141 7.1 Move Method(搬移函数) 142 7.2 Move Field(搬移字段) 146 7.3 Extract Class(提炼类) 149 7.4 Inline Class(将类内联化) 154 7.5 Hide Delegate(隐藏“委托关系”) 157 7.6 Remove Middle Man(移除中间人) 160 7.7 Introduce Foreign Method(引入外加函数) 162 7.8 Introduce Local Extension(引入本地扩展) 164 第8章 重新组织数据 169 8.1 Self Encapsulate Field(自封装字段) 171 8.2 Replace Data Value with Object(以对象取代数据值) 175 8.3 Change Value to Reference(将值对象改为引用对象) 179 8.4 Change Reference to Value(将引用对象改为值对象) 183 8.5 Replace Array with Object(以对象取代数组) 186 8.6 Duplicate Observed Data(复制“被监视数据”) 189 8.7 Change Unidirectional Association to Bidirectional(将单向关联改为双向关联) 197 8.8 Change Bidirectional Association to Unidirectional(将双向关联改为单向关联) 200 8.9 Replace Magic Number with Symbolic Constant(以字面常量取代魔法数) 204 8.10 Encapsulate Field(封装字段) 206 8.11 Encapsulate Collection(封装集合) 208 8.12 Replace Record with Data Class(以数据类取代记录) 217 8.13 Replace Type Code with Class(以类取代类型码) 218 8.14 Replace Type Code with Subclasses(以子类取代类型码) 223 8.15 Replace Type Code with State/Strategy(以State/Strategy取代类型码) 227 8.16 Replace Subclass with Fields(以字段取代子类) 232 第9章 简化条件表达式 237 9.1 Decompose Conditional(分解条件表达式) 238 9.2 Consolidate Conditional Expression(合并条件表达式) 240 9.3 Consolidate Duplicate Conditional Fragments(合并重复的条件片段) 243 9.4 Remove Control Flag(移除控制标记) 245 9.5 Replace Nested Conditional with Guard Clauses(以卫语句取代嵌套条件表达式) 250 9.6 Replace Conditional with Polymorphism(以多态取代条件表达式) 255 9.7 Introduce Null Object(引入Null对象) 260 9.8 Introduce Assertion(引入断言) 267 第10章 简化函数调用 271 10.1 Rename Method(函数改名) 273 10.2 Add Parameter(添加参数) 275 10.3 Remove Parameter(移除参数) 277 10.4 Separate Query from Modifier(将查询函数和修改函数分离) 279 10.5 Parameterize Method(令函数携带参数) 283 10.6 Replace Parameter with Explicit Methods(以明确函数取代参数) 285 10.7 Preserve Whole Object(保持对象完整) 288 10.8 Replace Parameter with Methods(以函数取代参数) 292 10.9 Introduce Parameter Object(引入参数对象) 295 10.10 Remove Setting Method(移除设值函数) 300 10.11 Hide Method(隐藏函数) 303 10.12 Replace Constructor with Factory Method(以工厂函数取代构造函数) 304 10.13 Encapsulate Downcast(封装向下转型) 308 10.14 Replace Error Code with Exception(以异常取代错误码) 310 10.15 Replace Exception with Test(以测试取代异常) 315 第11章 处理概括关系 319 11.1 Pull Up Field(字段上移) 320 11.2 Pull Up Method(函数上移) 322 11.3 Pull Up Constructor Body(构造函数本体上移) 325 11.4 Push Down Method(函数下移) 328 11.5 Push Down Field(字段下移) 329 11.6 Extract Subclass(提炼子类) 330 11.7 Extract Superclass(提炼超类) 336 11.8 Extract Interface(提炼接口) 341 11.9 Collapse Hierarchy(折叠继承体系) 344 11.10 Form Tem Plate Method(塑造模板函数) 345 11.11 Replace Inheritance with Delegation(以委托取代继承) 352 11.12 Replace Delegation with Inheritance(以继承取代委托) 355 第12章 大型重构 359 12.1 Tease Apart Inheritance(梳理并分解继承体系) 362 12.2 Convert Procedural Design to Objects(将过程化设计转化为对象设计) 368 12.3 Separate Domain from Presentation(将领域和表述/显示分离) 370 12.4 Extract Hierarchy(提炼继承体系) 375 第13章 重构,复用与现实 379 13.1 现实的检验 380 13.2 为什么开发者不愿意重构他们的程序 381 13.3 再论现实的检验 394 13.4 重构的资源和参考资料 394 13.5 从重构联想到软件复用和技术传播 395 13.6 小结 397 13.7 参考文献 397 第14章 重构工具 401 14.1 使用工具进行重构 401 14.2 重构工具的技术标准 403 14.3 重构工具的实用标准 405 14.4 小结 407 第15章 总结 409
一直很喜欢重构这本书,但是由于自己记性不太好,书看过之后其中的方法总是记不住,于是想如果有电子版的重构书就好了,工作中遇到重构的问题可以随时打开查阅。在网上搜索了许久,发现重构这本书有英文chm版本的,而中文版的电子书只有扫描的PDF版本,用起来非常不方便。于是萌生想做一本重构工具书的想法,本来打算自己重新将重构书的内容再整理归类一下,后来发现原书的目录编排就很适合做工具书,包括坏味道分类,重构手法归类等,都有了一个比较系统的整理。因此,我利用空余时间制作了这样的一本中文的chm版重构,希望对大家有所帮助,也算对中国软件业做出一点小小的贡献。 本书基本上是取自”重构”中文版一书的内容,但格式上参照的是chm英文版的格式,还有一些格式小修改,比如第一章的重构前后代码对比。因为时间匆促,个人能力有限,本书难免存在一些缺漏,如果大家发现有问题,随时可以给我发邮件,我会尽快更新错误的内容。 最后再次感谢几位大师 Martin Fowler、Kent Beck等,还有翻译的侯捷和熊节先生,为我们带来这么精彩的一本书。谢谢。 免责声明:本书仅供个人学习研究之用,不得用于任何商业目的,不得以任何方式修改本作品,基于此产生的法律责任本人不承担任何连带责任。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

许进进

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值