重构初体验——影片出租店例子
任何一个傻瓜都能写出计算机可以理解的代码。唯有写出人类容易理解的代码,才是优秀的程序员。
好代码是不断重构出来的,让我们先来看一个案例。
需求:
这是一个影片出租店用的程序,计算每一位顾客的消费金额并打印详单。操作者告诉程序:顾客租了哪些影片、租期多长,程序便根据租赁时间和影片类型计算出费用。影片分为三类:普通片、儿童片和新片。除了计算费用,还要为常客计算积分,具体的租赁用户积分规则为:
租赁规则
* 价格计算规则:
* 普通片儿 —— 起步价2¥,超过2天的部分每天每部电影收费1.5元
* 新片儿 —— 每天每部3元
* 儿童片 —— 起步价2¥,超过3天的部分每天每部电影收费1.5元
*
* 积分计算规则:
* 每借一部电影积分加1,新片每部加2
原始程序有三个类,分别为Moive,Rental,Customer类。
/**
* @Author: dyf
* @Date: 2019/9/19 16:15
* @Description: 电影类
*/
public class Movie {
public static final int CHILDRENS = 2;
public static final int REGULAR = 0;
public static final int NEW_REALEASE = 1;
private String title;
private int priceCode;
public Movie(String title, int priceCode) {
this.title = title;
this.priceCode = priceCode;
}
public String getTitle() {
return title;
}
public int getPriceCode() {
return priceCode;
}
public void setPriceCode(int priceCode) {
this.priceCode = priceCode;
}
}
**
* @Author: dyf
* @Date: 2019/9/19 16:17
* @Description: 租赁记录
*/
public class Rental {
private Movie movie;
private int daysRented;
public Rental(Movie movie, int daysRented) {
this.movie = movie;
this.daysRented = daysRented;
}
public Movie getMovie() {
return movie;
}
public int getDaysRented() {
return daysRented;
}
}
public class Customer {
private String name;
private Vector rentals = new Vector();
public Customer(String name) {
this.name = name;
}
public void addRental(Rental arg){
rentals.addElement(arg);
}
public String getName() {
return name;
}
public String statement(){
double totalAmount = 0;
int frequentRenterPoints = 0;
Enumeration rentalss = rentals.elements();
String result = "Rental Record for" + getName() + "\n";
while(rentalss.hasMoreElements()){
double thisAmount = 0;
Rental each = (Rental) rentalss.nextElement();
//determine amounts for each line
switch (each.getMovie().getPriceCode()){
case Movie.REGULAR:
thisAmount += 2;
if(each.getDaysRented() > 2)
thisAmount += (each.getDaysRented() - 2) * 1.5;
break;
case Movie.NEW_REALEASE:
thisAmount += each.getDaysRented() * 3;
break;
case Movie.CHILDRENS:
thisAmount += 1.5;
if(each.getDaysRented() > 3)
thisAmount += (each.getDaysRented() - 3) * 1.5;
break;
}
//每部电影添加积分
frequentRenterPoints ++;
//新片每部电影加2分
if((each.getMovie().getPriceCode() == Movie.NEW_REALEASE) && each.getDaysRented() > 1)
frequentRenterPoints++;
//打印每一条租借记录
result += "\t" + each.getMovie().getTitle() + "\t" + String.valueOf(thisAmount) + "\n";
totalAmount += thisAmount;
}
//add footer lines
result += "Amount owed is " + String.valueOf(totalAmount) + "\n";
result += "You earned " + String.valueOf(frequentRenterPoints) + " frequent enter points";
return result;
}
}
看到这个起始程序,就想到了自己日常写的面条代码,只为完成功能,不符合面向对象精神。假如用户希望新增一个详单打印statment2(),后面又更改了计费标准,后面又希望改变影片分类规则,后面又......那么这样的代码,是无法满足快速高效的修改的。
下面是修改后的类图:
代码及测试代码:
public class Movie {
public static final int CHILDRENS = 2;
public static final int REGULAR = 0;
public static final int NEW_REALEASE = 1;
private String title;
private int priceCode;
private Price price;
public Movie(String title, int arg) {
this.title = title;
setPriceCode(arg);
}
public String getTitle() {
return title;
}
public int getPriceCode() {
return priceCode;
}
public void setPriceCode(int arg) {
switch (arg){
case REGULAR:
price = new RegularPrice();
break;
case CHILDRENS:
price = new ChildrensPrice();
break;
case NEW_REALEASE:
price = new NewReleasePrice();
break;
default:
throw new IllegalArgumentException("Incorrect Price Code");
}
}
public double getCharge(int daysRented){
return price.getCharge(daysRented);
}
public int getFrequentRenterPoints(int daysRented) {
return price.getFrequentRenterPoints(daysRented);
}
}
public class Rental {
private Movie movie;
private int daysRented;
public Rental(Movie movie, int daysRented) {
this.movie = movie;
this.daysRented = daysRented;
}
public Movie getMovie() {
return movie;
}
public int getDaysRented() {
return daysRented;
}
public double getCharge(){
return movie.getCharge(daysRented);
}
public int getFrequentRenterPoints(){
return movie.getFrequentRenterPoints(daysRented);
};
}
public class Customer {
private String name;
private Vector rentals = new Vector();
public Customer(String name) {
this.name = name;
}
public void addRental(Rental arg){
rentals.addElement(arg);
}
public String getName() {
return name;
}
public String statement(){
Enumeration rentalss = rentals.elements();
String result = "Rental Record for " + getName() + "\n";
while(rentalss.hasMoreElements()){
Rental each = (Rental) rentalss.nextElement();
//我喜欢尽量去除这一类临时变量。虽然计算了两次,但是很容易被优化
result += "\t" + each.getMovie().getTitle() + "\t" + String.valueOf(each.getCharge()) + "\n";
}
//add footer lines
result += "Amount owed is " + String.valueOf(getTotalCharge()) + "\n";
result += "You earned " + String.valueOf(getTotalFrequentRenterPoints()) + " frequent enter points";
return result;
}
// private double amountFor(Rental rental){
// double thisAmount = 0;
// //最好不要在另一个对象的属性基础上运用switch语句。如果不得不使用,也应该在对象自己的数据上使用,而不是别人的数据上使用
// //这暗示getCharge()应该移动到Movie类里面去
// switch (rental.getMovie().getPriceCode()){
// case Movie.REGULAR:
// thisAmount += 2;
// if(rental.getDaysRented() > 2)
// thisAmount += (rental.getDaysRented() - 2) * 1.5;
// break;
// case Movie.NEW_REALEASE:
// thisAmount += rental.getDaysRented() * 3;
// break;
// case Movie.CHILDRENS:
// thisAmount += 1.5;
// if(rental.getDaysRented() > 3)
// thisAmount += (rental.getDaysRented() - 3) * 1.5;
// break;
// }
// return thisAmount;
// }
public double getTotalCharge(){
double result = 0;
Enumeration rentalss = rentals.elements();
while(rentalss.hasMoreElements()){
Rental each = (Rental) rentalss.nextElement();
result += each.getCharge();
}
return result;
}
public int getTotalFrequentRenterPoints(){
int result = 0;
Enumeration rentalss = rentals.elements();
while(rentalss.hasMoreElements()){
Rental each = (Rental) rentalss.nextElement();
result += each.getFrequentRenterPoints();
}
return result;
}
}
/**
* @Author: dyf
* @Date: 2019/9/23 11:12
* @Description:
* 我们有数种影片类型,它们以不同的方式回答相同的问题。
*/
abstract class Price {
abstract int getPriceCode();
abstract double getCharge(int daysRented);
int getFrequentRenterPoints(int daysRented) {
return 1;
}
}
class NewReleasePrice extends Price {
@Override
int getPriceCode() {
return Movie.NEW_REALEASE;
}
double getCharge(int daysRented){
return daysRented * 3;
}
int getFrequentRenterPoints(int daysRented) {
return (daysRented > 1) ? 2 : 1;
}
}
class RegularPrice extends Price {
@Override
int getPriceCode() {
return Movie.REGULAR;
}
double getCharge(int daysRented){
double result = 2;
if(daysRented > 2)
result += (daysRented - 2) * 1.5;
return result;
}
}
class ChildrensPrice extends Price {
@Override
int getPriceCode() {
return Movie.CHILDRENS;
}
double getCharge(int daysRented){
double result = 1.5;
if(daysRented > 3)
result += (daysRented - 3) * 1.5;
return result;
}
}
测试代码:
/**
* @Author: dyf
* @Date: 2019/9/23 16:01
* @Description:
* 需求描述:
*
* 有三种类型的电影,顾客可以进行租赁
*
* 租赁规则
* 价格计算规则:
* 普通片儿 —— 起步价2¥,超过2天的部分每天每部电影收费1.5元
* 新片儿 —— 每天每部3元
* 儿童片 —— 起步价1.5¥,超过3天的部分每天每部电影收费1.5元
*
* 积分计算规则:
* 每借一部电影积分加1,新片每部加2
*/
public class CustomerTest {
@Test
public void test_allMovie_2days_rent(){
Customer c2 = new Customer("2天顾客");
setRentDays(c2, 2);
System.out.println(c2.statement());
assertEquals(9.5, c2.getTotalCharge(), 0.00);//delta是个精度值
assertEquals(4, c2.getTotalFrequentRenterPoints());
}
@Test
public void test_allMovie_3days_rent(){
Customer c3 = new Customer("3天顾客");
setRentDays(c3, 3);
System.out.println(c3.statement());
assertEquals(14, c3.getTotalCharge(), 0.00);//delta是个精度值
assertEquals(4, c3.getTotalFrequentRenterPoints());
}
@Test
public void test_allMovie_4days_rent(){
Customer c4 = new Customer("4天顾客");
setRentDays(c4, 4);
System.out.println(c4.statement());
assertEquals(20, c4.getTotalCharge(), 0.00);//delta是个精度值
assertEquals(4, c4.getTotalFrequentRenterPoints());
}
@Test
public void test_2new1regularMovie_5days_rent(){
Customer c = new Customer("5天2部新片1部普通影片顾客");
c.addRental(new Rental(new Movie("普通片儿",0), 5));
c.addRental(new Rental(new Movie("新上映片",1), 5));
c.addRental(new Rental(new Movie("新上映片",1), 5));
System.out.println(c.statement());
assertEquals(36.5, c.getTotalCharge(), 0.00);//delta是个精度值
assertEquals(5, c.getTotalFrequentRenterPoints());
}
private void setRentDays(Customer c, int days){
c.addRental(new Rental(new Movie("普通片儿",0), days));
c.addRental(new Rental(new Movie("新上映片",1), days));
c.addRental(new Rental(new Movie("儿童片儿",2), days));
}
}
总结:
通过这个例子感受重构的魅力,此例用到了数个重构方法:Extract Method(提取方法),Move Method(移动方法),Replace Conditional with polymorphism(有条件的用多态取代),Self Encapsulate Field(安全压缩变量),Replace Type Code with State/Strategy(策略模式),重构方法实在太多,具体可看《重构》一书中讲解,我认为还是实践才能记得住。
1、复习重构的定义:
重构是在不改变软件可观察行为的前提下改善其内部结构。对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。
2、何时重构?
事不过三,三则重构
-
重复性工作,既有的代码无法帮助你轻松添加新特性时
-
修补bug时,排查逻辑困难
-
code review 可以让他人来复审代码检查是否具备可读性,可理解性
-
太多的代码无注释,已然连自己都无法快速理清代码逻辑
3、重构的衡量指标
-
数量: 代码的行数
-
质量: 代码复杂度,耦合度,可读性,架构依赖复杂度等
-
成本: 花费的时间
-
回报(成果): 支持后续功能的快速叠加,解决现有因代码设计问题无法优化的矛盾等
4、一些需重构代码的坏味道
1)Duplicated Code(重复代码)
2) Long Method(过长函数)
遵循一条原则: 每当感觉需要注释来说明什么的时候,可以尝试将需要说明的东西写进一个函数中
3)Large Class(过大的类)
这种情况容易出现冗余代码。比如如果类内出现了多个变量带有相同的前缀或者后缀,这意味着你可以考虑把他们提炼到某个组件内,或者考虑这个组件是否可以成为子类,使用提炼类的手法来重构。
4) Lazy Class(冗赘类)
当子类没有做足够的工作的时候,或者说在可见的预期内,不会有新的情况出现,考虑将类内联化。
上一篇: 二叉搜索树和平衡二叉搜索树应用
推荐阅读