重构是这样的一个过程:在不改变代码的外在行为的前提下,对代码做出修改,以改进程序的内部结构。
重构的第一步:
每当我要进行重构的时候,为了让改变后的代码满足原先代码效用,往往我们需要尽可能多的建立一组可靠的的测试环境。这些测试是必要的,因为我们在重构过程中引入很多bug。我们是人,并不是神,所以我们需要测试来保证我们重构是改善代码,而不是增加error。
ps:任何一个傻瓜都能写出的计算机可以理解的代码。唯有写出人类容易理解的代码,才是优秀的程序员。
当然一个程序员成长,也离开不了身边的良师益友。
下面是我入职培训的一个小题目。
题目:
一家公司,有些月有预算,有些月没有预算(预算为零)。现在公司告诉你那年那月有预算,给定一个时间段,叫你算出这个时间的预算。乍一看,这题目小学生难度,大家都会做,但是你写出来的代码很优秀吗,比其他人有优势吗?
要求:有测试,代码尽善尽美
下面给出一个代码(现场码的)
BudgetDaoImpl.java
package com.oocl.course;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
public class BudgetDaoImpl implements BudgetDao {
@Override
public List<Budget> getAllBudges() {
List<Budget> budgets=new ArrayList<Budget>();
Budget budget = new Budget(LocalDate.of(2012,2,5),100);
budgets.add(budget);
return budgets;
}
}
BudgetDao.java
package com.oocl.course;
import java.util.List;
public interface BudgetDao {
public List<Budget> getAllBudges();
}
Budget.java
package com.oocl.course;
import java.time.LocalDate;
public class Budget {
private LocalDate date;
double amount;
public Budget(LocalDate date,double amount){
this.amount=amount;
this.date=date;
}
public void setDate(LocalDate date) {
this.date = date;
}
public void setAmount(double amount) {
this.amount = amount;
}
private double getDailyAmount() {
return amount / date.lengthOfMonth();
}
private LocalDate getEnd() {
return date.withDayOfMonth(date.lengthOfMonth());
}
private LocalDate getStart() {
return date.withDayOfMonth(1);
}
private Duration getDuration() {
return new Duration(getStart(), getEnd());
}
public double getOverlappingAmount(Duration duration) {
return getDailyAmount() * duration.getOverlappingDays(getDuration());
}
}
Duration.java
package com.oocl.course;
import java.time.LocalDate;
import java.time.Period;
public class Duration {
private final LocalDate start;
private final LocalDate end;
public Duration(LocalDate start, LocalDate end) {
this.start = start;
this.end = end;
}
private int getDays() {
if (start.isAfter(end))
return 0;
return Period.between(start, end).getDays() + 1;
}
public int getOverlappingDays(Duration another) {
LocalDate overlappingEnd = end.isBefore(another.end) ? end : another.end;
LocalDate overlappingStart = start.isAfter(another.start) ? start : another.start;
return new Duration(overlappingStart, overlappingEnd).getDays();
}
}
BudgetService.java
package com.oocl.course;
import java.time.LocalDate;
public class BudgetService {
private BudgetDao budgetDao;
public BudgetService(BudgetDao budgetDao) {
this.budgetDao = budgetDao;
}
public double queryBudget(LocalDate start, LocalDate end) {
return budgetDao.getAllBudges().stream()
.mapToDouble(budget -> budget.getOverlappingAmount(new Duration(start, end)))
.sum();
}
}
BudgetTest.java
package com.oocl.course;
import org.junit.Test;
import java.util.Arrays;
import java.util.List;
import static java.time.LocalDate.of;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class BudgetTest {
StubBudgetDao budgetDao = new StubBudgetDao();
BudgetService budgetService = new BudgetService(budgetDao);
@Test
public void no_budget() {
givenBudgets();
double actual = budgetService.queryBudget(
of(2018, 6, 4),
of(2018, 6, 4));
assertEquals(0, actual, 0.01);
}
@Test
public void start_is_after_end() {
givenBudgets(budget(2018, 6, 300));
double actual = budgetService.queryBudget(
of(2018, 6, 4),
of(2018, 5, 4));
assertEquals(0, actual, 0.01);
}
@Test
public void sameDay() {
givenBudgets(budget(2018, 6, 300));
double actual = budgetService.queryBudget(
of(2018, 6, 4),
of(2018, 6, 4));
assertEquals(10, actual, 0.01);
}
@Test
public void sameMonth() {
givenBudgets(budget(2018, 6, 300));
double actual = budgetService.queryBudget(
of(2018, 6, 2),
of(2018, 6, 7));
assertEquals(60, actual, 0.01);
}
@Test
public void start_is_before_month_first_day() {
givenBudgets(budget(2018, 6, 300));
double actual = budgetService.queryBudget(
of(2018, 5, 20),
of(2018, 6, 7));
assertEquals(70, actual);
}
@Test
public void end_is_after_month_last_day() {
givenBudgets(budget(2018, 6, 300));
double actual = budgetService.queryBudget(
of(2018, 6, 20),
of(2018, 7, 7));
assertEquals(110, actual, 0.01);
}
@Test
public void end_is_before_month_first_day() {
givenBudgets(budget(2018, 6, 300));
double actual = budgetService.queryBudget(
of(2018, 5, 20),
of(2018, 5, 23));
assertEquals(0, actual, 0.01);
}
@Test
public void start_is_after_month_last_day() {
givenBudgets(budget(2018, 6, 300));
double actual = budgetService.queryBudget(
of(2018, 7, 20),
of(2018, 7, 23));
assertEquals(0, actual, 0.01);
}
@Test
public void start_and_end_cover_the_whole_month() {
givenBudgets(budget(2018, 6, 300));
double actual = budgetService.queryBudget(
of(2018, 5, 20),
of(2018, 7, 23));
assertEquals(300, actual, 0.01);
}
@Test
public void adjacentMonth() {
givenBudgets(
budget(2018, 6, 300),
budget(2018, 7, 310));
double actual = budgetService.queryBudget(
of(2018, 6, 15),
of(2018, 7, 10));
assertEquals(260, actual, 0.01);
}
@Test
public void DiscreteMonth() {
givenBudgets(
budget(2018, 6, 300),
budget(2018, 8, 310));
double actual = budgetService.queryBudget(
of(2018, 6, 15),
of(2018, 8, 12));
assertEquals(280, actual, 0.01);
}
@Test
public void differentYear() {
givenBudgets(budget(2018, 6, 300));
double actual = budgetService.queryBudget(of(2015, 6, 7), of(2016, 6, 4));
assertEquals(0, actual, 0.01);
}
private void givenBudgets(final Budget... budgets) {
budgetDao.setBudgets(Arrays.asList(budgets));
}
private Budget budget(int year, int month, int amount) {
return new Budget(of(year, month, 4), amount);
}
private class StubBudgetDao implements BudgetDao {
private List<Budget> budgets;
public void setBudgets(List<Budget> budgets) {
this.budgets = budgets;
}
@Override
public List<Budget> getAllBudges() {
return budgets;
}
}
}
以上代码是重构之后的代码。初始的代码我就不贴了,code smell 太多了。
接下来,我归一下以上代码使用的重构方法。
1、提炼类(Extract Class)
2、提炼接口(Extract Interface)
3、提炼函数(Extract Method)
4、搬移字段(Move Field)
5、搬移函数(Move Method)
6、函数更名(Rename Method)
7、以测试取代异常(Replace Exception with Test)
8、以查询取代临时变量(Replace Temp with Query)