代码重构相关内容
分解大方法
在功能开发的时候,很多开发同学更喜欢在已有的逻辑后面继续添加自己的内容,持续下去后导致方法变得非常长。这种方法的可读性和可复用程度都很低。如何将哪些代码放到一个独立的方法中,这里考验的是开发者对业务的拆解能力。就像是我们写文章,我们会进行分段、分句。如果将所有内容不加任何标点符号堆叠在一起,不仅阅读困难,而且内容也无法被理解。
大方法的问题
一个大方法会产生很多问题。
- 职责模糊,一个大方法很大概率存在两个职责上的问题:多种平行的职责共存,多种嵌套职责共存。对于后续阅读者很难用一个准确的名字命名这个方法。
- 可读性差,当一个方法的代码过长或过于冗杂时,大量的变量声明和逻辑分支以及循环会导致在阅读某些逻辑的时候会被其他内容所干扰,进一步提高阅读难度。
- 代码复用度差,方法包含的内容越多,在开发类似功能时这段代码被复用的可能就越低。当开发同学实现新需求时,即使存在类似逻辑,但是因为这部分内容和其他逻辑混淆在一起,而导致开发同学无法对其进行复用。
如何判断需要提炼方法
- 代码复用的角度,如果方法中部分逻辑在其他方法中重复出现,可以考虑这部分内容是否可以提炼成一个独立的方法,并且使用一个合适的名字来描述它。
- 方法长度的角度,如果一个方法过长,不仅会使代码难以理解和维护。而且大方法很可能存在可以继续细分的职责。可以考虑方法内逻辑根据职责再次细分,或者将与其核心逻辑无关的代码独立出去。
- 复杂程度的角度,如果一个方法中包含大量条件控制语句,循环语句或者嵌套语句。可以考虑将控制语句部分、循环体、或者嵌套的循环分离出来。
- 职责唯一性的角度,如果一个方法内部逻辑很明显属于不同的业务内容,可以考虑其拆分成多个独立的方法,每个方法只负责单一的职责。
实际中,除了过长的方法以外,其他情况是否需要提炼方法,在考虑上面因素的时候还需要对代码所在的类进行整体考虑。过度使用提炼方法可能会导致代码变得更加复杂和难以理解,也会让类中的方法变得零散,因此需要根据实际情况进行灵活使用。
如何开始提炼方法
- 审查代码内容,确定其存在上面描述的问题。
- 根据上面问题来确定哪些代码片段需要被分离。并且为这段代码设定一个职责范围
- 将需要提炼的代码抽离到一个新的方法中
- 检查新的方法,那些参数需要源方法提供,而那些结果需要输出到源方法中。这里需要注意如果提炼出来的方法参数非常多,需要考虑是否进行参数对象化重构
- 在原方法中使用新方法,并传递适当的参数。需要确认新方法的返回值是否符合预期。
- 测试新方法
- 检查被提炼方法的内容是否在其他地方有类似调用,如果存在,则考虑抽取公共方法。
提炼方法的例子
public class OrderService {
public void createOrder(List<OrderItem> items, Customer customer) {
double totalAmount = 0.0;
for (OrderItem item : items) {
// 计算订单
}
if (totalAmount > 1000) {
// 优惠1
}
if (customer.isVip()) {
// 优惠2
}
Order order = new Order(items, customer, totalAmount);
saveOrder(order);
sendEmail(order);
printInvoice(order);
}
}
重构后
public class OrderService {
public void createOrder(List<OrderItem> items, Customer customer) {
double totalAmount = calculateTotalAmount(items);
applyDiscount(totalAmount, customer);
Order order = createOrder(items, customer, totalAmount);
saveOrder(order);
sendEmail(order);
printInvoice(order);
}
private double calculateTotalAmount(List<OrderItem> items) {
// 计算订单
return totalAmount;
}
private void applyDiscount(double totalAmount, Customer customer) {
// 优惠1
// 优惠2
}
private Order createOrder(List<OrderItem> items, Customer customer, double totalAmount) {
return new Order(items, customer, totalAmount);
}
}
在原始代码中createOrder()
方法中存在多个任务:计算订单总额、应用折扣、创建订单对象、保存订单、发送邮件和打印发票。如果将前面两部分提炼成单独的方法,在createOrder()
方法中,关于订单的执行序列就变得易于理解和修改。
移除方法
什么时候移除方法
我们将一部分类似逻辑的代码抽离成一个方法。是让我们代码能够清晰的理解,或者提高复用程度。随着代码的重构或者业务大幅度调整。一些冗余的内容被移除会让一些以前以为是很复杂的逻辑实际上发现却是很简单的处理。另外当一些业务进行调整的时候,因为业务的调整也会导致代码复用性消失,导致以前为了提高复用性而独立出来的逻辑显得没有必要了。总结下来如果出现下面的场景就需要考虑是否移除方法
- 方法已经不在被使用。
- 之前为了支持复用而新增的方法如今已经不在有重复地方使用。
- 随着变化方法内部已经很简单了,没有必要专门创建方法
如何移除方法
-
首先需要确定代码是否需要被移除,如果代码直接在调用方法中实现是否使调用方可读性大幅度降低。
-
找到这个方法调用者,将调用方法代码替换为方法内容
-
测试
-
删除过时方法。
改变方法声明
方法中哪些内容可能变化
随着业务变化,一些方法结构也在发生变化。为了保证方法更加通用、灵活、易于使用我们会修改方法的内容来适应需求。
-
修改方法名称,一个好的方法名,让我们第一时间了解到方法的在业务中的作用,让我们更快的理解业务逻辑。很多时候我们没办法第一时间想到一个好的名字,这是很正常的。这个时候可以给方法补充注释,在注释中描述方法的作用,在完成这些后,再去思考如何起名可能会更好。
-
修改参数,如果你需要在方法中添加新的功能或数据,你可以添加一个新参数来传递这些信息。一个好的方法应该是只需要自己使用到的参数,如果这个参数在后续已经不在使用需要坚决的删除掉。
-
修改参数的访问控制,private、protected、public各自有自己的控制范围。很多抽离出来的方法,仅仅是作为某个方法的某些步骤被抽离出来,这些方法不应该在其他地方被使用。而一些超类中的方法使用范围也应该是有范围的。通过这些控制,也可以观察一个方法承担责任的范围。在访问控制上要坚持最低访问权限的原则,如果不是必要就不要让其他人访问。
判断是否需要修改方法声明
当方法涉及的业务发生变化,内部逻辑出现调整的时候就需要考虑,方法名称是否可以被修改的前提,仔细了解方法内容,只有明白方法里面到底做了什么,才清楚方法的名称是否正常。
现在想找出参数是否无人使用非常简单,很多IDE软件都提供了对参数未使用的提醒,方法中未使用的字段都会被标记出来,一定不要忽略这些标记。通过IDE也可以找到所有调用方法的地方。如果某个字段不被使用,一定要及时移除,否则对于调用者来说一个不被使用但是却不得不传递的参数,会让人根据很糊涂的。
对于访问控制,如果一个方法只是为类内部的方法提供支持,那么需要设置为private
,如果方法在一个存在子类的父类中,且你希望这些父类方法被子类所使用,那么可以使用protected
。在实际开发中访问控制权限,如果可以尽量设置的访问范围小一些。
固定值方法参数化
其目的是当方法中出现常量值,可以将值提取为参数进行传递,从而增加代码的灵活性和可重用性。
将方法中固定值提升为参数需要考虑下面的情况
- 当方法中有多个常量值,它们可能会随着需求的变化而变化时,可以将它们提取为参数,以便在方法调用时进行传递。
- 当方法需要处理不同的情况时,可以将这些情况作为参数传递给方法,从而减少方法的复杂度。
- 当需要将方法重构为通用方法时,可以使用参数化方法来传递不同的参数,以便在不同的上下文中使用。
提取参数的步骤
- 识别要参数化的常量值,对于硬编码的常量值。它们被用于定义方法行为中的某些方面,但却不应该作为方法行为的一部分。
- 将常量值转换为方法参数:一旦我们识别了要参数化的常量值,我们需要将它们转换为方法参数。
- 同步更新调用此方法地方的参数传递
固定值方法参数化的示例代码
// 没有使用函数参数化重构前的代码
public double calculateTotalPrice(int itemCount, double price) {
double taxRate = 0.08;
double shippingCost = 5.0;
double subtotal = itemCount * price;
double tax = subtotal * taxRate;
double totalPrice = subtotal + tax + shippingCost;
return totalPrice;
}
// 使用函数参数化重构后的代码
public double calculateTotalPrice(int itemCount, double price, double taxRate, double shippingCost) {
double subtotal = itemCount * price;
double tax = subtotal * taxRate;
double totalPrice = subtotal + tax + shippingCost;
return totalPrice;
}
// 调用新函数
System.out.println(calculateTotalPrice(5, 20, 0.08, 5.0)); // 121.0
在重构后的代码中,我们将之前的常量值shippingCost
提取为方法参数,并在方法调用时传递它们的值。这使得代码更具可读性和可维护性。
移除标记参数
其目的是方法的标识参数,并将其拆分为两个或多个具有更明确目的的参数。标识参数通常是布尔类型,其用途是控制方法的行。
使用标记参数区分逻辑
实现业务不同分支的时候,创建一些type
值。这些类型提供了不同的分支,有时候我们会将这些类型值或者布尔值传递到更深的方法中进行分支切换。使用标记参数我们可以让一个方法实现多种逻辑分支,提高方法复用程度,但是这也导致方法中承担的职责也变多了。代码的可读性也会变得很差。
使用标记参数会产生下面问题
- 作为接收标记参数的方法,标记字段的赋值和调用它的地方是分离的,调用标记参数的方法里是无法知道这些值可能出现范围。如果想知道需要去更上层的代码查看,而更上层的代码中不存在使用它的代码。
- 另外一个情况是无法知道这些标记参数的含义或者为什么来。尤其是布尔值,我们不知道什么情况会出现true什么时候出现false,也不知道这些值代表的含义。因为所有的内容都在更上层的内容中
而移除标记参数就是为了解决这些情况,将代码逻辑根据标记参数对分支的控制,创建对应的方法,每个方法执行对应逻辑分支内的逻辑,在传递标记参数的地方就直接调用对应的方法即可。
在出现下面情况的时候,就需要考虑是否通过移除标记参数来优化代码。
- 方法中是否存在大量标记方法,这将导致逻辑被切割的非常零散。
- 方法中穿插大量基于标记方法进行判断的分支切换。
- 因为标记方法在存在,在后续对方法进行扩展时,变得非常困难。
如果上面任意一种情况出现就需要考虑是否需要使用移除标记参数方式重构代码
如何进行操作
- 检查标记参数,分析标记参数在方法内存在的不同的逻辑分支。
- 为每个逻辑分支提供单独的方法。
- 将根据传入值进入不同的逻辑分支,修改为调用不同的方法。
移除标记方法后的例子
public double calculateTotalPrice(boolean calculateDiscountedPrice) {
double totalPrice = 0.0;
for (Product product : products) {
if (calculateDiscountedPrice) {
totalPrice += product.getDiscountedPrice();
} else {
totalPrice += product.getPrice();
}
}
return totalPrice;
}
就像上面的例子中,使用标记参数的地方和创建他的地方被分离
public double calculateTotalPrice() {
double totalPrice = 0.0;
for (Product product : products) {
totalPrice += product.getPrice();
}
return totalPrice;
}
public double calculateTotalDiscountedPrice() {
double totalPrice = 0.0;
for (Product product : products) {
totalPrice += product.getDiscountedPrice();
}
return totalPrice;
}
重构后将原来的逻辑拆分成两个方法。在方法中不需要考虑标记方法的含义,因为方法中只存在单一场景的逻辑。这时只需要给方法提供一个合理的名字,就能大大提高代码可读性。