初始设计与实现
1.需求:
(1)大体是设计一个影片出租店的程序,计算每一位顾客的消费金额并打印详单。
(2)首先操作者会告诉程序,顾客租了哪些影片,租期多长,程序便根据租赁时间和影片类型算出费用。
(3)还有要知道影片分为三类:普通片,儿童片和新片。
(4)最后除了计算费用,还要为常客计算积分,积分会根据租片种类是否为新片而有不同
2.结构图:
3.代码实现:
Movie类,影片:
@Data public class Movie { /** * 儿童片 */ public static final int CHILDRENS = 2; /** * 普通片 */ public static final int REGULAR = 0; /** * 新片 */ public static final int NEW_RELEASE = 1; private String title; private int priceCode; public Movie(String title, int priceCode) { this.title = title; this.priceCode = priceCode; } }
Rental类,租赁:
@Data public class Rental { private Movie movie; private int dayRented; public Rental(Movie movie, int daysRented) { this.movie = movie; this.dayRented = daysRented; } }
Customer类,顾客类:
@Data public class Customer { private String name; private Vector rentals = new Vector(); public Customer(String name) { this.name = name; } public void addRental(Rental arg) { rentals.add(arg); } public String statement() { double totalAmount = 0; int frequentRenterPoints = 0; Enumeration rentalElement = rentals.elements(); String result = "Rental Record for " + getName() + " "; while (rentalElement.hasMoreElements()) { double thisAmount = 0; Rental each = (Rental) rentalElement.nextElement(); //计算总额 switch (each.getMovie().getPriceCode()) { case Movie.REGULAR: thisAmount += 2; if (each.getDayRented() > 2) { thisAmount += (each.getDayRented() - 2) * 1.5; } break; case Movie.NEW_RELEASE: thisAmount += each.getDayRented(); break; case Movie.CHILDRENS: thisAmount += 1.5; if (each.getDayRented() > 3) { thisAmount += (each.getDayRented() - 3) * 1.5; } break; default: break; } //增加积分 frequentRenterPoints++; //add bonus for a two day new release rental if ((each.getMovie().getPriceCode() == Movie.NEW_RELEASE) && each.getDayRented() > 1) { frequentRenterPoints++; } //展示租赁详情 result += " " + each.getMovie().getTitle() + " "; totalAmount += thisAmount; } result += "Amount owed is " + String.valueOf(totalAmount) + " "; result += "You earned " + String.valueOf(frequentRenterPoints) + " frequent renter points"; return result; } }
分析并重构
对上述代码进行分析
1.分析:
这段代码statement()方法很长,做了很多其他类应该完成的事,违背了单一职责原则,开放封闭原则等,灵活性和扩展性都比较差,也不方便复用,但是能满足目前的需求。
2.重构的必要性:
可能你心里想着:“不管怎么说,它运行得很好,只要没坏,就不要动它”。但实际上虽然它没坏,但是它造成了伤害,它让你的生活比较难过,因为当客户有其他新的需求时(如客户想改变影片分类规则,但还没决定怎么改,只是决定了几套方案,一旦决定就要迅速改完),就很难完成客户所需要的修改,所以重构是很有必要的。
小笔记:如果你发现需要为程序添加一个特性,而代码结构使你无法很方便地达成目的,那就先重构那个程序,是特性的添加比较容易进行,然后再添加特性。
对上述代码进行重构
1.重构第一步:
重构的第一步永远相同,为即将修改的代码建立一组可靠的测试环境。
小笔记:重构之前,首先检查自己是否有一套可靠的测试机制,这些测试必须有自我检验能力
2.首先分解重组statement()的switch判断逻辑:
(1)先将switch判断当一个方法提出来amountFor(Rental each),计算总额时,直接传参,调用刚提出的计算总额方法即可得到thisAmount,这次改动后最好先做一次测试,避免后续改动过多增加测试难度。
thisAmount = amountFor(each);
小笔记:重构技术就是以微小的步伐修改程序,如果你发现错误,很容易就能发现它。
(2)改变amountFor()里的变量名称。
将 each 改为 aRental
将 thisAmount 改为 result
问:改名值得么?
答:绝对值得,好的代码有良好的表达,和好的清晰度,改完之后记得先测一下。
小笔记:任何一个傻瓜都能写出计算机理解的代码,唯有写出人类可以理解的代码,才是优秀的程序员。
(3)观察amountFor(Rental aRental)方法时,发现传参时Rental类型参数,和Customer类无关,所以要调整位置,将这个方法放到Rental,方法名叫:getCharge(Rental aRental),然后将Customer类改成如下,并重新测试编译。
Rental类:
public double getCharge() { double result = 0; //算出总额 switch (getMovie().getPriceCode()) { case Movie.REGULAR: result += 2; if (getDayRented() > 2) { result += (getDayRented() - 2) * 1.5; } break; case Movie.NEW_RELEASE: result += getDayRented(); break; case Movie.CHILDRENS: result += 1.5; if (getDayRented() > 3) { result += (getDayRented() - 3) * 1.5; } break; default: break; } return result; }
Customer类:
private double amountFor(Rental aRental) {
return aRental.getCharge();
}
疑问:为什么这里不直接调用getCharge()方法,而是先调用amountFor(),再通过amountFor()调用getCharge()呢?
答:这就是下一步要做的事情,但不能直接就先调用新的方法。
(4)先迁移成为新方法,测试没问题后,再删除旧方法
将调用的amountFor()替换为thisAmount = each.getCharge();
(5)替换成 each.getCharge() 后发现,thisAmount 也没了用处,因为它除了赋值没其他作用,而且值在后面也不会有改变,于是将 thisAmount 替换成each.getCharge(),修改后及时测试。
小习惯:可以尽量取消一些临时变量,像上面这种临时变量,被传来传去容易跟丢也没有必要(这里调用了两次计算总额的方法,后续说明怎么优化)
3.然后重构statement()的常客积分的计算
(1)观察发现常客积分的计算也是只与Rental有关,所以讲计算的方法提到Rental类中。
public int getFrequentRenterPoints() { if ((getMovie().getPriceCode() == Movie.NEW_RELEASE) && getDayRented() > 1) { return 2; } else { return 1; } }
将Customer类中提炼为:
//计算积分
frequentRenterPoints += each.getFrequentRenterPoints();
(2)同样因为要算总的积分,所以也可以像计算总额一样单独提出来:
private int getTotalFrequentRenterPoints() { int result = 0; Enumeration rentalElement = rentals.elements(); while (rentalElement.hasMoreElements()) { Rental each = (Rental)rentalElement.nextElement(); result += each.getFrequentRenterPoints(); } return result; }
3.马上要修改影片分类规则,但具体怎么做还未决定,需要再进行重构
(1)思路:现在对程序进行修改,肯定是愚蠢的,应该进入积分计算和常客积分计算中,把因条件而异的代码替换掉,这样才能为将来的改变镀上一层保护膜。
(2)先改变switch语句,将getChange()方法移动到Movie里,原因是本系统可能发生的变化是加入新影片的影响,这种变化带有不稳定倾向,所以为尽量控制它的影响,就在Movie里计算费用
疑问(未解决):为什么在Movie里计算费用就可以控制影响?
于是先将getChange()移动到Moive类中:
public double getCharge(int daysRented) { double result = 0; //算出总额 switch (getPriceCode()) { case Movie.REGULAR: result += 2; if (daysRented > 2) { result += (daysRented - 2) * 1.5; } break; case Movie.NEW_RELEASE: result += daysRented; break; case Movie.CHILDRENS: result += 1.5; if (daysRented > 3) { result += (daysRented - 3) * 1.5; } break; default: break; } return result; }
再改变Rental类里相应代码:
public double getCharge() { return movie.getCharge(dayRented); }
(3)以相同手法处理常客积分计算
Movie类:
public int getFrequentRenterPoints(int daysRented) { if ((getPriceCode() == Movie.NEW_RELEASE) && daysRented > 1) { return 2; } else { return 1; } }
Rental类:
public int getFrequentRenterPoints() { return movie.getFrequentRenterPoints(dayRented); }
(4)使用状态模式来设计Movie类
这里先了解下实现思路,之后看完重构后再细看第一章...
随记:
1.代码块越小,代码功能就越容易管理,代码的处理和移动就余越轻松。
2.还不太能get到为什么要将Rental类的逻辑迁移到Movie里,虽然按照后面的结果,通过状态模式来拆开Movie里getCharge()的逻辑,在知道了后续实现的前提下我觉得将getCharge()的逻辑迁移到Movie里是没问题的,但要我根据文中所说因为可能新做影片类别,就直接要迁移这个方法到Movie里,我是不能get到这个点的,直接用三种影片算价方式继承Rental就可以吧,这样就只用改变一个类,就算后续有新加影片,或者重新定义怎么分片,Movie类也只是配置参数就行,不用大改。
3.还有另一点我也没有想清楚,和第2点也是相关的,就是为什么不能用继承的方式,文中说:“一部影片可以在生命周期内修改自己的分类,一个对象却不能在自己的生命周期修改所属类”,这句话也没有理解。
4.希望第2第3个问题,在看了后面的内容能得到解答。