《重构 改善既有代码的设计》 读书笔记(二)

1.1 起点

实例

名称:影片出租店程序

功能:计算每位顾客的消费金额并打印详单。

需求:输入信息(哪位顾客租了哪些影片、租期多长),然后让程序计算费用。影片分为普通片、儿童片、新片。除计算费用外,还要为常客计算积分,积分根据是否是新片有所不同。

UML类图如下:

/'在线作图(UML)网址:
http://www.plantuml.com/plantuml/uml/SyfFKj2rKt3CoKnELR1Io4ZDoSa70000
如果要修改的的话,打开网址后,直接复制上图片链接(或者粘贴下方代码)修改即可'/
@startuml
Title "影片出租店程序"
class Movie{
- private int priceCode
}
class Rental{
- private int daysRented
}
class Customer{
+ public statement()
}
Rental --> Movie
Customer --> Rental
@enduml

影片出租店程序

尽可能少篇幅地将程序写在这里

class Movie://影片
    static final CHILDRENS = 2//儿童片
    static final REGULAR = 0//普通片
    static final NEWRELEASE = 1//新片
    String title
    int priceCode
    Movie(title, priceCode){...}//带有全参数的构造器
    (title,priceCode) => getter,setter//分别title和priceCode的getter和setter方法
class Rental://租赁
	Movie movie
	int dayRented
	Rental(movie, dayRented){...}
	(movie,dayRented) => getter,setter
class Customer://顾客
	String name
	rentals = Vector<Rental>()
	Customer(name, rentals){...}
	(name,rentals) => getter,setter
	public String statement() {
		double totalAmount = 0;
		int frequentRenterPoints = 0;
		Iterator<Rental> rentalIter = rentals.iterator();
		String result = "Rental Record for " + getName() + "\n";
		while (rentalIter.hasNext()) {
			double thisAmount = 0;
			Rental each = rentalIter.next();

			// determine amounts for each line(确定每行的金额)
			switch (each.getMovie().getPriceCode()) {
				case Movie.REGULAR :
					thisAmount += 2;
					if (each.getDayRented() > 2)
						thisAmount += (each.getDayRented() - 2) * 1.5;
					break;
				case Movie.NEWRELEASE :
					thisAmount += each.getDayRented() * 3;
					break;
				case Movie.CHILDRENS :
					thisAmount += 1.5;
					if (each.getDayRented() > 3)
						thisAmount += (each.getDayRented() - 3) * 1.5;
					break;
				default :
					break;
			}

			// add frequent renter points(增加常客的积分)
			frequentRenterPoints++;
			// add bonus for a two day new release rental(为两天的新释放租金增加奖金)
			// 积分根据是否是新片有所不同
			if ((each.getMovie().getPriceCode() == Movie.NEWRELEASE)
					&& each.getDayRented() > 1)
				frequentRenterPoints++;

			// show figures for the rental(显示租金的数字)
			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 renter points";
		return result;
	}

分析:在Customer类中,statement()方法过长,内部业务过多,不符合面向对象思想。

假设有如下几点需求。

序号需求措施
1以HTML格式输出详单阅读statement方法发现,无法复用,需要重新写一个专门生成html详单的方法htmlStatement()
此功能而言,仅需复制旧方法更改即可
2计费标准改变需求1+需求2,需要同时更改statement()和htmlStatement()两处中的计费标准
3影片分类规则更改,具体改变方法据情况而定很显然,这个需求仅仅代表着,暂时这样,将来一定会改

在需求1的前提下,当业务越来越复杂时,statement()和htmlStatement()方法需要保持一致,代码会越来越臃肿,也越来越容易出现问题

什么时候需要重构?

当你发现,当新需求出现时,原先的代码结构不能很好的适应,这个时候,就需要先考虑怎么将原结构优化(重构),然后再考虑加入新需求。

1.2 重构的第一步

建立测试环境:测试环境是为了保障在重构过程中,不会使原程序出现大的BUG(无论重构方法再怎么先进,总会有出现BUG的可能性,可靠的测试能够尽可能避免这种情况发生),这是一个安全锁。

搭建测试环境思路:

影片出租店程序中的statement方法,最终输出结果是字符串,其内部不会发生其他的变化,所以,在测试环境中,需要保证重构前的代码与重构后的代码输出字符串相同。所以,在搭建的测试环境中,需要有一个比对字符串的方法(告诉你哪行不一致)。

1.3 分解并重组

对于statement()方法而言,过于臃肿,需要进行分解。

代码块越小,代码功能就越容易管理,处理和移动更轻松。所以我们需要将statement()进行分解。

下面将逐步进行分解。

1.3.1 提炼函数(6.1 Extract Method)

找出代码中的整块部分(逻辑泥团),提炼成一个独立函数。

提炼函数(6.1 Extract Method):详细内容在第六章第一节。此处简单介绍:
把大型函数提出来一部分变成小型函数,需要注意:小型函数的命名(以它‘做什么’来命名)、检查变量作用域(关键)、处理临时变量、编译测试。

在statement方法中,switch语句是一个明显的逻辑泥团,可以尝试提炼出来。

首先找到函数中的局部变量和参数:each和thisAmount,前者仅被使用,后者会被修改。

任何不会被修改的变量可以直接当作参数传入;如果只有一个变量被修改,可以把它当作返回值

thisAmount是个临时变量,switch语句前的初始值始终为0,所以可以直接在提炼函数中把返回值赋给它。

// 要被提炼的代码判断
switch (each.getMovie().getPriceCode()) {
	case Movie.REGULAR :
		thisAmount += 2;
		if (each.getDayRented() > 2)
			thisAmount += (each.getDayRented() - 2) * 1.5;
		break;
	case Movie.NEWRELEASE :
		thisAmount += each.getDayRented() * 3;
        break;
	case Movie.CHILDRENS :
		thisAmount += 1.5;
		if (each.getDayRented() > 3)
			thisAmount += (each.getDayRented() - 3) * 1.5;
		default :
	break;
}
// 以下代码均位于Customer类中
// 提炼后的代码
thisAmount = this.amountFor(each);
...
// 提炼函数名为amountFor
/**
 * 确定出租的金额
 * 
 * @author newre
 * @param rental
 * @return
 */
private double amountFor(Rental rental) {
	// 在重构时,对变量名修改更易于表达提炼函数的功能
	double result = 0;
	switch (rental.getMovie().getPriceCode()) {
		case Movie.REGULAR :
			result += 2;
			if (rental.getDayRented() > 2)
				result += (rental.getDayRented() - 2) * 1.5;
			break;
		case Movie.NEWRELEASE :
			result += rental.getDayRented() * 3;
			break;
		case Movie.CHILDRENS :
			result += 1.5;
			if (rental.getDayRented() > 3)
				result += (rental.getDayRented() - 3) * 1.5;
			break;
		default :
			break;
	}
	return result;
}

需要注意的一点是:重构需要一步一步来,每次修改的幅度不能过大,这样才能保证,当你犯下错误时,很容易便可锁定问题的位置。 )

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

NewReErWen

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值