今天,我们要讲的重构方法为,提取方法(Extract Method)。这也是我最常用的重构方法之一。
注:虽然代码示例是用PHP写的,但相同的概念同样也适用于其他任何OOP语言。
定义
下面是Martin Fowler给出的官方定义:
如果你有一个可以组合在一起的代码段。那么将这个代码片段整合为一个方法,其方法名就用来解释该方法的目的。
我 认为再也没有比这更简单的定义了。此处我唯一想强调的是,方法名。事实上,你命名方法的方式决定了你能从这种重构中受益多少。例 如,methodmoveToPendingList()这个方法名就比mvToPLst()和moveToList()要好。如果你担心代码太长,那么 你错了——我们的目标不是字符最少化,而是让代码更易于理解。好的命名方法能够代替你为这个方法额外添加的注释。
为什么要使用重构?
重构很重要。慢慢的,你就会发现,重构带来的好处比你付出的努力要多得多。最重要的一点是,它从根本上简化了代码。此外,重构让代码变得更易读;允许重用;代替了那些令人讨厌却又不得不写的用来描述代码作用的注释。我认为这些理由已经足够说服你来使用重构了,不是吗?
提取方法的案例
在你使用Extract Method(提取方法)重构的时候,可能会面临这三种情况,它们分别是:没有局部变量,使用局部变量和重新分配局部变量。下面我将一一说明。
举例
假设,在你的电子商务应用程序中有一个方法,该方法用来打印用户购物车中包括总价格在内的所有项目的细节。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
public function printCartDetails() { // print items in the cart echo "Your shopping cart contains the following items:<br>" ; echo "<table>" ; echo "<th>Name</th> <th>Price</th>" ; foreach($ this ->items as $item) { echo "<tr>" ; echo "<td>{$item->getName()}</td>" ; echo "<td>\${$item->getPrice()}</td>" ; echo "</tr>" ; } echo "</table>" ; // calculate the total price $totalPrice = 0 ; foreach($ this ->items as $item) $totalPrice += $item->getPrice(); // print the total price printf( "The total price: $%d" , $totalPrice); } |
请注意我们是如何从类的数组中获取项目的。该数组包含了一列Item(项目)对象,这些Item对象每一个都有访问名称和价格属性的函数:getName()和getPrice()。
这 种方法有许多设计问题,首先方法太长,细节太烦琐。其次,使用注释来描述每个代码片段要做什么,是一种不被认可的坏方法。同时,这也违背了Single Responsibility Principle(单一功能原则)。因此,我们将这个方法分解为更小的方法,这些更小的方法每个都给一个名称用来描述它们是做什么的。
让我们先从负责打印用户购物车中的项目的代码片段开始。实际上,这是最简单的方法提取情况,因为只需要这样做:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
public function printCartDetails() { $ this ->printItemsInCart(); // calculate the total price $totalPrice = 0 ; foreach($ this ->items as $item) $totalPrice += $item->getPrice(); // print the total price printf( "The total price: $%d" , $totalPrice); } private function printItemsInCart() { echo "Your shopping cart contains the following items:<br>" ; echo "<table>" ; echo "<th>Name</th> <th>Price</th>" ; foreach($ this ->items as $item) { echo "<tr>" ; echo "<td>{$item->getName()}</td>" ; echo "<td>\${$item->getPrice()}</td>" ; echo "</tr>" ; } echo "</table>" ; } |
我们只需要剪切和粘贴代码段到一个新的私有方法中,然后再从源方法调用它即可。这就是我所谓的没有局部变量的情况。因为我们提取的代码不依赖于我们从中提取代码的方法中的任何局部变量。
这样我们就不再需要注释来描述这个代码片段要做什么,这个提取方法的名字已经告诉了我们。
接下来要提取的是打印总价格。也很容易。这一次我们需要将源方法中的$totalPrice局部变量作为一个参数,传递到提取方法中。就像这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
public function printCartDetails() { $ this ->printItemsInCart(); // calculate the total price $totalPrice = 0 ; foreach($ this ->items as $item) { $totalPrice += $item->getPrice(); } $ this ->printTotalPrice($totalPrice); } private function printTotalPrice($totalPrice) { printf( "The total price: $%d" , $totalPrice); } |
而这种情况就是使用局部变量。因为提取出的方法需要使用来自于源方法的一个局部变量(在这个例子中就是$totalPrice)来显示总价格。很简单,是不是?
现 在,让我们提取最后一个负责计算总价的方法。如果你有仔细看的话,你会发现,它修改了源方法中的局部变量($totalPrice)。此外,之后还使用了 本地变量。因此,我们不能简单地不做任何修改地剪切和粘贴完全相同的代码到新方法中:我们得根据新版本的提取方法来重新分配局部变量。而且我们只需要返回 修改后的变量就可以办到。就像这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
public function printCartDetails() { $ this ->printItemsInCart(); $totalPrice = 0 ; $totalPrice = $ this ->calculateTotalPrice($totalPrice); $ this ->printTotalPrice($totalPrice); } private function calculateTotalPrice($totalPrice) { foreach($ this ->items as $item) { $totalPrice += $item->getPrice(); } return $totalPrice; } |
不错,但还可以提高。如果我们只是用类似于那样的文本值初始化局部变量(即这里的$totalPrice)的话,那么就没有必要在源方法中保留它,因此我们可以将初始化放到提取方法中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
public function printCartDetails() { $ this ->printItemsInCart(); $totalPrice = $ this ->calculateTotalPrice(); $ this ->printTotalPrice($totalPrice); } private function calculateTotalPrice() { $totalPrice = 0 ; foreach($ this ->items as $item) { $totalPrice += $item->getPrice(); } return $totalPrice; } |
但是,如果初始化依赖于源方法的值,那么我们就需要在提取方法之外保留那个局部变量,然后像之前那样传递。例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
public function printCartDetails($previousAmount) { $ this ->printItemsInCart(); $totalPrice = previousAmount * 1.1 ; $totalPrice = $ this ->calculateTotalPrice($totalPrice); $ this ->printTotalPrice($totalPrice); } private function calculateTotalPrice($totalPrice) { $result = $totalPrice; foreach($ this ->items as $item) { $result += $item->getPrice(); } return $result; } |
对比
下面让我们将改进之后的公共方法printCartDetails()与改进之前做一个对比。
之前:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
public function printCartDetails() { // print items in the cart echo "Your shopping cart contains the following items:<br>" ; echo "<table>" ; echo "<th>Name</th> <th>Price</th>" ; foreach($ this ->items as $item) { echo "<tr>" ; echo "<td>{$item->getName()}</td>" ; echo "<td>\${$item->getPrice()}</td>" ; echo "</tr>" ; } echo "</table>" ; // calculate the total price $totalPrice = 0 ; foreach($ this ->items as $item) $totalPrice += $item->getPrice(); // print the total price printf( "The total price: $%d" , $totalPrice); } |
之后:
1
2
3
4
5
6
7
8
|
public function printCartDetails() { $ this ->printItemsInCart(); $totalPrice = $ this ->calculateTotalPrice(); $ this ->printTotalPrice($totalPrice); } |
很明显,改进之后容易理解多了!只需要5秒我就知道这段代码要做什么:首先打印用户购物车中的项目,然后它计算总价格并打印出来。就是这么简单。
请注意,我们并不关心这段代码如何打印购物车的详细信息。我们只关心代码要做什么。再次重申:我们关心的“what”而不是“how”。如果你想了解“how”的详细信息,那么你就去看如何实现这件事的方法。
总结
以上就是一个非常简单的提取方法重构的例子。提取方法重构是如此强大又易于使用,所以,我建议你从今天开始就使用到你的代码中。
推荐阅读:
相关推荐
为了使读者更好地理解,每个重构方法都附有具体的实例,这些实例大多数使用Java编写,但其概念适用于任何面向对象的编程语言。 此外,书中还提供了一些指导原则,比如重构的时机、重构操作的前后验证以及重构过程中...
自我测试可以通过编写单元测试、集成测试等方式来进行,确保每个模块都按照预期工作。 #### 每天都要学一些新东西 随着技术的快速发展,持续学习成为了程序员职业生涯中的必备能力。无论是最新的编程语言、框架还是...
书中列举了大量的重构模式,如提取函数、移动函数、引入参数对象等,每个模式都有具体的步骤和示例,让开发者学会在实际工作中如何识别和应用这些模式。此外,书中还强调了单元测试在重构过程中的重要性,因为良好的...
- **模块化编码**:将复杂的功能分解为多个独立的模块,每个模块负责一部分功能,这有助于降低系统的复杂度。 - **模块间通信**:设计良好的接口使得模块间能够顺畅地交换数据。 - **日志记录**:合理地记录日志信息...
2. **提取方法**:将大函数分解成多个小函数,每个函数只做一件事情,提高代码的可读性。 3. **引入参数对象**:当一个函数接收过多参数时,可以将这些参数封装到一个新的对象中,简化调用接口。 4. **移动函数/...
这种细微的平衡操作需要对代码进行一系列小的修改,每个小修改都会增加整体代码的质量,同时保持系统行为的一致性。正确的重构流程包括了以下步骤:首先,确保有一套可靠的测试用例,用来验证代码修改前后的行为;...
遇到问题时,如何快速定位并解决问题是衡量一个程序员能力的重要指标。这涉及到调试技巧、日志分析、性能监控以及利用开源社区资源的能力。学会阅读和理解官方文档,使用如JProfiler、VisualVM等工具,对提高问题...
下面将根据给定的信息,详细介绍一名程序员在职业发展道路上应该掌握的知识点。 #### Java基础及核心库 1. **理解SkillMap**:SkillMap是指在特定领域内所需要掌握的技能集合。对于Java程序员来说,构建一个清晰的...
总之,《重构-改善既有代码的设计》是一本每个程序员都应阅读的书,它教会我们如何通过持续的、有条理的改进,让代码保持整洁,让软件系统始终保持活力,适应不断变化的需求。在阅读这本书的过程中,你将学会如何用...
性能优化是每个Java程序员都应该关注的问题。一些常见的优化策略包括:使用高效的算法和数据结构、避免不必要的对象创建、合理使用缓存、优化SQL查询、减少远程调用次数等。同时,利用JVM的性能监控工具(如VisualVM...
总之,“当程序员的第一件事”不仅仅是学会写代码这么简单,更重要的是建立起一套系统化的学习方法、养成良好的编程习惯,并始终保持对新技术的好奇心和探索精神。希望每位初学者都能在这条道路上越走越远,最终成为...
4. 问题追踪:利用Jira、Trello等工具跟踪问题,确保每个任务都有明确的负责人。 五、健康与工作生活平衡 1. 工作环境:保持良好的工作环境,减少身体疲劳,提高注意力。 2. 健康生活习惯:定时休息,适当运动,...
2. **入门程序**:Java的 HelloWorld 程序是每个初学者的起点。通过编写并运行这个简单的程序,你可以了解Java的基本语法结构,包括类、主方法和输出语句。 3. **常量与变量**:常量是不可改变的值,如 PI;变量则...
### 老程序员才知道的技巧 #### 1. 重构:程序员的核心技能 重构是程序员的一项核心技能,它不仅能够帮助提高代码的质量,还能增强软件的可维护性和扩展性。重构涉及修改现有代码而不...每个人都值得尊重,不论性别。
10. **标准库的深度使用**:深入研究C标准库,如`stdio`、`stdlib`、`string`、`math`等,了解每个函数的用途和潜在陷阱。 11. **编码风格与代码规范**:良好的编码风格对于代码的可读性和维护性至关重要,了解并...
掌握Git的基本操作如commit、push、pull、merge和rebase,是每个程序员必备的技能。 7. **持续集成/持续部署(CI/CD)**:CI/CD流程可以自动化构建、测试和部署,确保每次代码更改都能快速验证其正确性。Jenkins、...
他还提供了实践中的建议,包括如何为求职准备、如何在职业生涯的每个阶段做出合理规划,以及如何将职业规划的思维应用于日常工作中。通过这些知识分享,张振华希望帮助程序员朋友们明确自己的职业道路,逐步实现从...