什么是重构
具体来说就是在不改变代码功能行为的情况下,对其内部结构的一种调整。需要注意的是,重构不是代码优化,重构注重的是提高代码的可理解性与可扩展性,对性能的影响可好可坏。而性能优化则让程序运行的更快,当然,最终的代码可能更难理解和维护。
为什么要重构
改善程序的内部设计
如果没有重构,在软件不停的版本迭代中,代码的设计只会越来越腐败,导致软件开发寸步难行。
- 人们只为了短期目的而修改代码时,往往没有完全理解整体的架构设计(在大项目中常有这种情况,比如在不同的地方,使用完全相同的语句做着同样的事情),代码就会失去自己的结构,代码结构的流失具有累积效应,越难看出代码所代表的设计意图,就越难保护其设计。
- 我们几乎不可能预先做出完美的设计,以面对后续未知的功能开发,只有在实践中才能找到真理。
使得代码更容易理解
在开发中,我们需要先理解代码在做什么,才能着手修改,很多时候自己写的代码都会忘记其实现,更不论别人的代码。可能在这段代码中有一段糟糕的条件判断逻辑,又或者其变量命名实在糟糕又确实注释,需要花上好一段时间才能明白其真正用意。
提高开发的速度 && 方便定位错误
提高开发的速度可能有点“反直觉”,因为重构在很多时候看来是额外的工作量,并没有新的功能和特性产出,但是减少代码的书写量(复用模块),方便定位错误(代码结构优良),这些能让我们在开发的时候节省大量的时间,在后续的开发中“轻装上阵”。
何时重构?
重构不是 一件应该特别拨出时间做的事情,重构应该随时随地进行。不应该为重构而重构,之所以重构,是因为我们想做别的什么事,而重构可以帮助我们把那些事做好。
作者给出了一个三次原则,让我们来看一下: 三次法则:事不过三,三则重构。
- 添加功能时重构。
- 修补错误时重构。
- 复审代码时重构。
代码坏味道
1 神秘命名 Mysterious Name
函数命名体现功能用途,一个函数做一件事,多功能进行拆分独立函数 变量命名体现是什么
2 重复代码 Duplicated Code
- 如果重复代码只是相似,而不是完全相同。 请先
移动语句,调整代码顺序,把相似部分放到一起以便提炼 在不同子类,重复代码函数上移到超类。
3 过长函数
提炼函数的时候会出现很多的参数或者临时变量,此种情形,应该使用查询取代临时变量。
提炼函数的信号:注释、条件表达式、switch和循环
这个重构手法的目的是将临时变量的计算逻辑替换为一个查询操作,以减少参数和临时变量的数量,使代码更加简洁易读。
public class Calculation {
public static void main(String[] args) {
int quantity = 10;
double price = 5.0;
//将不同的价格计算接口进行相加,减少了中间计算结果的临时变量,只有一个最终的结果变量
double finalPrice = calculateTotalPrice(quantity, price) + calculateFee1(calculateTotalPrice(quantity, price))
+ calculateFee2(calculateTotalPrice(quantity, price), calculateFee1(calculateTotalPrice(quantity, price)));
System.out.println("Final Price: " + finalPrice);
}
public static double calculateTotalPrice(int quantity, double price) {
// 计算总价
double total = quantity * price;
return total;
}
public static double calculateFee1(double total) {
// fee1的计算逻辑
// ...
return fee1;
}
public static double calculateFee2(double total, double fee1) {
// fee2的计算逻辑
// ...
return fee2;
}
}
4 过长参数列表 Long Parameter List
函数组合成类 如果多个函数有同样的几个参数,引入一个类就极为有意义。将参数编程类的字段。
当函数之间有相同的参数列表时,可以将这些函数组合成一个类并将参数作为类的字段。这样做可以提高代码的可读性和维护性。以下是一个示例的Java代码实现:
public class LongParameterListExample {
private int param1;
private String param2;
private boolean param3;
public LongParameterListExample(int param1, String param2, boolean param3) {
this.param1 = param1;
this.param2 = param2;
this.param3 = param3;
}
public void function1() {
// 使用param1、param2、param3执行逻辑
}
public void function2() {
// 使用param1、param2、param3执行逻辑
}
public void function3() {
// 使用param1、param2、param3执行逻辑
}
}
在上述示例中,我们创建了一个名为LongParameterListExample的类,并将参数param1、param2和param3作为类的字段。通过构造函数将这些参数传入对象,并在类的其他方法中使用这些参数执行特定的逻辑。
通过将参数转化为类的字段,我们避免了在每个函数中重复传递相同的参数。这样做不仅减少了代码冗余,还使得函数之间的关系更加清晰。此外,如果需要添加或修改参数,只需在类的字段中进行调整,而无需修改所有使用该参数的函数签名。
总结起来,将具有相同参数列表的函数组合成一个类可以提高代码的可读性、维护性和重用性。
如果发现一组函数形影不离地操作者同一块数据(通常是将这块数据作为参数传递给函数),那就是时候组建一个类了。
使用类有一大好处:客户端可以修改对象的核心数据,通过计算得出的派生数据则会自动与核心数据保持一致。
5 全局变量 Global Data
如果代码中存在全局变量的坏味道,可以考虑使用封装变量(Encapsulate Field)的重构手法来解决。封装变量的目的是隐藏直接访问全局变量的细节,并通过提供访问方法来控制变量的访问权限。
public class GlobalVariableExample {
private int globalValue;
public int getGlobalValue() {
return globalValue;
}
public void setGlobalValue(int value) {
globalValue = value;
}
public void doSomething() {
// 使用封装后的全局变量
int result = getGlobalValue() * 2;
System.out.println("Result: " + result);
}
public static void main(String[] args) {
GlobalVariableExample example = new GlobalVariableExample();
example.setGlobalValue(10);
example.doSomething();
}
}
在上述示例中,globalValue 被封装为私有变量,并且提供了公共的访问方法 getGlobalValue() 和 setGlobalValue()。其他方法(例如 doSomething())只能通过这些访问方法来访问和修改 globalValue 的值,而无法直接操作全局变量。
通过封装全局变量,我们可以将对全局变量的访问和修改限制在类的内部,从而增强了代码的可控性和可维护性。此外,封装变量还为未来的代码修改提供了更大的灵活性,因为我们可以在访问方法中添加额外的逻辑或验证。
需要注意的是,封装变量并不是一种解决所有全局变量问题的银弹。在某些情况下,全局变量可能确实是必要的,但应该谨慎使用,并且仅在有明确的理由和合理的设计考虑下使用全局变量。
可变数据
当代码存在可变数据(Mutable Data)的坏味道时,可以采用多种重构手法来改善代码。以下是一些常见的重构手法和示例Java代码:
1. 拆分变量(Split Variable)
拆分变量的目的是将一个包含多个意义的变量拆分为独立的变量,每个变量只负责一个具体的意义。
public class SplitVariableExample {
private int width;
private int height;
public void setDimensions(int width, int height) {
this.width = width;
this.height = height;
}
public int calculateArea() {
return width * height;
}
public static void main(String[] args) {
SplitVariableExample example = new SplitVariableExample();
example.setDimensions(10, 5);
int area = example.calculateArea();
System.out.println("Area: " + area);
}
}
在上述示例中,width 和 height 两个变量被拆分为独立的变量,每个变量负责记录单独的维度值。
2. 查询函数和修改函数分离(Separate Query from Modifier)
查询函数负责获取数据,并且没有副作用;修改函数负责修改数据,但不返回任何结果。
public class SeparateQueryModifierExample {
private int temperature;
public int getTemperature() {
return temperature;
}
public void setTemperature(int temperature) {
this.temperature = temperature;
}
public void increaseTemperature(int amount) {
temperature += amount;
}
public static void main(String[] args) {
SeparateQueryModifierExample example = new SeparateQueryModifierExample();
example.setTemperature(25);
System.out.println("Current Temperature: " + example.getTemperature());
example.increaseTemperature(5);
System.out.println("New Temperature: " + example.getTemperature());
}
}
在上述示例中,getTemperature() 是一个查询函数,用于获取温度值;setTemperature() 是一个修改函数,用于设置温度值;increaseTemperature() 也是一个修改函数,用于增加温度值。
3. 移除设值函数(Remove Setters)
如果一个字段不需要被外部修改,可以移除对应的设值函数,将其变为只读属性。
public class RemoveSettersExample {
private final String name;
public RemoveSettersExample(String name) {
this.name = name;
}
public String getName() {
return name;
}
public static void main(String[] args) {
RemoveSettersExample example = new RemoveSettersExample("John");
System.out.println("Name: " + example.getName());
}
}
在上述示例中,name 字段被声明为 final,并且没有相应的设值函数。因此,外部无法修改 name 的值,只能通过读取 getName() 方法来获取名字。
4. 以查询取代派生变量(Replace Derived Variable with Query)
如果一个变量的值可以通过计算得到,可以使用查询函数来替换直接访问该变量。
public class ReplaceDerivedVariableExample {
private int length;
private int width;
public int getArea() {
return calculateArea();
}
private int calculateArea() {
return length * width;
}
public static void main(String[] args) {
ReplaceDerivedVariableExample example = new ReplaceDerivedVariableExample();
example.length = 10;
example.width = 5;
int area = example.getArea();
System.out.println("Area: " + area);
}
}
在上述示例中,getArea() 方法通过调用 calculateArea() 方法来计算矩形的面积,而不是直接访问 area 变量。
5. 函数组合成变换(Combine Functions into Transform)
将多个函数组合为一个更高级别的变换函数,以简化代码。
public class CombineFunctionsIntoTransformExample {
public String formatFullName(String firstName, String lastName) {
return capitalize(firstName) + " " + capitalize(lastName);
}
private String capitalize(String str) {
return str.substring(0, 1).toUpperCase() + str.substring(1).toLowerCase();
}
public static void main(String[] args) {
CombineFunctionsIntoTransformExample example = new CombineFunctionsIntoTransformExample();
String fullName = example.formatFullName("john", "doe");
System.out.println("Full Name: " + fullName);
}
}
在上述示例中,formatFullName() 方法将首字母大写并格式化为全名。它使用了 capitalize() 方法来对每个名字进行首字母大写的处理,然后通过字符串拼接得到完整的全名。
6. 将引用对象改为值对象(Change Reference Object to Value)
如果一个对象的值不会发生变化,并且希望它在传递时被看作是不可变的值,可以将其从引用对象改为值对象。
public class ChangeReferenceObjectToValueExample {
public static void main(String[] args) {
PhoneNumber phoneNumber1 = new PhoneNumber("123", "4567890");
PhoneNumber phoneNumber2 = new PhoneNumber("123", "4567890");
System.out.println("Phone numbers are equal: " + phoneNumber1.equals(phoneNumber2));
}
}
class PhoneNumber {
private final String areaCode;
private final String number;
public PhoneNumber(String areaCode, String number) {
this.areaCode = areaCode;
this.number = number;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
PhoneNumber other = (PhoneNumber) obj;
return areaCode.equals(other.areaCode) && number.equals(other.number);
}
@Override
public int hashCode() {
return Objects.hash(areaCode, number);
}
}
在上述示例中,PhoneNumber 类被声明为一个不可变的值对象。通过重写 equals() 和 hashCode() 方法,我们可以比较两个电话号码对象的值是否相等。
以上是一些常见的重构手法示例,用于解决代码中存在的可变数据的坏味道。根据实际情况和需求,选择合适的重构手法来改善代码质量和可维护性。
7 发散式变化 Divergent Change
某个模块经常因为不同的原因在不同的方向上发生变化。
重要:“每次只关心一个上下文”
1. 拆分阶段(Split Phase)
拆分阶段的目的是将一个长函数或方法拆分成多个独立的阶段或步骤。
public class SplitPhaseExample {
public void processOrder(Order order) {
validateOrder(order);
calculateTotalPrice(order);
applyDiscount(order);
generateInvoice(order);
}
private void validateOrder(Order order) {
// 验证订单逻辑
}
private void calculateTotalPrice(Order order) {
// 计算总价逻辑
}
private void applyDiscount(Order order) {
// 应用折扣逻辑
}
private void generateInvoice(Order order) {
// 生成发票逻辑
}
public static void main(String[] args) {
SplitPhaseExample example = new SplitPhaseExample();
Order order = new Order(/* order details */);
example.processOrder(order);
}
}
在上述示例中,processOrder() 方法被拆分为多个独立的阶段,每个阶段负责不同的业务逻辑。通过拆分阶段,我们能够更好地组织代码,并使每个阶段专注于特定的功能。
2. 搬移函数(Move Function)
搬移函数的目的是将一个函数或方法从一个类中搬移到另一个相关的类中,使得函数和它所操作的数据在同一个类中。
public class MoveFunctionExample {
public static void main(String[] args) {
Circle circle = new Circle(5);
double area = circle.calculateArea();
System.out.println("Area: " + area);
}
}
class Circle {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
public double calculateArea() {
return Math.PI * radius * radius;
}
}
在上述示例中,calculateArea() 方法被搬移到 Circle 类中,因为计算面积是与圆形的特性紧密相关的功能。
3. 提炼函数(Extract Function)
提炼函数的目的是将一段代码拆分为一个独立的函数或方法,以便复用和更好地组织代码。
public class ExtractFunctionExample {
public static void main(String[] args) {
int[] numbers = {1, 2, 3, 4, 5};
int sum = sumNumbers(numbers);
System.out.println("Sum: " + sum);
}
public static int sumNumbers(int[] numbers) {
int sum = 0;
for (int number : numbers) {
sum += number;
}
return sum;
}
}
在上述示例中,原本在主函数中的求和逻辑被提炼为 sumNumbers() 方法,以便在其他地方重用该逻辑。
4. 提炼类(Extract Class)
提炼类的目的是将一个类中的部分功能拆分为一个新的相关类,以便更好地组织和管理代码。
public class ExtractClassExample {
public static void main(String[] args) {
Person person = new Person("John", "Doe", 30);
System.out.println("Full Name: " + person.getFullName());
System.out.println("Age: " + person.getAge());
}
}
class Person {
private String firstName;
private String lastName;
private int age;
public Person(String firstName, String lastName, int age) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
public String getFullName() {
return firstName + " " + lastName;
}
public int getAge() {
return age;
}
}
在上述示例中,原本在一个类中的人物相关信息被提炼到了 Person 类中,使得代码更加清晰和模块化。
8霰弹式修改 Shotgun Surgey
每当遇到某种变化,都要在许多不同的类内做出许多小修改。需要修改的代码散布四处,不但很难找到它们,也容易错过某个重要的修改。
1. 内联函数(Inline Function)
内联函数的目的是将函数调用处的代码替换为被调用函数的实际内容,以减少不必要的函数调用。
public class InlineFunctionExample {
public int calculateTotalPrice(int quantity, double price) {
return quantity * applyDiscount(price);
}
private double applyDiscount(double price) {
return price * 0.9;
}
public static void main(String[] args) {
InlineFunctionExample example = new InlineFunctionExample();
int totalPrice = example.calculateTotalPrice(10, 5.0);
System.out.println("Total Price: " + totalPrice);
}
}
在上述示例中,calculateTotalPrice() 方法内部调用了 applyDiscount() 方法,并将其返回值与 quantity 相乘得到总价。如果 applyDiscount() 方法的逻辑非常简单且只在一个地方被调用,我们可以将其内联到 calculateTotalPrice() 方法中,从而简化代码。
如果函数代码和函数的名称同样清晰易读,此时,应该去掉这个函数,直接使用其内部代码。通过内联手法,找出有用的中间层,将无用的中间层去除。
function getRating(driver) {
return moreThanFiveLateDelivers(driver)? 2 : 1;
}
function moreThanFiveLateDelivers(driver){
return driver.numberOfLateDelivers > 5;
}
===== >
function getRating(driver) {
return (driver.numberOfLateDelivers > 5) ? 2 : 1;
}
2. 内联类(Inline Class)
内联类的目的是将一个只包含少量成员的类的功能合并到使用它的地方,以减少类之间的依赖关系和维护成本。
public class InlineClassExample {
public static void main(String[] args) {
Order order = new Order("1234", "John Doe");
String customerId = order.getCustomerId();
System.out.println("Customer ID: " + customerId);
}
}
class Order {
private String orderId;
private String customerId;
public Order(String orderId, String customerId) {
this.orderId = orderId;
this.customerId = customerId;
}
public String getOrderId() {
return orderId;
}
public String getCustomerId() {
return customerId;
}
}
在上述示例中,Order 类只包含两个成员变量并提供了对应的获取方法。如果 Order 类只在一个地方被使用,我们可以将其内联到使用它的地方,以消除额外的类依赖。
需要注意的是,内联函数和内联类可能会导致代码重复,因此在进行重构时需要权衡代码的可读性、维护性和性能等方面的因素。
9依恋情节 Feature Envy
一个函数跟另一个模块中的函数或者数据交流格外频繁,远胜于自己所处模块内部的交流。这就是依恋情节的典型情况。
原则:将总是一起变化的东西放在一块儿
1. 移动函数(Move Method)
移动函数的目的是将一个函数从一个类移到它所依赖的类中,以减少跨类的频繁交流。
public class MoveMethodExample {
public static void main(String[] args) {
Customer customer = new Customer("John Doe");
double totalPrice = customer.calculateTotalPrice(10, 5.0);
System.out.println("Total Price: " + totalPrice);
}
}
class Customer {
private String name;
public Customer(String name) {
this.name = name;
}
public double calculateTotalPrice(int quantity, double price) {
return Order.calculateTotalPrice(quantity, price);
}
}
class Order {
public static double calculateTotalPrice(int quantity, double price) {
return quantity * price;
}
}
在上述示例中,Customer 类的 calculateTotalPrice() 方法频繁地调用了 Order 类的静态方法。为了减少依恋情节,我们将 calculateTotalPrice() 方法移动到 Order 类中,使得计算总价的逻辑与订单相关的数据在同一个类中。
2. 提炼函数(Extract Function)
提炼函数的目的是将一段代码提取成一个单独的函数,并将其放在所依赖的类中,以减少跨类的频繁交流。
public class ExtractFunctionExample {
public static void main(String[] args) {
Customer customer = new Customer("John Doe");
double totalPrice = customer.calculateTotalPrice(10, 5.0);
System.out.println("Total Price: " + totalPrice);
}
}
class Customer {
private String name;
public Customer(String name) {
this.name = name;
}
public double calculateTotalPrice(int quantity, double price) {
return calculateTotalPrice(quantity, price, getDiscountRate());
}
private double calculateTotalPrice(int quantity, double price, double discountRate) {
return quantity * price * (1 - discountRate);
}
private double getDiscountRate() {
// 获取折扣率的逻辑
return 0.1;
}
}
在上述示例中,Customer 类的 calculateTotalPrice() 方法依赖于另一个函数 getDiscountRate() 来获取折扣率。为了减少依恋情节,我们将计算总价的逻辑提取到一个单独的私有函数,并将其放在 Customer 类中。
需要注意的是,在进行重构时应根据实际情况权衡设计和代码的复杂性。有时候依恋情节是合理的,但如果出现过度的依赖关系,则需要考虑使用适当的重构手法来改善代码质量和可维护性。
10数据泥团 Data Clumps
常常在很多地方看到相同的三四项数据。两个类中相同的字段、许多函数签名中相同的参数。
1. 提炼类(Extract Class)
提炼类的目的是将相关联的数据字段和函数提取到一个新的类中,以减少重复的数据和函数。
public class ExtractClassExample {
public static void main(String[] args) {
Customer customer = new Customer("John Doe", "123 Main St", "New York");
customer.placeOrder("ABC123", 5);
}
}
class Customer {
private String name;
private String address;
private String city;
public Customer(String name, String address, String city) {
this.name = name;
this.address = address;
this.city = city;
}
public void placeOrder(String productId, int quantity) {
// 订单处理逻辑
Order order = new Order(productId, quantity, name, address, city);
// ...
}
}
class Order {
private String productId;
private int quantity;
private String customerName;
private String customerAddress;
private String customerCity;
public Order(String productId, int quantity, String customerName, String customerAddress, String customerCity) {
this.productId = productId;
this.quantity = quantity;
this.customerName = customerName;
this.customerAddress = customerAddress;
this.customerCity = customerCity;
}
// 其他订单相关方法...
}
在上述示例中,Customer 类和 Order 类中都包含相同的客户信息字段。为了消除数据泥团,我们将客户信息相关的字段和函数提取到一个新的 Order 类中,以便在订单处理时直接使用该类。
2. 提炼参数(Extract Parameter)
提炼参数的目的是将多个函数签名中相同的参数合并成一个对象参数,以减少重复的参数。
public class ExtractParameterExample {
public static void main(String[] args) {
Customer customer = new Customer("John Doe", "123 Main St", "New York");
customer.placeOrder(new Order("ABC123", 5));
}
}
class Customer {
private String name;
private String address;
private String city;
public Customer(String name, String address, String city) {
this.name = name;
this.address = address;
this.city = city;
}
public void placeOrder(Order order) {
// 订单处理逻辑
// ...
}
}
class Order {
private String productId;
private int quantity;
public Order(String productId, int quantity) {
this.productId = productId;
this.quantity = quantity;
}
// 其他订单相关方法...
}
在上述示例中,Customer 类的 placeOrder() 方法合并了原先包含客户信息的参数,并接受一个 Order 对象作为参数。这样可以减少重复的客户信息参数。
需要注意的是,在进行重构时应根据实际情况权衡设计和代码的复杂性。有时候数据泥团是合理的,但如果出现过度冗余的数据和函数,则需要考虑使用适当的重构手法来改善代码质量和可维护性。
11 基本类型偏执 Primitive Obsession
很多程序员不愿意创建对自己问题域有用的基本类型,如钱、坐标、范围等。
以对象取代基本类型:将原本单独存在的数据值替换为对象。
以子类取代类型码
1. 以对象取代基本类型(Replace Primitive with Object)
将原本单独存在的基本类型数据值替换为对象,以便在对象中封装更多相关的功能和行为。
public class ReplacePrimitiveWithObjectExample {
public static void main(String[] args) {
Money amount = new Money(100, "USD");
amount.add(50);
System.out.println("Amount: " + amount.getAmount());
System.out.println("Currency: " + amount.getCurrency());
}
}
class Money {
private int amount;
private String currency;
public Money(int amount, String currency) {
this.amount = amount;
this.currency = currency;
}
public void add(int value) {
amount += value;
}
public int getAmount() {
return amount;
}
public String getCurrency() {
return currency;
}
}
在上述示例中,使用 Money 类替代了原本的基本类型 int 和 String。Money 类封装了金额和货币类型,并提供了相关的功能方法,例如增加金额。
2. 以子类取代类型码(Replace Type Code with Subclasses)
如果代码中使用了类型码(Type Code)来表示不同的情况,可以将这些类型码替换为具体的子类,以便每个子类都能表示特定的情况,并提供相应的行为。
public class ReplaceTypeCodeWithSubclassesExample {
public static void main(String[] args) {
Shape circle = new Circle(5);
double circleArea = circle.calculateArea();
System.out.println("Circle Area: " + circleArea);
Shape rectangle = new Rectangle(3, 4);
double rectangleArea = rectangle.calculateArea();
System.out.println("Rectangle Area: " + rectangleArea);
}
}
abstract class Shape {
public abstract double calculateArea();
}
class Circle extends Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double calculateArea() {
return Math.PI * radius * radius;
}
}
class Rectangle extends Shape {
private double length;
private double width;
public Rectangle(double length, double width) {
this.length = length;
this.width = width;
}
@Override
public double calculateArea() {
return length * width;
}
}
在上述示例中,使用 Shape 类和具体的子类 Circle 和 Rectangle 替代了原本的类型码。每个子类都有自己特定的属性和行为,例如计算面积。
需要注意的是,在进行重构时应根据实际情况权衡设计和代码的复杂性。有时候基本类型的使用是合理的,但如果过度依赖基本类型并丧失了对领域概念的抽象能力,则需要考虑使用适当的重构手法来改善代码质量和可维护性。
12 重复的switch Repeated Switches
以多态取代条件表达式
public class RepeatedSwitchesExample {
public static void main(String[] args) {
Shape circle = new Circle(5);
Shape rectangle = new Rectangle(3, 4);
double circleArea = circle.calculateArea();
System.out.println("Circle Area: " + circleArea);
double rectangleArea = rectangle.calculateArea();
System.out.println("Rectangle Area: " + rectangleArea);
}
}
abstract class Shape {
public abstract double calculateArea();
}
class Circle extends Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double calculateArea() {
return Math.PI * radius * radius;
}
}
class Rectangle extends Shape {
private double length;
private double width;
public Rectangle(double length, double width) {
this.length = length;
this.width = width;
}
@Override
public double calculateArea() {
return length * width;
}
}
在上述示例中,使用多态来取代了原本的重复的 switch 语句。通过定义抽象基类 Shape 和具体子类 Circle 和 Rectangle,每个子类都覆盖了父类的 calculateArea() 方法,并提供了各自特定的实现。这样就不再需要使用 switch 来判断对象的类型并执行不同的逻辑。
使用多态的好处是能够更好地利用面向对象的特性,提高代码的可扩展性和可维护性。当需要添加新的形状时,只需要创建一个新的子类并实现相应的逻辑即可,而不必修改现有的代码。
根据具体情况,可以结合其他重构手法来进一步改善代码的设计和可读性,例如封装变量、提炼函数等。
13 循环语句 Loops
以管道取代循环:管道操作(如filter和map)可以帮助我们更快地看清被处理的元素以及处理它们的动作
import java.util.Arrays;
import java.util.List;
public class ReplaceLoopsWithStreamsExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// 使用循环计算平方和
int sumOfSquares = 0;
for (int number : numbers) {
int square = number * number;
sumOfSquares += square;
}
System.out.println("Sum of Squares (Loop): " + sumOfSquares);
// 使用管道操作计算平方和
int sumOfSquaresStream = numbers.stream()
.map(number -> number * number)
.reduce(0, Integer::sum);
System.out.println("Sum of Squares (Stream): " + sumOfSquaresStream);
}
}
在上述示例中,原本使用循环来计算列表中每个元素的平方,并将每个平方值相加得到总和。通过使用管道操作(stream),我们可以使用更简洁、表达力强的方式来处理这些元素。使用 map() 方法将每个元素进行平方运算,然后使用 reduce() 方法将平方值相加得到总和。
管道操作能够更直观地描述数据的转换和处理过程,使代码更具可读性和可维护性。此外,管道操作还能够更好地支持并行处理,提高代码的性能。
需要注意的是,对于某些复杂的逻辑或条件,可能仍然需要使用循环来处理。在使用管道操作时,要根据实际情况权衡代码的简洁性和性能等因素。
14 冗赘的元素 Lazy Element
1. 内联函数(Inline Function)
如果一个函数的实现和名字一样易读,可以考虑通过内联(Inline)操作,将该函数的实现直接替换到调用处,从而消除冗赘的函数。
public class InlineFunctionExample {
public static void main(String[] args) {
String name = "John Doe";
System.out.println("First Name: " + getFirstName(name));
}
public static String getFirstName(String fullName) {
return fullName.split(" ")[0];
}
}
在上述示例中,getFirstName() 方法只是简单地根据空格将全名拆分为姓和名,并返回姓氏。由于实现非常简单且函数名已经很明确,我们可以直接在调用处将其内联,消除了冗赘的函数。
2. 内联类(Inline Class)
如果一个类只包含一个函数,并且没有其他成员变量或函数依赖它,可以考虑通过内联(Inline)操作,将该类的函数实现直接合并到使用它的地方,从而消除冗赘的类。
java复制代码
public class InlineClassExample {
public static void main(String[] args) {
String firstName = Person.getFirstName("John Doe");
System.out.println("First Name: " + firstName);
}
}
class Person {
public static String getFirstName(String fullName) {
return fullName.split(" ")[0];
}
}
在上述示例中,Person 类只包含一个静态函数 getFirstName(),其实现和用途都非常简单。由于没有其他成员变量或函数依赖于该类,我们可以在使用它的地方直接调用函数,从而消除了冗赘的类。
3. 折叠继承关系(Collapse Hierarchy)
如果一个多余的类处于一个继承体系之中,可以考虑折叠继承关系(Collapse Hierarchy),将子类的内容合并到父类中,从而消除多余的类和继承关系。
java复制代码
public class CollapseHierarchyExample {
public static void main(String[] args) {
Shape shape = new Circle(5);
double area = shape.calculateArea();
System.out.println("Area: " + area);
}
}
abstract class Shape {
public abstract double calculateArea();
}
class Circle extends Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double calculateArea() {
return Math.PI * radius * radius;
}
}
在上述示例中,Circle 类继承自 Shape 类,并实现了计算面积的方法。然而,由于继承关系并没有提供额外的价值,我们可以将 Circle 类的内容直接合并到 Shape 类中,从而消除了多余的类和继承关系。
需要注意的是,在进行重构时应根据实际情况权衡设计和代码的复杂性。有时候冗赘的元素可能有其合理的存在原因,但如果它们没有提供额外的价值或复杂度不符合需求,则可以考虑使用适当的重构手法来改善代码质量和可维护性。
15 夸夸其谈通用性 Speculative Generality
- 如果抽象类没有太大的作用,请运用折叠继承体系;
- 不必要的委托和函数,就内联,消除掉。
- 如果函数的某些参数未被用到,改变函数声明去掉这些参数;
- 如果函数或者类只会被测试用例调用到,这就有了夸夸其谈通用性的坏味道,如果发现了这种函数或者类,可以先删除测试用例,然后移除死代码
16 临时字段 Temporary Field
临时字段要想办法挪走。可以提炼类或者搬移函数。
可以使用引入特例,在“变量”不合法的情况下创建一个替代对象,从而避免写出条件式代码。
public class TemporaryFieldExample {
private Object temporaryField;
public void processTemporaryField() {
if (temporaryField == null) {
temporaryField = createSpecialInstance();
}
// 执行与temporaryField相关的其他逻辑
}
private Object createSpecialInstance() {
// 创建特殊实例的逻辑
return new SpecialObject();
}
private static class SpecialObject {
// 特殊实例的实现
}
}
在上述示例中,我们定义了一个TemporaryFieldExample类,其中包含一个临时字段temporaryField。在processTemporaryField()方法中,我们首先检查temporaryField是否为null。如果是,则调用createSpecialInstance()方法创建一个特殊实例,并将其赋值给temporaryField。然后,我们可以执行与temporaryField相关的其他逻辑。
通过引入特例对象,我们避免了使用条件式代码来处理临时字段。这样做的好处是,我们可以减少对临时字段的各种判断和条件分支,使代码更加简洁和易于理解。
17 过长的消息链 Message Chains
用户向一个对象请求另一个对象,然后再向后者请求另一个对象,然后再请求另一个对象。这就是消息链。在实际代码中可能是一长串取值函数或者是一长串临时变量。
隐藏委托关系
当代码中存在过长的消息链时,可以使用隐藏委托关系的技巧来减少代码的复杂性和依赖性。以下是一个示例的Java代码实现:
public class MessageChainsExample {
private ClassA classA;
public String getValueFromClassA() {
// 隐藏委托关系,通过classA对象获取值
return classA.getValue();
}
}
public class ClassA {
private ClassB classB;
public String getValue() {
// 隐藏委托关系,通过classB对象获取值
return classB.getValue();
}
}
public class ClassB {
private ClassC classC;
public String getValue() {
// 隐藏委托关系,通过classC对象获取值
return classC.getValue();
}
}
public class ClassC {
private String value;
public String getValue() {
return value;
}
}
在上述示例中,我们创建了一个MessageChainsExample类,该类隐藏了委托关系。通过将代码逻辑封装在不同的类中,每个类只负责自己的职责,并通过委托调用其他类的方法来获取值。
通过隐藏委托关系,我们避免了过长的消息链,使得代码更加清晰和可维护。如果需要修改或扩展逻辑,只需关注特定的类而无需考虑整个消息链。
总结起来,隐藏委托关系是一种减少过长消息链的技巧,它可以降低代码的复杂性和依赖性,并提高代码的可读性和可维护性。
18 中间人 Middle Man
人们可能会过度使用委托。有时候会看到某个类的接口有一半的函数都委托给其他类。这就是过度应用。
当代码中存在中间人(Middle Man)时,也就是某个类的接口过度委托给其他类的情况,可以采取移除中间人的方式来简化代码。下面是一些在Java中实现移除中间人的方法:
- 直接与真正负责的对象打交道:将调用方直接与被委托对象进行通信,而不经过中间人。
// 调用端直接与真正负责的对象打交道
OriginalObject originalObject = new OriginalObject();
originalObject.doSomething();
- 内联函数:如果中间人只包含少数几个不起实际作用的函数,可以考虑使用内联函数(Inline Function)将这些函数放入调用端。
// 在调用端直接调用所需的函数,避免通过中间人委托
MiddleMan middleMan = new MiddleMan();
middleMan.someFunction(); // 将someFunction内联到调用端
- 以委托取代超类或以委托取代子类:如果中间人还有其他行为,并且需要扩展原对象的行为,可以通过"以委托取代超类"(Replace Delegation with Inheritance)或"以委托取代子类"(Replace Delegation with Subclass)的方式将中间人转变为真正的对象。
// 以委托取代超类
public class RealObject extends MiddleMan {
// 实现原来中间人的行为
}
// 以委托取代子类
public class RealObject extends OriginalObject {
private MiddleMan middleMan = new MiddleMan();
@Override
public void someFunction() {
// 添加其他行为
// 委托给中间人
middleMan.someFunction();
}
}
通过以上方法,我们可以移除中间人,并根据具体情况选择合适的方式。这样可以简化代码结构,降低耦合度,并提高代码的可读性和可维护性。
总结起来,移除中间人是一种减少过度应用委托的技巧,在Java中可以通过直接与被委托对象打交道、内联函数或以委托取代超类/子类等方式来实现。
19 内幕交易 Insider Trading
当两个模块有共同的兴趣,并且需要一个中介来处理它们之间的交互时,可以使用隐藏委托关系将另一个模块变成两者的中介。
继承关系可能导致密切耦合,因为子类对超类的了解通常超过了超类的意愿。如果您认为应该让子类独立存在,可以运用"以委托取代子类"或"以委托取代超类"的方式,使其脱离继承体系。以下是在Java中实现这些技巧的示例代码:
以委托取代子类(Replace Subclass with Delegation):
// 原本的子类
public class Subclass extends Superclass {
// 子类特定的逻辑和行为
}
// 使用委托的新类
public class DelegateClass {
private Superclass superclass;
public void delegatedMethod() {
// 委托给Superclass的方法
superclass.superclassMethod();
// 新逻辑和行为
}
}
以委托取代超类(Replace Superclass with Delegation):
// 原本的超类
public class Superclass {
public void superclassMethod() {
// 超类的方法实现
}
}
// 使用委托的新类
public class DelegateClass {
private Superclass superclass;
public void delegatedMethod() {
// 新逻辑和行为
// 委托给Superclass的方法
superclass.superclassMethod();
}
}
在以上示例中,我们通过创建一个新的类(DelegateClass)并将原来的子类或超类作为其成员变量,实现了以委托取代子类或以委托取代超类的效果。这样一来,新的类可以独立于继承体系进行运作,同时仍然可以访问原来子类或超类的功能。
通过使用委托,我们降低了模块之间的耦合度,并允许每个模块专注于自己的责任。这样做可以提高代码的可维护性和灵活性。
总结起来,当两个模块有共同兴趣且需要一个中介时,可以使用隐藏委托关系。如果您认为某个子类应该独立存在,可以运用"以委托取代子类";如果您认为某个超类不再适用于继承体系,可以运用"以委托取代超类"。这些技巧可以帮助减少密切耦合,并使代码更加灵活和可维护。
20 过大的类 Large Class
提炼类:将几个变量一起提炼至新类内。通常,类内数个变量有着相同的前缀或者后缀,这就意味着有机会把它们提炼到某个组件内。如果此组件适合作为一个子类,提炼超类和以子类型取代类型码比较简单。
观察一个大类的使用者,会找到拆分类的线索。看使用者是否只用到了这个类的功能的一个子集,这样,每个子集都能拆分成一个独立的类。
// 原始的大类
public class LargeClass {
private int commonVariable1;
private int commonVariable2;
private int specificVariable;
public void commonMethod1() {
// 公共方法1的实现
}
public void commonMethod2() {
// 公共方法2的实现
}
public void specificMethod() {
// 特定方法的实现
}
}
// 拆分出的新类
public class NewClass {
private int commonVariable1;
private int commonVariable2;
public void commonMethod1() {
// 公共方法1的实现
}
public void commonMethod2() {
// 公共方法2的实现
}
}
在上述示例中,我们将原始的大类(LargeClass)拆分成了两个类:一个是新类(NewClass),包含了共同的变量和方法;另一个是保留了特定方法的原始大类。通过这样的拆分,我们可以使代码更加模块化和可维护,每个类只关注自己的职责。
提炼类是一种有效的方式来应对过大的类问题,它可以提高代码的可读性、可维护性和重用性。根据具体情况,还可以考虑使用提炼超类或以子类型取代类型码等技巧来进一步优化代码结构。
21 异曲同工的类 Alternative Classes with Different Interfaces
当遇到具有相似功能但接口不同的类时,可以考虑将它们提炼成相同的接口。这样做可以增加代码的可重用性和灵活性,并简化代码的使用方式。以下是一个示例的Java代码实现:
// 原始的两个类,具有相似的功能但接口不同
public class ClassA {
public void methodA() {
// ClassA特定的逻辑
}
}
public class ClassB {
public void methodB() {
// ClassB特定的逻辑
}
}
// 提炼出的相同接口
public interface CommonInterface {
void commonMethod();
}
// 分别实现相同接口的两个类
public class ClassA implements CommonInterface {
@Override
public void commonMethod() {
// ClassA的实现
}
}
public class ClassB implements CommonInterface {
@Override
public void commonMethod() {
// ClassB的实现
}
}
在上述示例中,我们原本有两个具有相似功能但接口不同的类(ClassA和ClassB)。通过提炼出相同的接口(CommonInterface),我们创建了两个新的类(ClassA和ClassB)来分别实现这个接口。这样一来,我们可以将相同接口的方法进行统一调用,无论是操作ClassA还是ClassB的实例。
通过提炼成相同接口,我们提高了代码的可重用性和灵活性。我们可以使用相同的方式来操作不同的实现类,而不需要关心它们具体的类型。
总结起来,当遇到具有相似功能但接口不同的类时,可以考虑将它们提炼成相同的接口。这样做可以增加代码的可重用性和灵活性,并简化代码的使用方式。在Java中,我们可以通过创建一个相同接口并在不同的类中实现该接口来实现这一目标。
22 纯数据类
当遇到早期具有公共字段的纯数据类时,可以使用封装记录(Encapsulate Record)的技巧将这些字段封装起来。
如果一个类只包含数据而不涉及行为,那它可以被视为纯数据类。纯数据类往往意味着行为被放在了错误的地方。解决方法是将处理数据的行为从客户端搬移到纯数据类中。
对于不可修改的字段,无需进行封装,使用者可以直接通过字段获取数据,无需通过取值函数。以下是Java代码示例:
// 早期的具有公共字段的纯数据类
public class DataClass {
public String field1;
public int field2;
// ...
}
// 使用封装记录的纯数据类
public class EncapsulatedDataClass {
private String field1;
private int field2;
// ...
public EncapsulatedDataClass(String field1, int field2) {
this.field1 = field1;
this.field2 = field2;
}
public String getField1() {
return field1;
}
public int getField2() {
return field2;
}
}
在上述示例中,我们将早期的具有公共字段的纯数据类(DataClass)改造为使用封装记录的纯数据类(EncapsulatedDataClass)。通过添加私有字段并提供对应的取值函数,我们将字段封装起来,并限制了直接访问字段的权限。
对于可修改的字段,可以提供相应的设值函数(setter)来进行修改。这样一来,我们可以更好地控制数据的访问和修改,并提高代码的可维护性和安全性。
总结起来,当遇到早期具有公共字段的纯数据类时,可以使用封装记录的技巧将它们封装起来。对于不可修改的字段,无需封装,直接通过字段获取数据即可。通过封装记录,我们可以提高代码的可维护性和安全性,并更好地控制数据的访问和修改。
23 被拒绝的遗赠 Refused Bequest
当子类不想继承父类的某些数据或函数时,可以考虑创建一个兄弟类来接收不需要的成员,并将这些成员从子类中移动到兄弟类中。
如果子类复用了父类的行为,但又不愿意支持父类的接口,这就是被拒绝的遗赠(Refused Bequest)的情况。在这种情况下,使用以委托取代子类(Replace Subclass with Delegate)或以委托取代超类(Replace Superclass with Delegate)的方式可能更合适。
以下是Java代码示例:
// 原始的超类
public class Superclass {
public void commonMethod() {
// 公共方法的实现
}
public void specificMethod() {
// 特定方法的实现
}
}
// 子类不想继承的数据和函数移动到兄弟类中
public class BrotherClass {
// 兄弟类接收不需要的成员
// ...
}
// 子类复用父类行为但不想支持父类接口的情况,以委托取代子类
public class Subclass {
private BrotherClass brother;
public void commonMethod() {
// 子类自己的实现
}
public void specificMethod() {
brother.specificMethod();
}
}
在上述示例中,我们将子类不想继承的数据和函数移动到一个兄弟类(BrotherClass)中。对于子类复用父类行为但不想支持父类接口的情况,我们创建一个新的子类(Subclass)来继承父类,并使用以委托取代子类的方式,将需要复用的行为委托给兄弟类。
通过这样的重构,我们解决了被拒绝的遗赠问题,并提高了代码的灵活性和可维护性。
总结起来,当子类不想继承父类的某些数据或函数时,可以考虑创建一个兄弟类来接收不需要的成员。如果子类复用了父类的行为但不愿意支持父类的接口,可以使用以委托取代子类或以委托取代超类的方式来处理被拒绝的遗赠问题。这样可以提高代码的灵活性和可维护性。
24 注释
当我们发现一段代码有很多长长的注释时,可能意味着这些注释存在的原因是因为代码本身质量不佳。在这种情况下,我们可以尝试进行重构,以减少或消除对注释的依赖。
以下是一些常见的重构技巧,可以帮助我们降低代码的复杂性,从而减少注释的需求:
- 提取方法(Extract Method):将一段复杂的代码逻辑提取为一个独立的方法,并给该方法一个具有描述性的名称。这样可以使代码更加清晰易懂,减少注释的需要。
- 拆分类(Split Class):如果一个类承担了过多的职责,可以考虑将其拆分为多个单一职责的类。每个类只关注自己的领域,减少了代码的复杂性和注释的需求。
- 命名优化(Naming Optimization):使用清晰、具有描述性的命名来表达代码的意图,避免使用不明确或含糊的命名。良好的命名可以使代码更易于理解,减少对注释的依赖。
- 消除魔法数(Magic Number):将代码中的魔法数(硬编码的数字)替换为具有描述性的常量或枚举。这样可以使代码更具可读性,减少对注释的需求。
- 精简逻辑(Simplify Logic):优化复杂的条件语句和循环,使代码逻辑更加简洁明了。通过简化逻辑,我们可以减少对注释的依赖并提高代码的可读性。
通过以上重构技巧,我们可以改善代码的质量,降低其复杂性,并尽量减少或消除对注释的需求。好的代码应该是自解释的,清晰地传达其意图和功能,从而减少对注释的依赖。
总结起来,当我们感觉需要编写注释时,应先尝试进行重构。通过提取方法、拆分类、优化命名、消除魔法数和精简逻辑等方式,使代码更加清晰易懂,减少对注释的需要。这样可以提高代码质量和可维护性。
不必要的函数和变量干掉。