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.

Long Parameter List

More than three or four parameters for a method

原因:

在面向对象编程中,一个核心原则是降低类之间的耦合度,即使类之间的依赖关系最小化。这通常被视为一种良好的设计实践,因为它可以增加代码的模块性和可重用性,并简化系统的测试和维护。

考虑一个生成报告的方法,需要多种数据(如标题、页码、内容等)。最初,这些数据的获取可能直接在方法内部进行,使得方法与数据源高度耦合。为了降低耦合,决定在调用该生成报告方法之前先在外部准备好所有必要的数据,然后通过参数传递给方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ReportGenerator {
public void generateReport(String title, int pageNumber, List<String> content) {
// 生成报告的逻辑
}
}

public class ReportService {
public void prepareAndGenerateReport() {
String title = dataSource.getTitle(); // 获取标题
int pageNumber = dataSource.getPageNumber(); // 获取页码
List<String> content = dataSource.getContent(); // 获取内容
ReportGenerator generator = new ReportGenerator();
generator.generateReport(title, pageNumber, content); // 传递参数
}
}

尽管这种设计减少了ReportGenerator对数据来源的直接依赖,提高了类之间的独立性,但它也引入了长参数列表的问题。当方法依赖于多个外部创建的对象时,这些对象必须通过方法的参数列表传递,导致参数数量急剧增加。

方案

Replace Parameter with Method Call

假如某些arguments是另一个object方法执行的结果,可以将这些arguments替换为方法内部的Method Call。

long_parameter_list_1

Preserve Whole Object

我们可以pass一个包含所有需要参数的object到方法里

long_parameter_list_2

Introduce Parameter Object

和上面的方法其实有点类似上面的是将被调用本身的object存入,这个是创建一个新的object存入,这个方法适用于parameters 来自于不同的source。

long_parameter_list_3

总结

比较简单,但是如果去掉某个参数会导致耦合度增加,那就不要删掉了

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

Data_Clumps_1

Introduce Parameter Object

Data_Clumps_2

Preserve Whole Object

传入参数所对应的对象,而不是单独的几个field

Data_Clumps_3

总结

解决方案很多和长参数列表类似,其实就是通过抽出到某一个class或者直接使用从而避免长/重复Clumps

Object-Orientation Abusers

不规范的使用oop

Switch Statements

原因

非常复杂的switch或者if

一般原因可能是一开始很小但是随着不停的加入条件但是单块swtich不断变大。

在oop中,条件判断应该很少,这是因为OOP可以使用多态的特性

比如:

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
// 使用 switch 的传统方式
public void performOperation(String userType) {
switch (userType) {
case "Admin":
performAdminOperation();
break;
case "Moderator":
performModeratorOperation();
break;
case "User":
performUserOperation();
break;
default:
throw new IllegalArgumentException("Invalid user type");
}
}

// 使用多态的 OOP 方式
public interface User {
void performOperation();
}

public class Admin implements User {
public void performOperation() {
performAdminOperation();
}
}

public class Moderator implements User {
public void performOperation() {
performModeratorOperation();
}
}

public class RegularUser implements User {
public void performOperation() {
performUserOperation();
}
}

通过采用多态性,每个用户类负责定义其操作的具体实现,系统的其他部分则不需要知道用户的具体类型,也不需要 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:

switch_statements_1

Move the method:

switch_statements_2

(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:
switch_statements_3

Replace Type Code with State/Strategy Pattern:

switch_statements_4

理解两者的不同确实需要更具体的代码示例来阐明。让我们通过具体示例更清晰地区分策略模式和状态模式,并强调在实际使用中它们的区别。

策略模式的示例:支付方式选择

假设我们有一个电子商务系统,用户可以选择不同的支付方式(例如信用卡支付、PayPal支付等)。这些支付方式可以被视为不同的策略,用户可以根据自己的需求选择使用哪种支付策略。

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
42
43
44
45
46
47
48
// 支付策略接口
public interface PaymentStrategy {
void pay(int amount);
}

// 信用卡支付策略
public class CreditCardPayment implements PaymentStrategy {
@Override
public void pay(int amount) {
System.out.println("Paid " + amount + " using Credit Card.");
}
}

// PayPal支付策略
public class PaypalPayment implements PaymentStrategy {
@Override
public void pay(int amount) {
System.out.println("Paid " + amount + " using PayPal.");
}
}

// 客户端使用策略
public class ShoppingCart {
private PaymentStrategy paymentStrategy;

public ShoppingCart(PaymentStrategy paymentStrategy) {
this.paymentStrategy = paymentStrategy;
}

public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
this.paymentStrategy = paymentStrategy;
}

public void checkout(int amount) {
paymentStrategy.pay(amount);
}
}

// 使用示例
public class Main {
public static void main(String[] args) {
ShoppingCart cart = new ShoppingCart(new CreditCardPayment());
cart.checkout(100); // 使用信用卡支付

cart.setPaymentStrategy(new PaypalPayment());
cart.checkout(200); // 更改为使用 PayPal 支付
}
}

在这个例子中,ShoppingCart 对象可以在不同的支付策略之间灵活切换,但它自身的状态并没有改变,这反映了策略模式的典型用法。

状态模式的示例:电视状态控制

现在,让我们考虑一个电视机,它有多种状态(如开启、关闭、静音等)。这些状态决定了电视机的行为,状态改变意味着电视行为的改变。

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
42
43
44
45
46
47
48
49
50
51
// 电视状态接口
public interface TVState {
void pressPowerButton();
}

// 开启状态
public class OnState implements TVState {
@Override
public void pressPowerButton() {
System.out.println("Turning TV off.");
// 切换到关闭状态
}
}

// 关闭状态
public class OffState implements TVState {
@Override
public void pressPowerButton() {
System.out.println("Turning TV on.");
// 切换到开启状态
}
}

// 电视类
public class TV {
private TVState state;

public TV(TVState state) {
this.state = state;
}

public void setState(TVState state) {
this.state = state;
}

public void pressPowerButton() {
state.pressPowerButton();
// 根据当前状态改变行为
}
}

// 使用示例
public class Main {
public static void main(String[] args) {
TV tv = new TV(new OffState());
tv.pressPowerButton(); // 开启电视

tv.setState(new OnState());
tv.pressPowerButton(); // 关闭电视
}
}

在这个例子中,TV 类的行为随着内部状态的改变而改变。状态的改变通常由状态自身来管理(虽然在这里是由外部控制以简化示例),这反映了状态模式的核心特性:对象通过改变状态改变其行为。

区别总结

在策略模式中,选择哪种策略通常是由客户端决定,策略之间相互独立,不知道彼此的存在。而在状态模式中,状态通常是有相互转换逻辑的,每个状态知道在某个动作下应该转换到哪个新状态,这是内部管理的。

这两个模式虽然结构相似,但它们在设计意图、管理责任和使用上下文中有明显区别。策略模式强调选择,状态模式强调变化。

Replace Conditional With Polymorphism

switch_statements_5

Replace Parameters switching with Explicit Methods

switch_statements_6

Introduce Null Object

If one of the conditional option is null, use Null Object instead

switch_statements_7

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ReportGenerator {
private double specialCalculationValue;

public void generateReport(Data data) {
if (data.isSpecialCase()) {
specialCalculationValue = complexCalculation(data);
// 使用 specialCalculationValue 进行报告生成
}
// 其他情况下,specialCalculationValue 不被使用,保持为空
}

private double complexCalculation(Data data) {
// 复杂计算
return data.getValue() * 42;
}
}