了解了重构技巧和代码的坏味道,可能还是觉得缺少实际重构的经验。在这里分享一个觉得很不错的重构案例,通过这个案例,可以学习到如何在代码中将重构实际应用起来的。
案例讲解
重构终归是需要我们实际上手操作的,所以最快的学习方法就是亲眼目睹整个重构的过程,然后再加以模仿和学习,所以这里请参考原视频讲解:[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+T | Refactor This |
重命名 | Shift+F6 | Rename |
改变函数声明 | Ctrl+F6 | Change Signature |
移动方法到其他类 | F6 | Move |
提取变量 | Ctrl+Alt+V | Introduce Variable |
内联方法或变量 | Ctr+Alt+N | Inline |
提取方法 | Ctrl+Alt+M | Extract Method |