在软件开发的过程中,我们为了实现某个功能,往往都要去编译无数的函数,然后把函数组合起来完成功能,但有时候,为了图方便,在一个函数中,写了很长的业务逻辑,导致一个函数的长度超过了我们的显示器,这个时候,就要学会去分析和重构它。
一、什么是过长函数
定义
一个函数包含了过多的逻辑或者过分体现了逻辑功能的实现细节,导致函数产生过长的代码块,简单来说,过长函数就是很长的函数。(听君一席话,如听一席话)
影响
- 功能不单一:一般一个函数,最好做到功能单一,也就是一个函数只做一件事,功能单一的函数往往长度都不会太长,如果一个函数长度太长,很有可能这个函数功能不单一;
- 可读性差:函数过长,往往都会造成可读性差,如果一个函数超过了显示屏的显示范围,一般这个函数读起来往往会令人头疼;
- 难以维护: 在一段很长的函数中去该某段代码,维护成本非常的高;
重构过长函数的目的
增强代码的可读性,提高维护性,使函数功能单一。
重构手段
过长的函数,一般都是采用提炼函数的方法,并用函数做什么来进行重新命名;重构过长函数的难点在于,对一个函数的功能进行分析和拆分。
二、案例
2.1 案例一
2.1.1 代码功能
下面是一个打印发票的的函数,发票内容主要包括:
- 发票头,包含买方和卖方信息。
- 商品详细列表,包含商品的编号、名称、数目和价格
- 发票汇总,包含所有商品的总价。
public class Invoice {
private final String buyer;
private final String seller;
private final List<InvoiceLine> lines;
public Invoice(String buyer, String seller, List<InvoiceLine> lines) {
this.buyer = buyer;
this.seller = seller;
this.lines = new ArrayList<>(lines);
}
public String getBuyer() {
return buyer;
}
public String getSeller() {
return seller;
}
public List<InvoiceLine> getLines() {
return Collections.unmodifiableList(lines);
}
}
public class InvoiceLine {
private final String product;
private final float quantity;
private final float price;
public InvoiceLine(String product, int quantity, int price) {
this.product = product;
this.quantity = quantity;
this.price = price;
}
public String getProduct() {
return product;
}
public float getQuantity() {
return quantity;
}
public float getPrice() {
return price;
}
}
public class InvoicePrinter {
public static void print(Invoice invoice, PrintStream printStream) {
printStream.println("====== INVOICE ======");
printStream.println("Buyer: " + invoice.getBuyer());
printStream.println("Seller: " + invoice.getSeller());
printStream.println("------ Detail ------");
printStream.println("ln\tProduct\tPrice\tQt\tAmount");
float total = 0;
for (int lineId = 0; lineId < invoice.getLines().size(); ++lineId) {
final InvoiceLine line = invoice.getLines().get(lineId);
String product = line.getProduct();
float price = line.getPrice();
float quantity = line.getQuantity();
float amount = price * quantity;
if (product == null || "".equals(product.trim())
|| price <= 0 || quantity <= 0) {
printStream.println(lineId + "\tInvalid");
} else {
printStream.println(lineId + "\t" + product + "\t" + price
+ "\t" + quantity + "\t" + amount);
}
total += amount;
}
printStream.println("------ Total ------");
printStream.println(total);
}
}
运行效果如下:
2.1.2 问题分析
- 代码过长,所有逻辑都在一个print方法中,这个方法过分体现细节。
- for循环功能复杂,它既要获取数据,然后打印,同时还要计算商品总价
- if语句判断商品是否有效,表达式比较复杂。
- for循环中关于商品的临时变量太多。
- 类功能不够内聚,这个类的方法是print,应该专注于打印,计算总价不应该是打印机的职责。
功能模块分析
1、for循环打印和计算抽离
2、发票分为打印头,打印细节、打印价格,可以考虑根据三处不同的打印,拆分为三个函数,重构时选中对应的代码段,使用Ctrl+Shift+M,快捷重构
3、接下来依次重构子方法,printHeader已经很简单了,没有重构的必要,主要重构printDetail方法,这个方法比较长,很多一部分原因是因为for循环的临时变量过多,所以想办法删除临时变量,这里删除临时变量的方法是:通过查询取代临时变量和通过功能内聚取消临时变量,如图:
4、代码中的if判断此商品是否有效,可以把判断的语句内聚到line的方法中
5、for循环中,根据打印每行line对象的代码,也可以提炼到一个函数中专门负责每行的打印
6、最后重构printFooter方法,printFoot方法用于打印总价,这里使用功能内聚的方法,把计算总价的方法定义到Invoice对象中,printFooter方法直接取值就可以了。
重构后的代码如下:
public class Invoice {
private final String buyer;
private final String seller;
private final List<InvoiceLine> lines;
public Invoice(String buyer, String seller, List<InvoiceLine> lines) {
this.buyer = buyer;
this.seller = seller;
this.lines = new ArrayList<>(lines);
}
public float calcAmount() {
float total = 0;
for (int lineId = 0; lineId < getLines().size(); ++lineId) {
final InvoiceLine line = getLines().get(lineId);
total += line.getAmount();
}
return total;
}
public String getBuyer() {
return buyer;
}
public String getSeller() {
return seller;
}
public List<InvoiceLine> getLines() {
return Collections.unmodifiableList(lines);
}
}
public class InvoiceLine {
private final String product;
private final float quantity;
private final float price;
public InvoiceLine(String product, int quantity, int price) {
this.product = product;
this.quantity = quantity;
this.price = price;
}
boolean isValid() {
return product == null || "".equals(product.trim())
|| price <= 0 || quantity <= 0;
}
public String getProduct() {
return product;
}
public float getQuantity() {
return quantity;
}
public float getPrice() {
return price;
}
public float getAmount() {
return price * quantity;
}
}
public class InvoicePrinter {
public static void print(Invoice invoice, PrintStream printStream) {
printHeader(invoice, printStream);
printDetail(invoice, printStream);
printFooter(invoice, printStream);
}
public static void printFooter(Invoice invoice, PrintStream printStream) {
printStream.println("------ Total ------");
printStream.println(invoice.calcAmount());
}
public static void printDetail(Invoice invoice, PrintStream printStream) {
printStream.println("------ Detail ------");
printStream.println("ln\tProduct\tPrice\tQt\tAmount");
for (int lineId = 0; lineId < invoice.getLines().size(); ++lineId) {
final InvoiceLine line = invoice.getLines().get(lineId);
printInvoiceLine(printStream, lineId, line);
}
}
public static void printInvoiceLine(PrintStream printStream, int lineId, InvoiceLine line) {
if (line.isValid()) {
printStream.println(lineId + "\tInvalid");
} else {
printStream.println(lineId + "\t" + line.getProduct() + "\t" + line.getPrice()
+ "\t" + line.getQuantity() + "\t" + line.getAmount());
}
}
public static void printHeader(Invoice invoice, PrintStream printStream) {
printStream.println("====== INVOICE ======");
printStream.println("Buyer: " + invoice.getBuyer());
printStream.println("Seller: " + invoice.getSeller());
}
}
2.2 案例2
这是一个保持数据的方法,不同的数据类型,选中不同的保存方式。
问题:switch语句太长,而且随着数据类型增大,switch也需要不停的扩展。
解决方法:将每个switch,抽取为一个方法。