定义

一个函数跟另一个模块中的函数或数据交流格外频繁,远胜于在自己所处模块内部的交流

影响

  • 可读性、可维护性低:调用另一模块功能时往往需要打一套组合拳才能完成,需要知道过多的细节;往往会伴随有“内幕交易、 重复代码、霰弹式修 改……”

改进目标

  • 将函数搬移到对应的类,解除跨模块的过多交流

方法

  • 提炼函数
  • 搬移函数

案例:问题代码

代码背景

  • 发票(Invoice)有买方、卖方、还有多个明细行;
  • 明细行(InvoiceLine)有商品名称、单价、数量;
  • 发票打印器(InvoiceFormatter)将发票信息格式化后输出, 其中将包含总金额和各明细行金额。

症状/问题

InvoiceFormatter知道了过多关于Invoice和InvoiceLine的细节。 通过Invoice/InvoiceLine提供的属性,自行计算了金额:

  • 计算了InvoiceLine的总额(利用了InvoiceLine的2个方法,并带入了multiply方法的使用)
  • 计算了Invoice的总额(利用了Invoice/InvoiceLine的3个方法)
  • 万一,今后Invoice加上个折扣率,那么这些地方很难支持
/**
 * 发票信息
 */
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 BigDecimal quantity;

    private final BigDecimal price;

    public InvoiceLine(String product, BigDecimal quantity, BigDecimal price) {
        this.product = product;
        this.quantity = quantity;
        this.price = price;
    }

    public String getProduct() {
        return product;
    }

    public BigDecimal getQuantity() {
        return quantity;
    }

    public BigDecimal getPrice() {
        return price;
    }
}
/**
 * 发票格式化
 */
public class InvoiceFormatter {
    /**
     * 数值格式化
     */
    private final DecimalFormat decimalFormat;

    private final String lineSeparator;

    /**
     * @param precision 格式化精度,即小数点后位数(大于等于0)
     * @param lineSeparator 换行符
     */
    public InvoiceFormatter(int precision, String lineSeparator) {
        if (precision < 0) {
            throw new IllegalArgumentException();
        }
        StringBuilder pattern = new StringBuilder("0.");
        for (int p = 0; p < precision; p++) {
            pattern.append("0");
        }

        this.decimalFormat = new DecimalFormat(pattern.toString());
        this.lineSeparator = lineSeparator;
    }

    /**
     * 格式化发票信息
     * 
     * @param invoice 发票
     * @return 格式化结果
     */
    public final String format(Invoice invoice) {
        StringBuilder stringBuilder = new StringBuilder();

        // header:
        stringBuilder.append("Seller: ")
            .append(invoice.getSeller())
            .append(lineSeparator)
            .append("Buyer: ")
            .append(invoice.getBuyer())
            .append(lineSeparator);

        // body:
        stringBuilder.append("Details:(line,product,price,quantity,amount)").append(lineSeparator);
        int lineIndex = 1;
        for (InvoiceLine line : invoice.getLines()) {
            stringBuilder.append(lineIndex++)
                .append(". ")
                .append(line.getProduct())
                .append(" ")
                .append("$")
                .append(decimalFormat.format(line.getPrice()))
                .append(" ")
                .append(decimalFormat.format(line.getQuantity()))
                .append(" ")
                .append("$")
                .append(decimalFormat.format(line.getQuantity().multiply(line.getPrice())))
                .append(lineSeparator);
        }

        // footer
        BigDecimal total = BigDecimal.ZERO;
        for (InvoiceLine line : invoice.getLines()) {
            BigDecimal lineTotal = line.getQuantity().multiply(line.getPrice());
            total = total.add(lineTotal);
        }
        stringBuilder.append("Total: $").append(decimalFormat.format(total)).append(lineSeparator);

        return stringBuilder.toString();
    }
}

改进手法:提炼函数并搬移

/**
 * 发票信息
 */
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);
    }

    BigDecimal getAmount() {
        BigDecimal total = BigDecimal.ZERO;
        for (InvoiceLine line : getLines()) {
            BigDecimal lineTotal = line.getAmount();
            total = total.add(lineTotal);
        }
        return total;
    }
}
/**
 * 发票单行明细
 */
public class InvoiceLine {
    private final String product;

    private final BigDecimal quantity;

    private final BigDecimal price;

    public InvoiceLine(String product, BigDecimal quantity, BigDecimal price) {
        this.product = product;
        this.quantity = quantity;
        this.price = price;
    }

    public String getProduct() {
        return product;
    }

    public BigDecimal getQuantity() {
        return quantity;
    }

    public BigDecimal getPrice() {
        return price;
    }

    BigDecimal getAmount() {
        return getQuantity().multiply(getPrice());
    }
}
/**
 * 发票格式化
 */
public class InvoiceFormatter {
    /**
     * 数值格式化
     */
    private final DecimalFormat decimalFormat;

    private final String lineSeparator;

    /**
     * @param precision 格式化精度,即小数点后位数(大于等于0)
     * @param lineSeparator 换行符
     */
    public InvoiceFormatter(int precision, String lineSeparator) {
        if (precision < 0) {
            throw new IllegalArgumentException();
        }
        StringBuilder pattern = new StringBuilder("0.");
        for (int p = 0; p < precision; p++) {
            pattern.append("0");
        }

        this.decimalFormat = new DecimalFormat(pattern.toString());
        this.lineSeparator = lineSeparator;
    }

    /**
     * 格式化发票信息
     * 
     * @param invoice 发票
     * @return 格式化结果
     */
    public final String format(Invoice invoice) {
        StringBuilder stringBuilder = new StringBuilder();

        // header:
        stringBuilder.append("Seller: ")
            .append(invoice.getSeller())
            .append(lineSeparator)
            .append("Buyer: ")
            .append(invoice.getBuyer())
            .append(lineSeparator);

        // body:
        stringBuilder.append("Details:(line,product,price,quantity,amount)").append(lineSeparator);
        int lineIndex = 1;
        for (InvoiceLine line : invoice.getLines()) {
            stringBuilder.append(lineIndex++)
                .append(". ")
                .append(line.getProduct())
                .append(" ")
                .append("$")
                .append(decimalFormat.format(line.getPrice()))
                .append(" ")
                .append(decimalFormat.format(line.getQuantity()))
                .append(" ")
                .append("$")
                .append(decimalFormat.format(line.getAmount()))
                .append(lineSeparator);
        }

        // footer
        BigDecimal total = invoice.getAmount();
        stringBuilder.append("Total: $").append(decimalFormat.format(total)).append(lineSeparator);

        return stringBuilder.toString();
    }
}

操作手法

操作快捷键(推荐)Ctrl+Alt+Shift+T(或:鼠标右键“Refactor”)
提炼函数Ctrl+Alt+MExtract Method
移动函数F6Move Instance Method

补充: IDEA “Inspect Code” 需勾选配置项: Setting -> Editor -> Inspections -> Java -> Abstraction issues -> Feature envy 功能入口: IDEA -> Analyze -> Inspect Code