打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
从重复到重用(一)——可维护代码

第一版:It works

每位熟练的程序员都能快速地给出自己的实现。本文示例代码使用ANSI C99编写,Mac或Linux下用gcc均能正常编译运行,Windows环境未测试。选择C语言是因为主流编程语言都或多或少借鉴它的语法,同时它的语法特性也足够用于演示。

问题很简单,简单到把所有代码都塞到『main』函数里也不觉得长:

#include <stdio.h>int main(void) { struct { char name[8]; int age; int salary; } e[4]; FILE *istream, *ostream; int i; istream = fopen('work.txt', 'r'); for (i = 0; i < 4; i++) { fscanf(istream, '%s%d%d', e[i].name, &e[i].age, &e[i].salary); printf('%s %d %d\n', e[i].name, e[i].age, e[i].salary); if (e[i].salary < 30000) { e[i].salary += 3000; } } fclose(istream); ostream = fopen('work.txt', 'w'); for (i = 0; i < 4; i++) { printf('%s %d %d\n', e[i].name, e[i].age, e[i].salary); fprintf(ostream, '%s %d %d\n', e[i].name, e[i].age, e[i].salary); } fclose(ostream); return 0;}

其中:

  • 需求1:第一个循环从『work.txt』中读取4行数据,并把信息输出到屏幕;

  • 需求2:同时为薪资小于三万的职员增加三千元;

  • 需求3:第二个循环遍历所有数据,把调整后的结果输出屏幕;

  • 需求4:并保存结果到 『work.txt』。

试试将上述代码保存成『1.c』并执行『./check.sh 1.c』,屏幕上会输出『PASS』,即通过测试。

第二版:清晰的代码,重构的基础

第一版代码解决了问题,让原来重复的调薪工作变成简便的、可反复使用的程序。如果它是C语言课堂作业的答案,看起来还不错——至少缩进一致,也没混用空格和制表符;但从软件工程的角度来讲,它简直糟糕透了,因为没有清晰的表达意图:

  1. 魔法常量『4』重复出现,后续负责维护的程序员无法判断它们是碰巧相等还是有其他原因必须相等。

  2. 文件名『work.txt』重复出现。

  3. 重复且不清晰的文件指针类型定义,容易忽略『ostream』前面的『*』。

  4. 『e』和『i』变量命名不能顾名思义。

  5. 变量的定义与使用离得太远。

  6. 无异常处理,文件可能不可读。

借乔老爷子的话说:“看不见的地方也要用心做好”——这些代码的问题用户虽然看不见也不在乎,但也要用心做好——已有几处显眼的地方出现重复。不过,在代码变得清晰之前,不应急着动手去重构,因为清晰的代码更容易找出重复!针对上述意图不明的问题,准备对代码做以下调整:

  1. 确认数字『4』在三处的意义都是员工记录数,因此定义共享常量『#define RECORD_COUNT 4』。

  2. 常量『'work.txt'』和『4』不同,内容虽然相同但意义不同:一个作输入,一个作输出。如果也只简单地定义一个常量『FILE_NAME』共用,后续两者独立变化时,工作量并没减少。所以去除重复代码时,切忌只看表面相同,背后意义相同的才是真正的相同,否则就像给所有常量『1』定义『ONE』别名一样没有意义。所以需要定义三个常量『FILE_NAME』、『INPUT_FILE_NAME』和『OUTPUT_FILE_NAME』。

  3. 用自定义的文件类型『typedef FILE* File;』替代『FILE*』,可避免遗漏指针。

  4. 变量『e』是所有职员信息,把变量名改成『employees』。

  5. 变量『i』是迭代过程的下标,把变量名改成『index』。

  6. 将『index』变量定义放到『for』语句中。

  7. 将『File』变量定义从顶部挪到各自使用之前的位置。

  8. 对文件指针做异常检查,当文件无法打开时输出错误信息并提前终止程序。

  9. 程序退出时用『<stdlib.h>』中更语义化的『EXIT_FAILURE』,正常退出时用『EXIT_SUCCESS』。

你可能会问:“数字30000和3000也是魔法数字,为什么不调整?”原因是此时它们即不重复也无歧义。整理后的完整代码如下:

#include <stdlib.h>#include <stdio.h>#define RECORD_COUNT 4#define FILE_NAME 'work.txt'#define INPUT_FILE_NAME FILE_NAME#define OUTPUT_FILE_NAME FILE_NAMEtypedef FILE* File;int main(void) {  struct {    char name[8];    int age;    int salary;  } employees[RECORD_COUNT];  File istream = fopen(INPUT_FILE_NAME, 'r');  if (istream == NULL) {    fprintf(stderr, 'Cannot open %s with r mode.\n', INPUT_FILE_NAME);    exit(EXIT_FAILURE);  }  for (int index = 0; index < RECORD_COUNT; index++) {    fscanf(istream, '%s%d%d', employees[index].name, &employees[index].age, &employees[index].salary);    printf('%s %d %d\n', employees[index].name, employees[index].age, employees[index].salary);    if (employees[index].salary < 30000) {      employees[index].salary += 3000;    }  }  fclose(istream);  File ostream = fopen(OUTPUT_FILE_NAME, 'w');  if (ostream == NULL) {    fprintf(stderr, 'Cannot open %s with w mode.\n', OUTPUT_FILE_NAME);    exit(EXIT_FAILURE);  }  for (int index = 0; index < RECORD_COUNT; index++) {    printf('%s %d %d\n', employees[index].name, employees[index].age, employees[index].salary);    fprintf(ostream, '%s %d %d\n', employees[index].name, employees[index].age, employees[index].salary);  }  fclose(ostream);  return EXIT_SUCCESS;}

将以上代码保存成『2.c』并执行『./check.sh 2.c』,得到期望的输出『PASS』,证明本次重构没有改变程序的行为。

第三版:代码映射需求

经过第二版的优化,单行代码的意图已比较清晰,但还存在一些过早优化导致代码块的含义不清晰。

例如第一个循环中耦合了“输出到屏幕”和“调整薪资”两个功能,好处是可减少一次循环,性能也许有些提升;但这两个功能在需求中是相互独立的,后续独立变化的可能性更大。假设新需求是第一步输出到屏幕后,要求用户输入命令,再决定是否要进行薪资调整工作。此时,对需求方而言仅仅增加一个步骤,只有一个改动;但到了代码层面,却不是新增一个步骤对应新增一块代码,还会牵涉理论上不相关的代码块;负责维护的程序员在不了解背景时,就不确定这两段代码放在一起有没有历史原因,也就不敢轻易将它们拆开。当系统规模越大,这种与需求不是一一对应的代码就越让维护人员手足无措!

回想日常开发,需求改动很小而代码却牵一发而动全身,根源往往就是过早优化。“优化”和“通用”往往是对立的,优化得越彻底就与业务场景结合越紧密,通用性也越差。比如某个系统会在缓冲队列中对收到的消息进行排序,上线运行后发现因为产品设计等外部原因,消息可能天然接近排好序,于是用插入排序代替快速排序等更通用的排序算法,这就是一次不通用的优化:它让系统的性能更好,但系统的适用面更窄。过早的优化就是过早的给系统能力设置天花板。

理想情况是代码块与需求功能点一一对应,例如当前需求有4个功能点,得有4个独立的代码块与之对应。这样做的好处是:当需求发生变化时,代码的修改也相对集中。因此,基于第二版本代码准备做以下调整:

  • 拆分耦合的循环代码块,每段代码块都只完成一件事情。

  • 用注释明确标出每段代码块对应的需求。

整理后的完整代码如下:

#include <stdlib.h>#include <stdio.h>#define RECORD_COUNT 4#define FILE_NAME 'work.txt'#define INPUT_FILE_NAME FILE_NAME#define OUTPUT_FILE_NAME FILE_NAMEtypedef FILE* File;int main(void) { struct { char name[8]; int age; int salary; } employees[RECORD_COUNT]; /* 从文件读入 */ File istream = fopen(INPUT_FILE_NAME, 'r'); if (istream == NULL) { fprintf(stderr, 'Cannot open %s with r mode.\n', INPUT_FILE_NAME); exit(EXIT_FAILURE); } for (int index = 0; index < RECORD_COUNT; index++) { fscanf(istream, '%s%d%d', employees[index].name, &employees[index].age, &employees[index].salary); } fclose(istream); /* 1. 输出到屏幕 */ for (int index = 0; index < RECORD_COUNT; index++) { printf('%s %d %d\n', employees[index].name, employees[index].age, employees[index].salary); } /* 2. 调整薪资 */ for (int index = 0; index < RECORD_COUNT; index++) { if (employees[index].salary < 30000) { employees[index].salary += 3000; } } /* 3. 输出调整后的结果 */ for (int index = 0; index < RECORD_COUNT; index++) { printf('%s %d %d\n', employees[index].name, employees[index].age, employees[index].salary); } /* 4. 保存到文件 */ File ostream = fopen(OUTPUT_FILE_NAME, 'w'); if (ostream == NULL) { fprintf(stderr, 'Cannot open %s with w mode.\n', OUTPUT_FILE_NAME); exit(EXIT_FAILURE); } for (int index = 0; index < RECORD_COUNT; index++) { fprintf(ostream, '%s %d %d\n', employees[index].name, employees[index].age, employees[index].salary); } fclose(ostream); return EXIT_SUCCESS;}

将以上代码保存成『3.c』并执行『./check.sh 3.c』,确保程序的行为没有改变。

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
Python中的三个“黑魔法”与“骚操作”谁知道?
MySql基础语法、查询笔记
jr - 精品文章 - tomcat解压war?的一点例外
C/C++拾遗(十):文件流输入与输出
php文件上传: $_FILES 数组的内容
文件下载代码
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服