三、一个重构案例分享

216 阅读5分钟

了解了重构技巧和代码的坏味道,可能还是觉得缺少实际重构的经验。在这里分享一个觉得很不错的重构案例,通过这个案例,可以学习到如何在代码中将重构实际应用起来的。

案例讲解

重构终归是需要我们实际上手操作的,所以最快的学习方法就是亲眼目睹整个重构的过程,然后再加以模仿和学习,所以这里请参考原视频讲解:[JetBrains 网络研讨会] 重构还是重写?聊聊 Java 代码臭味与重构技巧_哔哩哔哩_bilibili

为了方便学习和模仿,下面贴出了重构前和重构后的代码,并整理了一些常用的重构快捷键。

问题背景

如图所示,我们对月份指定了一些预算,比如5月份指定了310的预算,那5月份每天的预算就为10。现在需求是给定一个日期范围区间 ,我们要计算指定区间内的预算金额是多少,比如上图中5月20日~8月8日的预算为7020。

重构前代码

BudgetPlan.class

import java.time.LocalDate;
import java.time.YearMonth;
import java.util.List;
import java.util.stream.Collectors;

import static java.time.temporal.ChronoUnit.DAYS;

public class BudgetPlan {
    private final BudgetRepo repo;

    public BudgetPlan(BudgetRepo repo) {
        this.repo = repo;
    }

    public long query(LocalDate startDate, LocalDate endDate) {
        //If Start and End are in the same budget period
        if (startDate.withDayOfMonth(1).equals(endDate.withDayOfMonth(1))) {
            long amount = getBudgetAmount(startDate);
            long daysInPeriod = getBudgetDaysCount(startDate);
            long daysBetween = startDate.until(endDate, DAYS) + 1;
            return amount / daysInPeriod * daysBetween;
        }

        // If the area between Start and End overlap at least two budget periods.
        if (YearMonth.from(startDate).isBefore(YearMonth.from(endDate))) {
            long amountStartPeriod = getBudgetAmount(startDate);
            long daysInStartPeriod = getBudgetDaysCount(startDate);
            long daysAfterStartDateInStartPeriod = startDate.until(startDate.withDayOfMonth(startDate.lengthOfMonth()), DAYS) + 1;
            long totalStartPeriod = amountStartPeriod / daysInStartPeriod * daysAfterStartDateInStartPeriod;

            long totalInMiddle = 0;
            for (Budget budget : getBudgetBetween(startDate, endDate)) {
                totalInMiddle += budget.getAmount();
            }

            long amountEndPeriod = getBudgetAmount(endDate);
            long daysInEndPeriod = getBudgetDaysCount(endDate);
            long daysBeforeEndDateInEndPeriod = endDate.getDayOfMonth();
            long totalEndPeriod = amountEndPeriod / daysInEndPeriod * daysBeforeEndDateInEndPeriod;

            return totalStartPeriod + totalInMiddle + totalEndPeriod;
        }

        throw new RuntimeException("You should not be here.");
    }

    private long getBudgetDaysCount(LocalDate date) {
        Budget budget = getBudgetContaining(date);
        return budget.getMonth().lengthOfMonth();
    }

    private Budget getBudgetContaining(LocalDate date) {
        List<Budget> budgets = repo.findAll();
        return budgets.stream()
            .filter(budget -> budget.getMonth().atDay(1).equals(date.withDayOfMonth(1)))
            .findFirst()
            .orElse(new Budget(YearMonth.of(date.getYear(), date.getMonth()), 0));
    }

    private long getBudgetAmount(LocalDate date) {
        Budget budget = getBudgetContaining(date);
        return budget.getAmount();
    }

    private List<Budget> getBudgetBetween(LocalDate startDate, LocalDate endDate) {
        List<Budget> budgets = repo.findAll();
        return budgets.stream()
            .filter(budget -> budget.getMonth().atDay(1).isAfter(startDate) && budget.getMonth().atEndOfMonth().isBefore(endDate))
            .collect(Collectors.toList());
    }
}

Budget.class

import java.time.YearMonth;

public class Budget {
    private final YearMonth month;
    private final long amount;

    public Budget(YearMonth month, long amount) {
        this.month = month;
        this.amount = amount;
    }

    public YearMonth getMonth() {
        return month;
    }

    public long getAmount() {
        return amount;
    }
}

BudgetRepo.class

import java.util.List;

public interface BudgetRepo {
    List<Budget> findAll();
}

BudgetPlanTest.class

import org.junit.jupiter.api.Test;

import java.time.LocalDate;
import java.time.YearMonth;
import java.util.Arrays;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

class BudgetPlanTest {
    private final BudgetRepo repo = mock(BudgetRepo.class);
    private final BudgetPlan plan = new BudgetPlan(repo);

    @Test
    public void noBudget() {
        givenBudgets();
        assertEquals(0, plan.query(LocalDate.of(2019, 10, 4), LocalDate.of(2019, 11, 5)));
    }

    @Test
    public void queryWholeMonth() {
        givenBudgets(new Budget(YearMonth.of(2019, 10), 3100));
        assertEquals(3100, plan.query(LocalDate.of(2019, 10, 1), LocalDate.of(2019, 10, 31)));
    }

    @Test
    public void queryOneDayWithinOneMonth() {
        givenBudgets(new Budget(YearMonth.of(2019, 10), 3100));
        assertEquals(100, plan.query(LocalDate.of(2019, 10, 3), LocalDate.of(2019, 10, 3)));
    }

    @Test
    public void queryTwoDayWithinOneMonth() {
        givenBudgets(new Budget(YearMonth.of(2019, 10), 3100));
        assertEquals(200, plan.query(LocalDate.of(2019, 10, 3), LocalDate.of(2019, 10, 4)));
    }

    @Test
    public void queryBeforeBudget() {
        givenBudgets(new Budget(YearMonth.of(2019, 10), 3100));
        assertEquals(400, plan.query(LocalDate.of(2019, 9, 25), LocalDate.of(2019, 10, 4)));
    }

    @Test
    public void queryAfterBudget() {
        givenBudgets(new Budget(YearMonth.of(2019, 10), 3100));
        assertEquals(400, plan.query(LocalDate.of(2019, 10, 28), LocalDate.of(2019, 11, 4)));
    }

    @Test
    public void queryOutOfBudget() {
        givenBudgets(new Budget(YearMonth.of(2019, 10), 3100));
        assertEquals(0, plan.query(LocalDate.of(2019, 9, 1), LocalDate.of(2019, 9, 24)));
    }

    @Test
    public void queryMultiBudget() {
        givenBudgets(new Budget(YearMonth.of(2019, 10), 3100),
                new Budget(YearMonth.of(2019, 11), 3000));
        assertEquals(2000, plan.query(LocalDate.of(2019, 10, 20), LocalDate.of(2019, 11, 8)));
    }

    private void givenBudgets(Budget... budgets) {
        when(repo.findAll()).thenReturn(Arrays.asList(budgets));
    }
}

重构后代码

BudgetPlan.class

public class BudgetPlan {
    private final BudgetRepo repo;

    public BudgetPlan(BudgetRepo repo) {
        this.repo = repo;
    }

    public long query(Period period) {
        return repo.findAll().stream()
                .mapToLong(budget -> budget.getOverlappingAmount(period))
                .sum();
    }
}

Budget.class

import java.time.LocalDate;
import java.time.YearMonth;

public class Budget {
    private final YearMonth month;
    private final long amount;

    public Budget(YearMonth month, long amount) {
        this.month = month;
        this.amount = amount;
    }

    public YearMonth getMonth() {
        return month;
    }

    public long getAmount() {
        return amount;
    }

    @Override
    public boolean equals(Object obj) {
        Budget another = (Budget) obj;
        return month.equals(another.month);
    }

    LocalDate getEnd() {
        return month.atEndOfMonth();
    }

    LocalDate getStart() {
        return month.atDay(1);
    }

    long getDaysCount() {
        return getMonth().lengthOfMonth();
    }

    public Period getPeriod() {
        return new Period(getStart(), getEnd());
    }

    long getOverlappingAmount(Period period) {
        return this.amount / getDaysCount() * period.getOverlappingDaysCount(getPeriod());
    }
}

Period.class

import java.time.LocalDate;

import static java.time.temporal.ChronoUnit.DAYS;

public class Period {
    private final LocalDate start;
    private final LocalDate end;

    public Period(LocalDate start, LocalDate end) {
        this.start = start;
        this.end = end;
    }

    long getDaysCount() {
        return start.isAfter(end) ? 0 : start.until(end, DAYS) + 1;
    }

    long getOverlappingDaysCount(Period another) {
        LocalDate startOfOverlapping = start.isAfter(another.start) ? start : another.start;
        LocalDate endOfOverlapping = end.isBefore(another.end) ? end : another.end;
        return new Period(startOfOverlapping, endOfOverlapping).getDaysCount();
    }
}

BudgetRepo.class

import java.util.List;

public interface BudgetRepo {
    List<Budget> findAll();
}

BudgetPlanTest.class

import org.junit.jupiter.api.Test;

import java.time.LocalDate;
import java.time.YearMonth;
import java.util.Arrays;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

class BudgetPlanTest {
    private final BudgetRepo repo = mock(BudgetRepo.class);
    private final BudgetPlan plan = new BudgetPlan(repo);

    @Test
    public void noBudget() {
        givenBudgets();
        assertEquals(0, plan.query(new Period(LocalDate.of(2019, 10, 4), LocalDate.of(2019, 11, 5))));
    }

    @Test
    public void queryWholeMonth() {
        givenBudgets(new Budget(YearMonth.of(2019, 10), 3100));
        assertEquals(3100, plan.query(new Period(LocalDate.of(2019, 10, 1), LocalDate.of(2019, 10, 31))));
    }

    @Test
    public void queryOneDayWithinOneMonth() {
        givenBudgets(new Budget(YearMonth.of(2019, 10), 3100));
        assertEquals(100, plan.query(new Period(LocalDate.of(2019, 10, 3), LocalDate.of(2019, 10, 3))));
    }

    @Test
    public void queryTwoDayWithinOneMonth() {
        givenBudgets(new Budget(YearMonth.of(2019, 10), 3100));
        assertEquals(200, plan.query(new Period(LocalDate.of(2019, 10, 3), LocalDate.of(2019, 10, 4))));
    }

    @Test
    public void queryBeforeBudget() {
        givenBudgets(new Budget(YearMonth.of(2019, 10), 3100));
        assertEquals(400, plan.query(new Period(LocalDate.of(2019, 9, 25), LocalDate.of(2019, 10, 4))));
    }

        @Test
    public void queryAfterBudget() {
        givenBudgets(new Budget(YearMonth.of(2019, 10), 3100));
        assertEquals(400, plan.query(new Period(LocalDate.of(2019, 10, 28), LocalDate.of(2019, 11, 4))));
    }

    @Test
    public void queryOutOfBudget() {
        givenBudgets(new Budget(YearMonth.of(2019, 10), 3100));
        assertEquals(0, plan.query(new Period(LocalDate.of(2019, 9, 1), LocalDate.of(2019, 9, 24))));
    }

    @Test
    public void queryMultiBudget() {
        givenBudgets(new Budget(YearMonth.of(2019, 10), 3100),
                new Budget(YearMonth.of(2019, 11), 3000));
        assertEquals(2000, plan.query(new Period(LocalDate.of(2019, 10, 20), LocalDate.of(2019, 11, 8))));
    }

    private void givenBudgets(Budget... budgets) {
        when(repo.findAll()).thenReturn(Arrays.asList(budgets));
    }
}

常用重构快捷键

熟悉常用的重构快捷键,可以让我们更快速、更准确的完成重构。下面整理了一些常用的重构快捷键:

作用默认快捷键快捷键英文
展示当前可重构的动作Ctrl+Shift+Alt+TRefactor This
重命名Shift+F6Rename
改变函数声明Ctrl+F6Change Signature
移动方法到其他类F6Move
提取变量Ctrl+Alt+VIntroduce Variable
内联方法或变量Ctr+Alt+NInline
提取方法Ctrl+Alt+MExtract Method