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

news2024/11/26 4:52:13

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

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

坏味道与重构手法速查表

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/345753.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Bigscreen Beyond头显解析:极致轻量化,显示部分仅127g

近年来,一体机占据了C端VR市场主要地位,其采用低成本、低门槛、无线化设计,对消费者足够友好。尽管如此,PC VR生态也在发展,相比于一体机,PC VR可提供视觉质量更高的VR体验,而且依托于SteamVR生…

LayUI表格渲染实现前后端交互

方法渲染 初始化 table 容器中配置好相应的参数,由 table 模块内部自动对其完成渲染,而无需你写初始的渲染方法。 首先满足: 带有 class"layui-table"的 标签;对标签设置属性lay-data"" 用于配置一些基础参…

索引-性能分析-explain

explain 执行计划 explain 执行计划各字段含义 1)id 就是代表 sql 的执行顺序或者表的执行顺序;id相同从上往下执行,id不同,id值越大越先执行;(注:有子查询时就会出现sql执行顺序)…

【物联网】mqtt初体验

文章目录安装EMQXjava集成添加依赖mqtt配置参数发布组件订阅组件测试接口接口测试最近在了解物联网云平台方面的知识,解除了mqtt协议,只看书籍难免有些枯燥,所以直接试验一下,便于巩固理论知识。 broker服务器操作系统&#xff1a…

Illegal char <:> at index 4

一、现象Java11环境下项目启动时报错&#xff1a;java.nio.file.InvalidPathException: Illegal char <:> at index 4但项目能正常启动、运行。二、解决办法方法1方法2项目路径\.idea\workspace.xml中的PropertiesComponent节点下新增配置&#xff1a;<property name&…

NoSQL(非关系型数据库)与SQL(关系型数据库)的差别

目录 NoSQL(非关系型数据库)与SQL(关系型数据库)的差别 1.数据结构&#xff1a;结构化与非结构化 2.数据关联&#xff1a;关联性与非关联性 3.查询方式&#xff1a;SQL查询与非SQL查询 4.事务特性&#xff1a;ACID与BASE 分析ACID与BASE的含义&#xff1a; 5.存储方式&am…

16- 梯度提升分类树GBDT (梯度下降优化) (算法)

梯度提升算法 from sklearn.ensemble import GradientBoostingClassifier clf GradientBoostingClassifier(subsample0.8,learning_rate 0.005) clf.fit(X_train,y_train) 1、交叉熵 1.1、信息熵 构建好一颗树&#xff0c;数据变的有顺序了&#xff08;构建前&#xff0c…

jvm对象创建与内存解析

1.类加载检查虚拟机遇到一条new指令时&#xff0c;首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用&#xff0c;并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有&#xff0c;那必须先执行相应的类加载过程。new指令对应到语言层面上讲…

【node.js】node.js的安装和配置

文章目录前言下载和安装Path环境变量测试推荐插件总结前言 Node.js是一个在服务器端可以解析和执行JavaScript代码的运行环境&#xff0c;也可以说是一个运行时平台&#xff0c;仍然使用JavaScript作为开发语言&#xff0c;但是提供了一些功能性的API。 下载和安装 Node.js的官…

linux篇【14】:网络https协议

目录 一.HTTPS介绍 1.HTTPS 定义 2.HTTP与HTTPS &#xff08;1&#xff09;端口不同&#xff0c;是两套服务 &#xff08;2&#xff09;HTTP效率更高&#xff0c;HTTPS更安全 3.加密&#xff0c;解密&#xff0c;密钥 概念 4.为什么要加密&#xff1f; 5.常见的加密方式…

裸辞5个月,面试了37家公司,终于找到理想工作了

上半年裁员&#xff0c;下半年裸辞&#xff0c;有不少人高呼裸辞后躺平真的好快乐&#xff01;但也有很多人&#xff0c;裸辞后的生活五味杂陈。 面试37次终于找到心仪工作 因为工作压力大、领导PUA等各种原因&#xff0c;今年2月下旬我从一家互联网小厂裸辞&#xff0c;没想…

linux高级命令之用户相关操作

用户相关操作学习目标能够知道创建用户的命令1. 创建用户命令说明useradd创建(添加)用户useradd命令选项:选项说明-m自动创建用户主目录,主目录的名字就是用户名-g指定用户所属的用户组&#xff0c;默认不指定会自动创建一个同名的用户组创建用户效果图:查看所有用户信息的文件…

nginx-host绕过实例复现

绕过Nginx Host限制第一种处理方法Nginx在处理Host的时候&#xff0c;会将Host用冒号分割成hostname和port&#xff0c;port部分被丢弃。所以&#xff0c;我们可以设置Host的值为2023.mhz.pw:xxx"example.com&#xff0c;这样就能访问到目标Server块&#xff1a;第二种处理…

SpringBoot的定时任务实现--SpringTask

SpringTask是Spring自带的功能。实现起来比较简单。 使用SpringTask实现定时任务有两种方式&#xff1a; 1.注解方式 基于注解 Scheduled Scheduled(cron "*/1 * * * * ?")public void up(){System.out.println("定时任务开启&#xff1a;"System.cu…

想做好项目经理,一定要知道这10句话

早上好&#xff0c;我是老原。有句话说过&#xff1a;“你是怎么过好一天的&#xff0c;就是怎么过好一生的。”这句话&#xff0c;我刚毕业那会没什么感觉&#xff0c;但工作越久&#xff0c;体会越深。你会发现优秀的人有些特质和习惯千篇一律&#xff0c;而普通人&#xff0…

深圳80后男子朋友圈晒情人节,一天收三个不同女子巧克力红包

每年情人节到来的时候&#xff0c;对于广大男同胞来说&#xff0c;都是倍受煎熬的日子&#xff0c;因为不论你怎么去做&#xff0c;都不会落到好处。如果你还没有对象&#xff0c;这个情人节就尴尬了&#xff0c;眼看着别人出入成双成对&#xff0c;自己却落得个孤家寡人。 如果…

微信Android架构历史——模块化架构重构实践

微信Android诞生之初&#xff0c;用的是常见的分层结构设计。这种架构简单、清晰并一直沿袭至今。这是微信架构的v1.x时代。 图1-架构演进 到了微信架构的v2.x时代&#xff0c;随着业务的快速发展&#xff0c;消息通知不及时和Android 2.3版本之前webview内存泄露问题开始突显…

java基于springboot+vue微信小程序的医疗监督反馈小程序

医疗监督反馈行业是一个传统的行业。根据当前发展现状,网络信息时代的全面普及,医疗监督反馈行业也在发生着变化,单就下单这一方面,利用手机下单正在逐步进入人们的生活。 传统的下单方式,不仅会耗费大量的人力、时间,有时候还会出错。小程序系统伴随智能手机为我们提供了新的方…

【贝叶斯方法】无论您是数据统计分析初学者,还是有一定基础

包括回归及结构方程模型概述及数据探索&#xff1b;R和Rstudio简介及入门和作图基础&#xff1b;R语言数据清洗-tidyverse包&#xff1b;贝叶斯回归与混合效应模型&#xff1b;贝叶斯空间自相关、时间自相关及系统发育相关数据分析&#xff1b;贝叶斯非线性数据分析;贝叶斯结构…

API数据是什么?举例说明,它是电商平台发展的领航者

API接口&#xff1a; API接口是什么&#xff1f; API全称是&#xff1a;Application Programming Interface&#xff0c;即&#xff1a;应用程序接口。开发人员可以使用这些API接口进行编程开发&#xff0c;而又无需访问源码&#xff0c;或理解内部工作机制的细节。 比较常见…