Code-Refactor
Code Smells
需要避免这些
Bloaters:
Code, methods and classes that have invreased to such gargantuan proportions that are hard to work with. They accumulate over time as the program evolves
Long Method
A method contains too many lines of code
(Method that is longer than ten lines should take care about it)
原因:
一般情况下加入原本方法比添加新方法看起来要简单一些并且方法的长度是逐渐增长的,因此是不容易被一次发现的。
方案:
As a rule of thumb, if you feel the need to comment on something inside a method, you should take this code and put it in a new method.
Extract Method
合并逻辑相通的代码:
1 | // Bad: |
Reduce local variables and parameters before extracting a method
If local variables and parameters interfere with extracting a method, use
- Replace Temp with Query
1 | // Bad: |
- Introduce Parameter Object
Methods contain a repeating group of parameters:
![Introduce Parameter Object - Before](/posts/Code-Refactor/Introduce Parameter Object - Before.png)
Replace these parameters with an object:
![Introduce Parameter Object - After](/posts/Code-Refactor/Introduce Parameter Object - After.png)
- Preserve Whole Object.
1 | // Bad: Several values are from an object and then pass them as parameters to a method: |
Replace Method with Method Object
If none of the previous recipes help, try moving the entire method to a separate object via Replace Method with Method Object.
1 | // Bad: A long method in which the local variables are so intertwined that you can't apply Extract Method. |
Conditionals and Loops
if判断
和loop
可以尝试放在一个单独的方法中
For conditionals, use Decompose Conditional.
If loops are in the way, try Extract Method.
1 | // Bad: complex conditional (if-then/else or switch). |
1 | // bad: Code fragment that can be grouped together. |
总结
更多的方法带来的性能差是可以忽略的且更易读的代码能够带来更好的结构化以及潜在优化
Large Class
Class that contains many fields/method/lines of code
原因
和 Long Method 类似,只管认为加field比加class要容易的多
方案
When a class is wearing too many (functional) hats, think about splitting it up.
Extract (as) Class
Extract Class helps if part of the behavior of the large class can be spun off into a separate component. 分离部分行为到单独的组件中
Extract (as) Subclass
Extract Subclass helps if part of the behavior of the large class can be implemented in different ways or is used in rare cases.
Extract (as) Interface
Extract Interface helps if it’s necessary to have a list of the operations and behaviors that the client can use.
Duplicate Observed Data
If a large class is responsible for the graphical interface, you may try to move some of its data and behavior to a separate domain object. In doing so, it may be necessary to store copies of some data in two places and keep the data consistent. Duplicate Observed Data offers a way to do this.
当一个类负责图形界面(GUI)时,同时还包含域(domain)数据的问题。在这个问题中,图形界面和域数据的逻辑耦合在了一起,这可能导致类变得过于复杂,并且难以维护。
这张图展示了代码重构的一个常见模式,即“Duplicate Observed Data”模式。这个模式解决了当一个类负责图形界面(GUI)时,同时还包含域(domain)数据的问题。在这个问题中,图形界面和域数据的逻辑耦合在了一起,这可能导致类变得过于复杂,并且难以维护。
问题(左侧): 在重构前的设计中,IntervalWindow
类同时负责显示信息(GUI)和存储数据(域数据)。这个类中有三个文本字段(TextField
),每个字段都有对应的失去焦点(FocusLost)事件处理函数,还有计算长度和计算结束时间的函数。这样的设计让 IntervalWindow
类承担了过多的职责,违反了单一职责原则,也使得该类过于庞大且难以测试。
解决方案(右侧): 图展示了如何将域数据从 IntervalWindow
类中分离出来,创建了一个新的 Interval
类。IntervalWindow
依然保留有界面相关的文本字段和事件处理函数,但是域数据(起始时间、结束时间和长度)现在被移到了新的 Interval
类中,这个类有自己的起始时间、结束时间和长度属性,以及计算长度和计算结束时间的方法。
这样做的好处包括:
- 单一职责原则:每个类都只处理一个职责,
IntervalWindow
负责界面的显示,而Interval
负责数据的处理。 - 更容易测试:因为
Interval
类只处理数据,所以比起含有GUI代码的类,它更容易进行单元测试。 - 更低的耦合度:这种分离减少了类之间的依赖性,使得修改界面或数据模型时,可以减少对另一部分的影响。
- 更容易维护和扩展:清晰的分离使得后续维护和添加新功能时更加简单。
在这种模式下,IntervalWindow
类会观察(Observe) Interval
类的实例。当 Interval
的数据发生变化时,IntervalWindow
可以更新其显示的数据。这通常通过某种形式的观察者模式来实现,其中 Interval
类会通知所有注册的观察者数据的变化。这样,任何时候 Interval
的数据改变了,IntervalWindow
都可以得到通知,并更新用户界面。
总结
- Refactoring of these classes spares developers from needing to remember a large number of attributes for a class. [重构这些类使开发人员无需记住类的大量属性。]
- In many cases, splitting large classes into parts avoids duplication of code and functionality. [在许多情况下,将大类分成几个部分可以避免代码和功能的重复。]
Primitive Obsession
- Use of primitives instead of small objects for simple tasks (such as currency, ranges, special strings for phone numbers, etc.)
- Use of constants for coding information (such as a constant
USER_ADMIN_ROLE = 1
for referring to users with administrator rights.) - Use of string constants as field names for use in data arrays.
“Primitive obsession”是一个常见的编码问题,指的是开发者过度使用原始数据类型(如int、float、boolean等)来表示应该由对象表示的概念。这个术语反映了一种倾向:即使在面向对象编程中,也倾向于使用基本数据类型而不是设计小型的类来表示具有业务逻辑的概念。
解决”primitive obsession”可以提高代码的可读性、可维护性和灵活性。
使用原始数据类型而不是小对象进行简单任务:
- 这里的“primitive”是指原始数据类型。例如,使用
int
或double
来表示货币值可能会导致精度问题和缺乏表达力。更好的做法是创建一个Currency
类,它可以封装货币的值和货币类型(比如美元、欧元等),同时提供货币相关的操作和转换。
- 这里的“primitive”是指原始数据类型。例如,使用
使用常量进行编码信息:
- 在这个上下文中,使用类似
USER_ADMIN_ROLE = 1
的常量代表了一种“primitive”的使用方式,因为它使用数字(一个原始类型)来代表用户角色。这种做法的问题在于它通常不够表达,而且容易出错。替代的方法是使用枚举(Enum)或者小型类,这样可以提供更加类型安全和描述性更强的方法来代表用户角色。
- 在这个上下文中,使用类似
使用字符串常量作为数据数组中的字段名称:
在这种情况下,“primitive”通常是指字符串类型。例如,当使用字符串来访问数据结构中的值时,这可能会导致拼写错误和与数据结构的耦合。通过创建小型类或结构体,我们可以更安全地封装数据,同时还可以提供更清晰的API和错误检查。
当然可以。使用字符串常量作为数据数组中的字段名称通常发生在我们处理键值对集合,如JSON对象或者Java中的
Map
时。假设我们有一个表示用户的
Map
,在没有使用小对象封装的情况下,它可能看起来是这样的:1
2
3
4Map<String, Object> user = new HashMap<>();
user.put("name", "John Doe");
user.put("email", "john.doe@example.com");
user.put("role", 1);在上述代码中,字段名称如
"name"
、"email"
和"role"
都是直接使用字符串常量。这种方式的问题在于,它容易出错(比如拼写错误)并且不利于重构(比如当字段名称变更时,所有的字符串都需要修改)。一个更好的做法是使用常量来代替这些字符串字面量:
1
2
3
4
5
6
7
8
9
10public class UserConstants {
public static final String NAME = "name";
public static final String EMAIL = "email";
public static final String ROLE = "role";
}
Map<String, Object> user = new HashMap<>();
user.put(UserConstants.NAME, "John Doe");
user.put(UserConstants.EMAIL, "john.doe@example.com");
user.put(UserConstants.ROLE, 1);这样,如果字段名需要更改,我们只需要在
UserConstants
类中修改一次,而不需要在整个代码库中寻找和替换所有的硬编码字符串。但是,要彻底解决”primitive obsession”的问题,我们应该创建一个用户类来封装这些属性:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public class User {
private String name;
private String email;
private int role;
public User(String name, String email, int role) {
this.name = name;
this.email = email;
this.role = role;
}
// 这里可以添加getter和setter方法
// ...
}
User user = new User("John Doe", "john.doe@example.com", 1);现在,我们有了一个类型安全的对象,其中的字段名称是类的属性,而不是字符串字面量。这种方式不仅减少了错误的机会,而且使得代码更容易阅读和维护。此外,
User
类可以包含逻辑,比如验证电子邮件的格式或者确定用户角色的权限。
简而言之,在这些情况下,”primitive”通常指的是基本数据类型或者直接使用的常量。”Primitive obsession”可能会导致代码难以理解和维护,因为它缺乏抽象层,使得代码中的业务逻辑不够明确。通过引入小对象来封装复杂性,代码会变得更加清晰和健壮。
原因
Like most other smells, primitive obsessions are born in moments of weakness. “Just a field for storing some data!” the programmer said. Creating a primitive field is so much easier than making a whole new class, right? And so it was done. Then another field was needed and added in the same way. Lo and behold, the class became huge and unwieldy. - 简单就加了
Primitives are often used to “simulate” types. So instead of a separate data type, you have a set of numbers or strings that form the list of allowable values for some entity. Easy-to-understand names are then given to these specific numbers and strings via constants, which is why they’re spread wide and far. - 用于代表一些东西
Another example of poor primitive use is field simulation. The class contains a large array of diverse data and string constants (which are specified in the class) are used as array indices for getting this data.
方案
Replace Set of Fields with Object
If you have a large variety of primitive fields, it may be possible to logically group some of them into their own class. Even better, move the behavior associated with this data into the class too. For this task, try Replace Data Value with Object.
将一部分原始变量打包进入一个class
Primitive Fields in Method Parameters
If the values of primitive fields are used in method parameters, go with Introduce Parameter Object or Preserve Whole Object.
在这个示例中,“primitive fields”指的是原始数据类型的字段。在Java语言中,原始类型(primitive types)包括基础的数据类型,例如int
、double
、float
、boolean
等,它们不是对象,不属于任何类的实例,并且通常用于表示简单的数值或真/假值。
然而,在这个上下文中,术语“primitive fields”可能被用来泛指那些没有被封装在对象中的简单数据类型的字段。例如,在方法参数中直接使用两个Date
类型的参数(start
和end
)而不是一个封装了这两个字段的对象。虽然Date
类型在Java中不是原始类型(它实际上是一个对象),这里的“primitive”可能是用来指代那些还没有被进一步抽象或封装的字段。
“引入参数对象”(Introduce Parameter Object)。该策略的目的是简化方法签名并提高代码的可读性和可维护性。
问题:有多个方法(如
amountInvoicedIn
、amountReceivedIn
和amountOverdueIn
)接受相同的参数(start
和end
日期),这导致了重复的参数组,并且每次调用这些方法时都需要重复这些参数。解决方案:创建一个新的类或对象来封装这些参数,例如
DateRange
对象,它包含start
和end
日期。然后,可以将这个对象作为单个参数传递给方法,从而减少参数的数量并使方法调用更加清晰。
重构后,方法调用变得更加简洁,因为只需要传递一个DateRange
对象而不是两个分开的Date
对象。这样做的好处包括减少重复代码,使方法调用更加直观,以及提供了进一步重构的可能性,例如,如果将来需要在日期范围中添加更多的信息或功能,只需要修改DateRange
类即可。
让我们用Java代码举个例子:
1 | // 重构前的代码,方法参数中直接使用了Date类型 |
在这个重构的过程中,通过引入DateRange
类,我们不仅简化了方法签名,而且提高了代码的可读性和可维护性。这也是设计模式中的封装原则的一个应用。
另一个方法是保留整个Object
Get Rid of Type codes
When complicated data is coded in variables, use Replace Type Code with Class, Replace Type Code with Subclasses or Replace Type Code with State/Strategy.
有时候,使用子类可能不是最佳选择,特别是当类型代码影响对象的状态,但又不能或不应该用子类来表示时。在这种情况下,可以使用状态或策略模式来替换类型代码。
状态/策略模式允许你将行为封装在不同的对象中,并在运行时切换对象的行为。你创建一个状态接口或策略接口,并为每种类型代码创建实现该接口的具体类。然后,你可以在运行时根据需要将这些对象替换为不同的状态或策略。
例如,如果Employee
对象的类型会在其生命周期中改变,那么使用状态模式可以在不同的状态之间切换而不需要创建和销毁对象。
不能使用子类的情况:
- 类型动态变化:如果对象的类型在其生命周期中需要改变,使用子类就不太适合了。因为一旦创建了一个对象的实例,它的类就不能改变了。
- 类爆炸:如果类型代码的组合非常多,创建对应的子类会导致类数量爆炸,增加系统复杂性。
- 多维度变化:如果对象的行为由多个独立的维度影响,那么使用子类可能会导致重复代码。在这种情况下,策略模式可以让你独立地改变对象的各个方面。
- 共享行为:如果不同的类型代码共享一些行为,使用子类可能会导致这些共享行为的重复实现。而状态/策略模式允许共享行为被多个状态/策略共用。
Replace Array with Object
If there are arrays among the variables, use Replace Array with Object.
总结
Code becomes more flexible thanks to use of objects instead of primitives.
Better understandability and organization of code. Operations on particular data are in the same place, instead of being scattered. No more guessing about the reason for all these strange constants and why they’re in an array.
Easier finding of duplicate code.
Long Parameter List
More than three or four parameters for a method
原因:
在面向对象编程中,一个核心原则是降低类之间的耦合度,即使类之间的依赖关系最小化。这通常被视为一种良好的设计实践,因为它可以增加代码的模块性和可重用性,并简化系统的测试和维护。
考虑一个生成报告的方法,需要多种数据(如标题、页码、内容等)。最初,这些数据的获取可能直接在方法内部进行,使得方法与数据源高度耦合。为了降低耦合,决定在调用该生成报告方法之前先在外部准备好所有必要的数据,然后通过参数传递给方法:
1 | public class ReportGenerator { |
尽管这种设计减少了ReportGenerator
对数据来源的直接依赖,提高了类之间的独立性,但它也引入了长参数列表的问题。当方法依赖于多个外部创建的对象时,这些对象必须通过方法的参数列表传递,导致参数数量急剧增加。
方案
Replace Parameter with Method Call
假如某些arguments是另一个object
方法执行的结果,可以将这些arguments替换为方法内部的
Method Call。
Preserve Whole Object
我们可以pass一个包含所有需要参数的object到方法里
Introduce Parameter Object
和上面的方法其实有点类似上面的是将被调用本身的object存入,这个是创建一个新的object存入,这个方法适用于parameters 来自于不同的source。
总结
比较简单,但是如果去掉某个参数会导致耦合度增加,那就不要删掉了
Data Clumps
Different Parts of the code contain identical groups of variables. These Clumps should be turned into their own classes
原因
封装失败导致的,相关连的变量应该被组合为一个对象。所以可以 尝试删除一个变量然后看看其他值是否仍然有意义。比如:如果删除了 user
后,剩余的 startDate
, endDate
, 和 roomNumber
仍描述了预订的时间和地点,但缺少了执行操作的用户信息,这是业务逻辑不完整的表现。这说明 startDate
, endDate
, roomNumber
, 和 user
这组变量彼此之间存在强烈的逻辑关联,应该一起考虑,它们的组合描述了一个完整的业务操作 —— 预订或取消预订一个会议室。
方案
Extract Class
Introduce Parameter Object
Preserve Whole Object
传入参数所对应的对象,而不是单独的几个field
总结
解决方案很多和长参数列表类似,其实就是通过抽出到某一个class或者直接使用从而避免长/重复Clumps
Object-Orientation Abusers
不规范的使用oop
Switch Statements
原因
非常复杂的
switch
或者if
一般原因可能是一开始很小但是随着不停的加入条件但是单块swtich不断变大。
在oop中,条件判断应该很少,这是因为OOP可以使用多态的特性
比如:
1 | // 使用 switch 的传统方式 |
通过采用多态性,每个用户类负责定义其操作的具体实现,系统的其他部分则不需要知道用户的具体类型,也不需要 switch
语句。这样做提高了代码的模块化和可维护性,并使得添加新的用户类型变得非常容易。
方案
Isolate switch
Operator
To isolate switch and put it in the right class, need Extract Method
and the Move Method
Extract the swtich logic to a code block:
Move the method:
(PS: 感觉这种方法没有什么意义,本质上就是将在另一个class中被经常调用的方法给挪过去,然后调用reference,本质上像是一个Util class)
Get Rid of Type Codes
这个其实在17514中提到过,少使用 instanceof,多使用多态性质
当被用于检查type时,use Replace Type Code with Subclasses or Replace Type Code with State/Strategy
Replace Type Code with Subclasses:
Replace Type Code with State/Strategy Pattern:
理解两者的不同确实需要更具体的代码示例来阐明。让我们通过具体示例更清晰地区分策略模式和状态模式,并强调在实际使用中它们的区别。
策略模式的示例:支付方式选择
假设我们有一个电子商务系统,用户可以选择不同的支付方式(例如信用卡支付、PayPal支付等)。这些支付方式可以被视为不同的策略,用户可以根据自己的需求选择使用哪种支付策略。
1 | // 支付策略接口 |
在这个例子中,ShoppingCart
对象可以在不同的支付策略之间灵活切换,但它自身的状态并没有改变,这反映了策略模式的典型用法。
状态模式的示例:电视状态控制
现在,让我们考虑一个电视机,它有多种状态(如开启、关闭、静音等)。这些状态决定了电视机的行为,状态改变意味着电视行为的改变。
1 | // 电视状态接口 |
在这个例子中,TV
类的行为随着内部状态的改变而改变。状态的改变通常由状态自身来管理(虽然在这里是由外部控制以简化示例),这反映了状态模式的核心特性:对象通过改变状态改变其行为。
区别总结
在策略模式中,选择哪种策略通常是由客户端决定,策略之间相互独立,不知道彼此的存在。而在状态模式中,状态通常是有相互转换逻辑的,每个状态知道在某个动作下应该转换到哪个新状态,这是内部管理的。
这两个模式虽然结构相似,但它们在设计意图、管理责任和使用上下文中有明显区别。策略模式强调选择,状态模式强调变化。
Replace Conditional With Polymorphism
Replace Parameters switch
ing with Explicit Methods
Introduce Null Object
If one of the conditional option is null
, use Null Object instead
Temporary Field
Temporary fields get their values (and thus are needed by objects) only under certain circumstances. Outside of these circumstances, they’re empty.
这种一般是因为之后要生成数据现在搁置的情况或者是数据在固定某个if分支中才会被赋值
另一种情况是避免长参数列表而引入。
1 | public class ReportGenerator { |