前几天在某个微信群里,有小伙伴发了一个力扣上的编程题,很有意思。如下:
题目很简单,群里的小伙伴开始了讨论。有些历害的小伙伴给出了公式:
image
还有的写出了公式的推导过程:
image
还有一些手快的已经写出了代码:
image
代码很简洁,也实现了功能。
大家的讨论都是围绕如何实现展开的,题目本身也很简单。但是好像没有人关注这个题目中描述的场景,这是很多程序员的通病:只关注技术实现而忽略了业务本身。比如上面的代码如果不是看题目都不知道代码在表达什么。如果我再添加一个需求:每周五要出去玩,不存钱。这段代码可能就多了一个 if-else。如果再加一个需求:由于周 5 没存钱,为了快速达到目标,周三存两倍。那可能就又多出了一个 if-else。随着业务的不断迭代,代码就慢慢的变成了意大利面似的代码(祖传代码),传说中的屎山。
野路子写法
如果我来做这个题的话,拢共分三步。
image
- 整明白每周每天能存多少钱
- 整明白一共有多少周
- 利用上面的完成的工作,用总天数算出有多少周,再把每周每天的钱加起来就完成了
整明白每周每天能存多少钱
在写代码之前我一般会写一些草稿代码,然后不断调整。这样可以站在代码读者的角度或者调用者的角度来审视代码的最终效果。过程我就不说了,最后的结果是得到了一组测试
@Test
public void save_money_every_day_on_frist_week() {
Week week = Week.of(1);
assertThat(week.day(1).money()).isEqualTo(1);
assertThat(week.day(2).money()).isEqualTo(2);
assertThat(week.day(3).money()).isEqualTo(3);
assertThat(week.day(4).money()).isEqualTo(4);
assertThat(week.day(5).money()).isEqualTo(5);
assertThat(week.day(6).money()).isEqualTo(6);
assertThat(week.day(7).money()).isEqualTo(7);
}
@Test
public void save_money_every_day_on_second_week() {
Week week = Week.of(2);
assertThat(week.day(1).money()).isEqualTo(2);
assertThat(week.day(2).money()).isEqualTo(3);
assertThat(week.day(3).money()).isEqualTo(4);
assertThat(week.day(4).money()).isEqualTo(5);
assertThat(week.day(5).money()).isEqualTo(6);
assertThat(week.day(6).money()).isEqualTo(7);
assertThat(week.day(7).money()).isEqualTo(8);
}
@Test
public void save_money_every_day_on_third_week() {
Week week = Week.of(3);
assertThat(week.day(1).money()).isEqualTo(3);
assertThat(week.day(2).money()).isEqualTo(4);
assertThat(week.day(3).money()).isEqualTo(5);
assertThat(week.day(4).money()).isEqualTo(6);
assertThat(week.day(5).money()).isEqualTo(7);
assertThat(week.day(6).money()).isEqualTo(8);
assertThat(week.day(7).money()).isEqualTo(9);
}
分别是第1周,第2周,第3周每天能存多少钱。代码中对周和天和钱这个概念进行了建模。现在实现代码还没有写,接下来就是实现了。
public class Week {
private int whichWeek;
public Week(int whichWeek) {
this.whichWeek = whichWeek;
}
public static Week of(int whickWeek) {
return new Week(whickWeek);
}
public DayOfWeek day(int day) {
return DayOfWeek.of(this, day);
}
public int whichWeek() {
return whichWeek;
}
}
public class DayOfWeek {
private final Week week;
private final int whichDay;
public DayOfWeek(Week week, int whichDay) {
this.week = week;
this.whichDay = whichDay;
}
public static DayOfWeek of(Week week, int day) {
return new DayOfWeek(week, day);
}
public int money() {
return week.whichWeek() - 1 + whichDay;
}
}
整明白一共有多少周
先用一个测试来表达我想要的最终效果
@Test
public void should_create_week_from_given_days() {
assertThat(Week.weeks(4)).isEqualTo(new Week[] {Week.of(1)});
assertThat(Week.weeks(7)).isEqualTo(new Week[] {Week.of(1)});
assertThat(Week.weeks(10)).isEqualTo(new Week[] {Week.of(1), Week.of(2)});
assertThat(Week.weeks(20)).isEqualTo(new Week[] {Week.of(1), Week.of(2), Week.of(3)});
}
实现
public class Week {
public static Week[] weeks(int days) {
int numberOfWeeks = days / 8 + 1;
Week[] weeks = new Week[numberOfWeeks];
for (int i = 0; i < numberOfWeeks; i++) {
weeks[i] = of(i + 1);
}
return weeks;
}
}
省略了equals方法
最后一步
还是先写测试
@Test
public void should_calucate_saved_money_given_days() {
LeetCodeBank bank = new LeetCodeBank();
assertThat(bank.howMuchMoneySaved(4)).isEqualTo(10);
assertThat(bank.howMuchMoneySaved(10)).isEqualTo(37);
assertThat(bank.howMuchMoneySaved(20)).isEqualTo(96);
}
实现
public class LeetCodeBank {
public static final int LAST_DAY_OF_WEEK = 8;
public LeetCodeBank() {
}
public int howMuchMoneySaved(int days) {
int totalMoney = 0;
Week[] weeks = Week.weeks(days);
int currentDayOfWeek = 1;
int currentWeek = 0;
for (int i = 0; i < days; i++) {
totalMoney += weeks[currentWeek].day(currentDayOfWeek).money();
currentDayOfWeek++;
if (currentDayOfWeek == LAST_DAY_OF_WEEK) {
currentDayOfWeek = 1;
currentWeek++;
}
}
return totalMoney;
}
}
现在就实现完了。当然,还有一些边界值处理这里就不说了。有人可能会说,这个代码的空间复杂度和时间复杂度太高了吧!3行代码能写完的居然整出这么多类。代码的整个生命周期中大部分时间是在被阅读和修改,所以代码表达力强,易于阅读和修改,可能比所谓的“性能”更重要。想想工作中大部分时间是不是都在处理需求变化。上面的代码其实就是OOP而已,可能很多程序员还不能理解这种写代码的方式,毕竟大家用Transaction script模式比较多,这就引出了为什么很多程序员学习DDD,根本找不到门,因为面向对象都还没整明白,domain design就design了个寂寞。
网友评论