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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Bad:
// code fragment that can be grouped together
void printOwing() {
printBanner();

// Print details.
System.out.println("name: " + name);
System.out.println("amount: " + getOutstanding());
}

// Good:
// Move this code to a separate new method (or function) and replace the old code with a call to the method.
void printOwing() {
printBanner();
printDetails(getOutstanding());
}

void printDetails(double outstanding) {
System.out.println("name: " + name);
System.out.println("amount: " + outstanding);
}
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Bad:
// place the result of an expression in a local variable for later use in your code.
double calculateTotal() {
double basePrice = quantity * itemPrice;
if (basePrice > 1000) {
return basePrice * 0.95;
}
else {
return basePrice * 0.98;
}
}

// Good:
// Move the entire expression to a separate method and return the result from it. Query the method instead of using a variable. Incorporate the new method in other methods, if necessary.
double calculateTotal() {
if (basePrice() > 1000) {
return basePrice() * 0.95;
}
else {
return basePrice() * 0.98;
}
}
double basePrice() {
return quantity * itemPrice;
}
  • Introduce Parameter Object

Methods contain a repeating group of parameters:

![Introduce Parameter Object - Before](/2023/11/17/Code-Refactor/Introduce Parameter Object - Before.png)

Replace these parameters with an object:

![Introduce Parameter Object - After](/2023/11/17/Code-Refactor/Introduce Parameter Object - After.png)

  • Preserve Whole Object.
1
2
3
4
5
6
7
// Bad: Several values are from an object and then pass them as parameters to a method:
int low = daysTempRange.getLow();
int high = daysTempRange.getHigh();
boolean withinPlan = plan.withinRange(low, high);

// Good: Pass the whole object
boolean withinPlan = plan.withinRange(daysTempRange);
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
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
28
29
30
31
32
33
// Bad: A long method in which the local variables are so intertwined that you can't apply Extract Method.
class Order {
// ...
public double price() {
double primaryBasePrice;
double secondaryBasePrice;
double tertiaryBasePrice;
// Perform long computation.
}
}
// Good: Transform the method into a separate class so that the local variables become fields of the class. Then you can split the method into several methods within the same class.

class Order {
// ...
public double price() {
return new PriceCalculator(this).compute();
}
}

class PriceCalculator {
private double primaryBasePrice;
private double secondaryBasePrice;
private double tertiaryBasePrice;

public PriceCalculator(Order order) {
// Copy relevant information from the
// order object.
}

public double compute() {
// Perform long computation.
}
}
Conditionals and Loops

if判断loop可以尝试放在一个单独的方法中

For conditionals, use Decompose Conditional.

If loops are in the way, try Extract Method.

1
2
3
4
5
6
7
8
9
10
11
12
13
// Bad: complex conditional (if-then/else or switch).
if (date.before(SUMMER_START) || date.after(SUMMER_END)) {
charge = quantity * winterRate + winterServiceCharge;
} else {
charge = quantity * summerRate;
}
// Good: Decompose the complicated parts of the conditional into separate methods: the condition, then and else.
if (isSummer(date)) {
charge = summerCharge(quantity);
} else {
charge = winterCharge(quantity);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// bad: Code fragment that can be grouped together.
void printProperties(List users) {
for (int i = 0; i < users.size(); i++) {
String result = "";
result += users.get(i).getName();
result += " ";
result += users.get(i).getAge();
System.out.println(result);

// ...
}
}
// Good: Move this code to a separate new method (or function) and replace the old code with a call to the method.
void printProperties(List users) {
for (User user : users) {
System.out.println(getProperties(user));

// ...
}
}

String getProperties(User user) {
return user.getName() + " " + user.getAge();
}

总结

更多的方法带来的性能差是可以忽略的且更易读的代码能够带来更好的结构化以及潜在优化

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. 分离部分行为到单独的组件中

large_class_1

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.

large_class_2

Extract (as) Interface

Extract Interface helps if it’s necessary to have a list of the operations and behaviors that the client can use.

large_class_3

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)数据的问题。在这个问题中,图形界面和域数据的逻辑耦合在了一起,这可能导致类变得过于复杂,并且难以维护。

large_class_4

这张图展示了代码重构的一个常见模式,即“Duplicate Observed Data”模式。这个模式解决了当一个类负责图形界面(GUI)时,同时还包含域(domain)数据的问题。在这个问题中,图形界面和域数据的逻辑耦合在了一起,这可能导致类变得过于复杂,并且难以维护。

问题(左侧): 在重构前的设计中,IntervalWindow 类同时负责显示信息(GUI)和存储数据(域数据)。这个类中有三个文本字段(TextField),每个字段都有对应的失去焦点(FocusLost)事件处理函数,还有计算长度和计算结束时间的函数。这样的设计让 IntervalWindow 类承担了过多的职责,违反了单一职责原则,也使得该类过于庞大且难以测试。

解决方案(右侧): 图展示了如何将域数据从 IntervalWindow 类中分离出来,创建了一个新的 Interval 类。IntervalWindow 依然保留有界面相关的文本字段和事件处理函数,但是域数据(起始时间、结束时间和长度)现在被移到了新的 Interval 类中,这个类有自己的起始时间、结束时间和长度属性,以及计算长度和计算结束时间的方法。

这样做的好处包括:

  1. 单一职责原则:每个类都只处理一个职责,IntervalWindow 负责界面的显示,而 Interval 负责数据的处理。
  2. 更容易测试:因为 Interval 类只处理数据,所以比起含有GUI代码的类,它更容易进行单元测试。
  3. 更低的耦合度:这种分离减少了类之间的依赖性,使得修改界面或数据模型时,可以减少对另一部分的影响。
  4. 更容易维护和扩展:清晰的分离使得后续维护和添加新功能时更加简单。

在这种模式下,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"可以提高代码的可读性、可维护性和灵活性。

  1. 使用原始数据类型而不是小对象进行简单任务

    • 这里的“primitive”是指原始数据类型。例如,使用intdouble来表示货币值可能会导致精度问题和缺乏表达力。更好的做法是创建一个Currency类,它可以封装货币的值和货币类型(比如美元、欧元等),同时提供货币相关的操作和转换。
  2. 使用常量进行编码信息

    • 在这个上下文中,使用类似USER_ADMIN_ROLE = 1的常量代表了一种“primitive”的使用方式,因为它使用数字(一个原始类型)来代表用户角色。这种做法的问题在于它通常不够表达,而且容易出错。替代的方法是使用枚举(Enum)或者小型类,这样可以提供更加类型安全和描述性更强的方法来代表用户角色。
  3. 使用字符串常量作为数据数组中的字段名称

    • 在这种情况下,“primitive”通常是指字符串类型。例如,当使用字符串来访问数据结构中的值时,这可能会导致拼写错误和与数据结构的耦合。通过创建小型类或结构体,我们可以更安全地封装数据,同时还可以提供更清晰的API和错误检查。

    • 当然可以。使用字符串常量作为数据数组中的字段名称通常发生在我们处理键值对集合,如JSON对象或者Java中的Map时。

      假设我们有一个表示用户的Map,在没有使用小对象封装的情况下,它可能看起来是这样的:

      1
      2
      3
      4
      Map<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
      10
      public 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
      16
      public 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.

primitive_obsession_1

将一部分原始变量打包进入一个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_obsession_2

在这个示例中,“primitive fields”指的是原始数据类型的字段。在Java语言中,原始类型(primitive types)包括基础的数据类型,例如intdoublefloatboolean等,它们不是对象,不属于任何类的实例,并且通常用于表示简单的数值或真/假值。

然而,在这个上下文中,术语“primitive fields”可能被用来泛指那些没有被封装在对象中的简单数据类型的字段。例如,在方法参数中直接使用两个Date类型的参数(startend)而不是一个封装了这两个字段的对象。虽然Date类型在Java中不是原始类型(它实际上是一个对象),这里的“primitive”可能是用来指代那些还没有被进一步抽象或封装的字段。

“引入参数对象”(Introduce Parameter Object)。该策略的目的是简化方法签名并提高代码的可读性和可维护性。

  • 问题:有多个方法(如amountInvoicedInamountReceivedInamountOverdueIn)接受相同的参数(startend日期),这导致了重复的参数组,并且每次调用这些方法时都需要重复这些参数。

  • 解决方案:创建一个新的类或对象来封装这些参数,例如DateRange对象,它包含startend日期。然后,可以将这个对象作为单个参数传递给方法,从而减少参数的数量并使方法调用更加清晰。

重构后,方法调用变得更加简洁,因为只需要传递一个DateRange对象而不是两个分开的Date对象。这样做的好处包括减少重复代码,使方法调用更加直观,以及提供了进一步重构的可能性,例如,如果将来需要在日期范围中添加更多的信息或功能,只需要修改DateRange类即可。

让我们用Java代码举个例子:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 重构前的代码,方法参数中直接使用了Date类型
public class Customer {
public double amountInvoicedIn(Date start, Date end) {
// 计算逻辑...
}

public double amountReceivedIn(Date start, Date end) {
// 计算逻辑...
}

public double amountOverdueIn(Date start, Date end) {
// 计算逻辑...
}
}

// 重构后的代码,引入了一个新的DateRange类来封装日期范围
public class DateRange {
private Date start;
private Date end;

public DateRange(Date start, Date end) {
this.start = start;
this.end = end;
}

// DateRange类的其他有用方法和逻辑...
}

public class Customer {
public double amountInvoicedIn(DateRange dateRange) {
// 使用dateRange.getStart()和dateRange.getEnd()进行计算...
}

public double amountReceivedIn(DateRange dateRange) {
// 使用dateRange.getStart()和dateRange.getEnd()进行计算...
}

public double amountOverdueIn(DateRange dateRange) {
// 使用dateRange.getStart()和dateRange.getEnd()进行计算...
}
}

在这个重构的过程中,通过引入DateRange类,我们不仅简化了方法签名,而且提高了代码的可读性和可维护性。这也是设计模式中的封装原则的一个应用。

另一个方法是保留整个Object

primitive_obsession_3

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.

primitive_obsession_4

primitive_obsession_5

primitive_obsession_6

有时候,使用子类可能不是最佳选择,特别是当类型代码影响对象的状态,但又不能或不应该用子类来表示时。在这种情况下,可以使用状态或策略模式来替换类型代码。

状态/策略模式允许你将行为封装在不同的对象中,并在运行时切换对象的行为。你创建一个状态接口或策略接口,并为每种类型代码创建实现该接口的具体类。然后,你可以在运行时根据需要将这些对象替换为不同的状态或策略。

例如,如果Employee对象的类型会在其生命周期中改变,那么使用状态模式可以在不同的状态之间切换而不需要创建和销毁对象。

不能使用子类的情况:

  • 类型动态变化:如果对象的类型在其生命周期中需要改变,使用子类就不太适合了。因为一旦创建了一个对象的实例,它的类就不能改变了。
  • 类爆炸:如果类型代码的组合非常多,创建对应的子类会导致类数量爆炸,增加系统复杂性。
  • 多维度变化:如果对象的行为由多个独立的维度影响,那么使用子类可能会导致重复代码。在这种情况下,策略模式可以让你独立地改变对象的各个方面。
  • 共享行为:如果不同的类型代码共享一些行为,使用子类可能会导致这些共享行为的重复实现。而状态/策略模式允许共享行为被多个状态/策略共用。
Replace Array with Object

If there are arrays among the variables, use Replace Array with Object.

primitive_obsession_7

总结

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.