目录
- 一、前言
- 二、通过例子了解重构
-
- 2.1 拆解statement()函数
-
- 2.1.1 提炼计算观众积分的逻辑
- 2.1.2 移除观众积分总和
- 2.1.3 移除总收费
- 2.1.4 小结
- 2.2 拆分计算阶段和格式阶段
-
- 2.2.1 提炼渲染函数
- 2.3 将代码分为多个文件
- 2.4 过程按类型重组计算
-
- 2.4.1 多态取代条件表达式
- 2.4.2 构建戏剧基类
- 2.4.3 构建喜剧子类
- 2.4.4 构建悲剧子类
- 2.5 使用多态计算器提供数据
- 三、总结
一、前言
上篇笔记 【阅读笔记】从零开始了解重构(1) 本文主要介绍了什么是重建和重建的准备工作,以及一些拆分函数的方法,如提炼函数、查询取代临时变量和内联变量。本文继续以戏剧表演小组计算费用的例子了解重建。
二、通过例子了解重构
: 【阅读笔记】从零开始了解重构(1) 。
在上一篇笔记中,我们通过以下方法将其拆分 statement() 函数,让我们继续这个过程,提高代码的可读性和可扩展性。
- 提炼函数;
- 查询取代临时变量;
- 内联变量技术。
2.1 拆解statement()函数
2.1.1 提炼计算观众积分的逻辑
现在 statement() 函数的内部实现如下:
str_t* statement(tk_object_t* invoice, tk_object_t* plays) {
double_t total_amount = 0; uint32_t volume_credits = 0; str_t* result = TKMEM_ZALLOC(str_t); const char* customer = tk_object_get_prop_str(invoice, "[0].customer"); tk_object_t* performances = tk_object_get_prop_object(invoice, "[0].performances"); uint32_t perf_num = tk_object_get_prop_uint32(performances, TK_OBJECT_PROP_SIZE, 0); str_init(result, 0); str_append_format(result, STATEMENT_SIZE, "Statement for %s\n", customer); for (uint32_t i = 0; i < perf_num; i++) {
value_t v;
uint32_t audience = 0;
tk_object_t* perf = NULL;
const char* play_name = NULL;
const char* play_type = NULL;
object_array_get(performances, i, &v);
perf = value_object(&v);
audience = tk_object_get_prop_uint32(perf, "audience", 0);
play_name = tk_object_get_prop_str(play_for_perf(perf, plays), "name");
play_type = tk_object_get_prop_str(play_for_perf(perf, plays), "type");
/* 增加观众量积分 */
volume_credits += tk_max(audience - 30, 0);
/* 每增加10名戏剧观众可以获得额外积分 */
if (tk_str_eq(play_type, "comedy")) {
volume_credits += floor(audience / 5);
}
total_amount += amount_for_perf(perf, plays);
/* 格式化输出每个剧目的收费 */
str_append_format(result, STATEMENT_SIZE, " %s: $%0.2f (%d seats)\n", play_name,
amount_for_perf(perf, plays) / 100, audience);
}
/* 格式化输出总收费和获得的积分 */
str_append_format(result, STATEMENT_SIZE, "Amount owed is $%.2f\n", total_amount / 100);
str_append_format(result, STATEMENT_SIZE, "You earned %d credits\n", volume_credits);
return result;
这里我们可以看到移除了 play 变量的好处,移除了一个局部作用域的变量,提炼观众量积分的计算逻辑又更简单一些。
这里我们仍然需要处理其他两个局部变量。 perf
和 volume_credits
同样可以作为参数传入,将它们的获取逻辑提炼到新函数中并修改新函数中的变量名,代码如下:
static tk_object_t* getPerf(tk_object_t* performance_list, uint32_t index) {
char str_i[4] = {
0};
return tk_object_get_prop_object(performance_list, tk_itoa(str_i, sizeof(str_i), index));
}
static uint32_t volume_credits_perf(tk_object_t* a_performance, tk_object_t* plays) {
uint32_t result = 0;
result += tk_max(tk_object_get_prop_uint32(a_performance, "audience", 0) - 30, 0);
if (tk_str_eq(tk_object_get_prop_str(play_for_perf(a_performance, plays), "type"), "comedy")) {
result += floor(tk_object_get_prop_uint32(a_performance, "audience", 0) / 5);
}
return result;
}
str_t* statement(tk_object_t* invoice, tk_object_t* plays) {
double_t total_amount = 0;
uint32_t volume_credits = 0;
str_t* result = TKMEM_ZALLOC(str_t);
const char* customer = tk_object_get_prop_str(invoice, "[0].customer");
tk_object_t* performances = tk_object_get_prop_object(invoice, "[0].performances");
uint32_t perf_num = tk_object_get_prop_uint32(performances, TK_OBJECT_PROP_SIZE, 0);
str_init(result, 0);
str_append_format(result, STATEMENT_SIZE, "Statement for %s\n", customer);
for (uint32_t i = 0; i < perf_num; i++) {
uint32_t audience = tk_object_get_prop_uint32(getPerf(performances, i), "audience", 0);
const char* play_name = tk_object_get_prop_str(play_for_perf(getPerf(performances, i), plays), "name");
volume_credits += volume_credits_perf(getPerf(performances, i), plays);
total_amount += amount_for_perf(getPerf(performances, i), plays);
/* 格式化输出每个剧目的收费 */
str_append_format(result, STATEMENT_SIZE, " %s: $%0.2f (%d seats)\n", play_name,
amount_for_perf(getPerf(performances, i), plays) / 100, audience);
}
/* 格式化输出总收费和获得的积分 */
str_append_format(result, STATEMENT_SIZE, "Amount owed is $%.2f\n", total_amount / 100);
str_append_format(result, STATEMENT_SIZE, "You earned %d credits\n", volume_credits);
return result;
}
这里只展示一步到位的结果,在实际操作时,每编辑一个步骤都需要执行编译、测试、提交。
《重构》的作者指出,临时变量往往会带来麻烦,它们只在对其进行处理的代码块中有用,因此临时变量实质上是鼓励我们写又长又复杂的函数,代码往往变得晦涩难懂。因此,重构代码的过程中非常重视替换这些临时变量,有时甚至存在“将函数赋值给临时变量”的场景,比如书中 JS 代码的 format 变量,这里由于我是用 C 语言写的,所以没有这种情况。作者在重构时将他的 format 变量替换为一个明确声明的函数,并使用了优化该函数,最终结果如下:
function usd(aNumber) {
return new Intl.NumberFormat("en-US", {
style: "currency", currency: "USD", minimumFractionDigits: 2}).format(aNumber/100);
}
备注:在写代码的时候,好的命名非常重要。只有恰如其分地命名,才能彰显出将大函数分解成小函数的价值。有了好的名称,就不必通过阅读函数体来了解其行为。但要一次把名取好并不容易,这是一个不断优化的过程。通常我们需要花时间通读更多代码,才能发现最好的名称是什么。
2.1.2 移除观众量积分总和
接着我们来重构 volume_credits,处理这个变量更加微妙,因为它是在循环的迭代过程中累加得到的。第一步,就是应用拆分循环手法将 volume_credits 的累加过程分离出来,代码如下:
str_t* statement(tk_object_t* invoice, tk_object_t* plays) {
double_t total_amount = 0;
uint32_t volume_credits = 0;
str_t* result = TKMEM_ZALLOC(str_t);
const char* customer = tk_object_get_prop_str(invoice, "[0].customer");
tk_object_t* performances = tk_object_get_prop_object(invoice, "[0].performances");
uint32_t perf_num = tk_object_get_prop_uint32(performances, TK_OBJECT_PROP_SIZE, 0);
str_init(result, 0);
str_append_format(result, STATEMENT_SIZE, "Statement for %s\n", customer);
for (uint32_t i = 0; i < perf_num; i++) {
uint32_t audience = tk_object_get_prop_uint32(getPerf(performances, i), "audience", 0);
const char* play_name =tk_object_get_prop_str(play_for_perf(getPerf(performances, i), plays), "name");
total_amount += amount_for_perf(getPerf(performances, i), plays);
/* 格式化输出每个剧目的收费 */
str_append_format(result, STATEMENT_SIZE, " %s: $%0.2f (%d seats)\n", play_name,
amount_for_perf(getPerf(performances, i), plays) / 100, audience);
}
for (uint32_t i = 0; i < perf_num; i++) {
uint32_t audience = tk_object_get_prop_uint32(getPerf(performances, i), "audience", 0);
volume_credits += volume_credits_perf(getPerf(performances, i), plays);
}
/* 格式化输出总收费和获得的积分 */
str_append_format(result, STATEMENT_SIZE, "Amount owed is $%.2f\n", total_amount / 100);
str_append_format(result, STATEMENT_SIZE, "You earned %d credits\n", volume_credits);
return result;
}
完成这一步,我就可以使用移动语句手法将变量声明挪动到紧邻循环的位置,代码如下:
str_t* statement(tk_object_t* invoice, tk_object_t* plays) {
...
uint32_t volume_credits = 0;
for (uint32_t i = 0; i < perf_num; i++) {
uint32_t audience = tk_object_get_prop_uint32(getPerf(performances, i), "audience", 0);
volume_credits += volume_credits_perf(getPerf(performances, i), plays);
}
...
return result;
}
把与更新 volume_credits 变量相关的代码都集中到一起,有利于以查询取代临时变量手法的施展。第一步同样是先对变量的计算过程应用提炼函数手法。
static uint32_t total_volume_credits(tk_object_t* performance_list, tk_object_t* plays) {
uint32_t result = 0;
for (uint32_t i = 0; i < tk_object_get_prop_uint32(performance_list, TK_OBJECT_PROP_SIZE, 0); i++) {
result += volume_credits_perf(getPerf(performance_list, i), plays);
}
return result;
}
str_t* statement(tk_object_t* invoice, tk_object_t* plays) {
double_t total_amount = 0;
str_t* result = TKMEM_ZALLOC(str_t);
const char* customer = tk_object_get_prop_str(invoice, "[0].customer");
tk_object_t* performances = tk_object_get_prop_object(invoice, "[0].performances");
uint32_t perf_num = tk_object_get_prop_uint32(performances, TK_OBJECT_PROP_SIZE, 0);
str_init(result, 0);
str_append_format(result, STATEMENT_SIZE, "Statement for %s\n", customer);
for (uint32_t i = 0; i < perf_num; i++) {
uint32_t audience = tk_object_get_prop_uint32(getPerf(performances, i), "audience", 0);
const char* play_name = tk_object_get_prop_str(play_for_perf(getPerf(performances, i), plays), "name");
total_amount += amount_for_perf(getPerf(performances, i), plays);
/* 格式化输出每个剧目的收费 */
str_append_format(result, STATEMENT_SIZE, " %s: $%0.2f (%d seats)\n", play_name,
amount_for_perf(getPerf(performances, i), plays) / 100, audience);
}
uint32_t volume_credits = total_volume_credits(performances, plays);
/* 格式化输出总收费和获得的积分 */
str_append_format(result, STATEMENT_SIZE, "Amount owed is $%.2f\n", total_amount / 100);
str_append_format(result, STATEMENT_SIZE, "You earned %d credits\n", volume_credits);
return result;
}
完成函数提炼后,再应用内联变量手法内联 total_volume_credits 函数。
str_t* statement(tk_object_t* invoice, tk_object_t* plays) {
double_t total_amount = 0;
str_t* result = TKMEM_ZALLOC(str_t);
const char* customer = tk_object_get_prop_str(invoice, "[0].customer");
tk_object_t* performances = tk_object_get_prop_object(invoice, "[0].performances");
uint32_t perf_num = tk_object_get_prop_uint32(performances, TK_OBJECT_PROP_SIZE, 0);
str_init(result, 0);
str_append_format(result, STATEMENT_SIZE, "Statement for %s\n", customer);
for (uint32_t i = 0; i < perf_num; i++) {
uint32_t audience = tk_object_get_prop_uint32(getPerf(performances, i), "audience", 0);
const char* play_name = tk_object_get_prop_str(play_for_perf(getPerf(performances, i), plays), "name");
total_amount += amount_for_perf(getPerf(performances, i), plays);
/* 格式化输出每个剧目的收费 */
str_append_format(result, STATEMENT_SIZE, " %s: $%0.2f (%d seats)\n", play_name,
amount_for_perf(getPerf(performances, i), plays) / 100, audience);
}
/* 格式化输出总收费和获得的积分 */
str_append_format(result, STATEMENT_SIZE, "Amount owed is $%.2f\n", total_amount / 100);
str_append_format(result, STATEMENT_SIZE, "You earned %d credits\n", total_volume_credits(performances, plays));
return result;
}
2.1.3 移除总收费
我们再以同样的手法移除 total_amount 变量,代码如下:
static double_t total_amount(tk_object_t* performance_list, tk_object_t* plays) {
double_t result = 0;
for (uint32_t i = 0; i < tk_object_get_prop_uint32(performance_list, TK_OBJECT_PROP_SIZE, 0); i++) {
result += amount_for_perf(getPerf(performance_list, i), plays);
}
return result;
}
str_t* statement(tk_object_t* invoice, tk_object_t* plays) {
str_t* result = TKMEM_ZALLOC(str_t);
tk_object_t* performances = tk_object_get_prop_object(invoice, "[0].performances");
str_init(result, 0);
str_append_format(result, STATEMENT_SIZE, "Statement for %s\n", tk_object_get_prop_str(invoice, "[0].customer"));
for (uint32_t i = 0; i < tk_object_get_prop_uint32(performances, TK_OBJECT_PROP_SIZE, 0); i++) {
/* 格式化输出每个剧目的收费 */
str_append_format(result, STATEMENT_SIZE, " %s: $%0.2f (%d seats)\n",
tk_object_get_prop_str(play_for_perf(getPerf(performances, i), plays), "name"),
amount_for_perf(getPerf(performances, i), plays) / 100,