从重复到重用

沙海 2021年5月26日12:11:31Java评论112字数 31274阅读104分14秒阅读模式
摘要

速读摘要

速读摘要文章源自JAVA秀-https://www.javaxiu.com/25879.html

后文始终围绕一个问题的解决方案,不断发现其中"重复"的代码,并提炼出"可重用"的抽象,持续"重构"。在我看来,编码规范最大的价值是便于发现代码中的重复!else中异常处理是放在if代码块中还是else,这类问题没有标准答案,公说公有理婆说婆有理。发现重复不能只浮于表面相同,得理解其背后的意义,只有后续需要一起变化的重复才是真正的重复。第二个问题是让代码生成代码,给个提示,可以用"宏"。文章源自JAVA秀-https://www.javaxiu.com/25879.html

原文约 1.2 万 | 图片 2 | 建议阅读 24 分钟 | 评价反馈文章源自JAVA秀-https://www.javaxiu.com/25879.html

从重复到重用

redraiment 阿里技术 文章源自JAVA秀-https://www.javaxiu.com/25879.html

从重复到重用文章源自JAVA秀-https://www.javaxiu.com/25879.html

温馨提示:本文较长,同学们可收藏后再看 从重复到重用文章源自JAVA秀-https://www.javaxiu.com/25879.html

一  前言

文章源自JAVA秀-https://www.javaxiu.com/25879.html

开发技术的发展,从第一次提出“函数/子程序”,实现代码级重用;到面向对象的“类”,重用数据结构与算法;再到“动态链接库”、“控件”等重用模块;到如今流行的云计算、微服务可重用整个系统。技术发展虽然日新月异,但本质都是重用,只是粒度不同。所以写代码的动机都应是把重复的工作变成可重用的方案,其中重复的工作包括业务上重复的场景、技术上重复的代码等。合格的系统可以简化当下重复的工作;优秀的系统还能预见未来重复的工作。文章源自JAVA秀-https://www.javaxiu.com/25879.html

文章源自JAVA秀-https://www.javaxiu.com/25879.html

本文不谈框架、不谈架构,就谈写代码的那些事儿!后文始终围绕一个问题的解决方案,不断发现其中“重复”的代码,并提炼出“可重用”的抽象,持续“重构”。希望通过这个过程和大家分享一些发现重复代码和提炼可重用抽象的方法。文章源自JAVA秀-https://www.javaxiu.com/25879.html

文章源自JAVA秀-https://www.javaxiu.com/25879.html

二  问题

文章源自JAVA秀-https://www.javaxiu.com/25879.html

作为贯穿全文的主线,这有一个任务需要开发一个程序来完成:有一份存有职员信息(姓名、年龄、工资)的文件“work.txt”,内容如下:文章源自JAVA秀-https://www.javaxiu.com/25879.html

文章源自JAVA秀-https://www.javaxiu.com/25879.html

William35 25000Kishore41 35000Wallace37 30000Bruce39 29999
文章源自JAVA秀-https://www.javaxiu.com/25879.html

文章源自JAVA秀-https://www.javaxiu.com/25879.html

  1. 要求从文件(work.txt)中读取员工薪酬,并输出到屏幕上。文章源自JAVA秀-https://www.javaxiu.com/25879.html

  2. 为所有工资小于三万的员工涨 3000 元。文章源自JAVA秀-https://www.javaxiu.com/25879.html

  3. 在屏幕上输出薪资调整后的结果。文章源自JAVA秀-https://www.javaxiu.com/25879.html

  4. 把调整后的结果保存到原始文件。文章源自JAVA秀-https://www.javaxiu.com/25879.html

文章源自JAVA秀-https://www.javaxiu.com/25879.html

即运行的结果是屏幕上要有八行输出,“work.txt”的内容将变成:文章源自JAVA秀-https://www.javaxiu.com/25879.html

文章源自JAVA秀-https://www.javaxiu.com/25879.html

William35 28000Kishore41 35000Wallace37 30000Bruce39 32999
文章源自JAVA秀-https://www.javaxiu.com/25879.html

文章源自JAVA秀-https://www.javaxiu.com/25879.html

三  测试文章源自JAVA秀-https://www.javaxiu.com/25879.html

文章源自JAVA秀-https://www.javaxiu.com/25879.html

在明确了需求之后,第一步要做的是写测试代码,而不是写功能代码。《重构》一书中对重构的定义是:“在不改变代码外在行为的前提下,对代码做出修改,以改进程序的内部结构。”其中明确指出“代码外在行为”是不改变的!在不断迭代重构时,“保证每次重构的行为不变”也是一项重复的工作,所以测试先行不仅能尽早地校验对需求理解的正确性、还能避免重复测试。本文通过一段 Shell 脚本完成以下工作:文章源自JAVA秀-https://www.javaxiu.com/25879.html

文章源自JAVA秀-https://www.javaxiu.com/25879.html

  • 初始化work.txt文件。文章源自JAVA秀-https://www.javaxiu.com/25879.html

  • 检查标准输出的内容与期望的结果是否一致。文章源自JAVA秀-https://www.javaxiu.com/25879.html

  • 检查修改后work.txt文件的内容是否与期望一致。文章源自JAVA秀-https://www.javaxiu.com/25879.html

  • 清理现场。文章源自JAVA秀-https://www.javaxiu.com/25879.html

#!/bin/shif [ $# -eq 0 ]; thenecho"usage: $0 <c-source-file>" >&2exit -1fiinput=$(cat <<EOFWilliam 35 25000Kishore 41 35000Wallace 37 30000Bruce 39 29999EOF)output=$(cat <<EOFWilliam 35 28000Kishore 41 35000Wallace 37 30000Bruce 39 32999EOF)echo"$input" > work.txtecho"$input" > .expect.stdout.txtecho"$output" >> .expect.stdout.txtecho"$output" > .expect.work.txt(gcc "$1" -o main && ./main | diff .expect.stdout.txt - && diff .expect.work.txt work.txt) && echo PASS || echo FAILrm -f main work.txt .expect.work.txt .expect.stdout.txt
文章源自JAVA秀-https://www.javaxiu.com/25879.html

文章源自JAVA秀-https://www.javaxiu.com/25879.html

将上述代码保存成check.sh,待测试的源文件名作为参数。如果程序通过,会显示“PASS”,否则会输出不同的行以及“FAIL”。文章源自JAVA秀-https://www.javaxiu.com/25879.html

文章源自JAVA秀-https://www.javaxiu.com/25879.html

四  可维护代码

文章源自JAVA秀-https://www.javaxiu.com/25879.html

第一版:It works

文章源自JAVA秀-https://www.javaxiu.com/25879.html

每位熟练的程序员都能快速地给出自己的实现。本文示例代码使用ANSI C99编写,Mac下用gcc能正常编译运行,其他环境未测试。选择C语言是因为主流编程语言都或多或少借鉴它的语法,同时它的语法特性也足够用于演示。文章源自JAVA秀-https://www.javaxiu.com/25879.html

文章源自JAVA秀-https://www.javaxiu.com/25879.html

问题很简单,简单到把所有代码都塞到 main 函数里也不觉得长:文章源自JAVA秀-https://www.javaxiu.com/25879.html

文章源自JAVA秀-https://www.javaxiu.com/25879.html

#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);return0;}
文章源自JAVA秀-https://www.javaxiu.com/25879.html

文章源自JAVA秀-https://www.javaxiu.com/25879.html

其中第一个循环从work.txt中读取4行数据,并把信息输出到屏幕(需求#1);同时为薪资小于三万的职员增加三千元(需求#2);第二个循环遍历所有数据,把调整后的结果输出屏幕(需求#3),并保存结果到 work.txt(需求#4)。文章源自JAVA秀-https://www.javaxiu.com/25879.html

文章源自JAVA秀-https://www.javaxiu.com/25879.html

试试将上述代码保存成1.c并执行 ./check.sh 1.c,屏幕上会输出“PASS”,即通过测试。文章源自JAVA秀-https://www.javaxiu.com/25879.html

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

文章源自JAVA秀-https://www.javaxiu.com/25879.html

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

文章源自JAVA秀-https://www.javaxiu.com/25879.html

  1. 魔法常量 4 重复出现,后续负责维护的程序员无法判断它们是碰巧相等还是有其他原因必需相等。文章源自JAVA秀-https://www.javaxiu.com/25879.html

  2. 文件名work.txt重复出现。文章源自JAVA秀-https://www.javaxiu.com/25879.html

  3. 重复且不清晰的文件指针类型定义,容易忽略 ostream 前面的 *文章源自JAVA秀-https://www.javaxiu.com/25879.html

  4. ei 变量命名不顾名思义。文章源自JAVA秀-https://www.javaxiu.com/25879.html

  5. 变量的定义与使用离得太远。文章源自JAVA秀-https://www.javaxiu.com/25879.html

  6. 无异常处理,文件可能不可读。文章源自JAVA秀-https://www.javaxiu.com/25879.html

文章源自JAVA秀-https://www.javaxiu.com/25879.html

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

文章源自JAVA秀-https://www.javaxiu.com/25879.html

  1. 确认数字 4 在三处的意义都是员工记录数,因此定义共享常量 #define RECORD_COUNT 4文章源自JAVA秀-https://www.javaxiu.com/25879.html

  2. 常量"work.txt"和 4 不同,内容虽然相同但意义不同:一个作输入,一个作输出。如果也只简单的定义一个常量 FILE_NAME 共用,后续两者独立变化时,工作量并没减少。所以去除重复代码时,切忌只看表面相同,背后意义相同的才是真正的相同,否则就像给所有常量 1 定义 ONE 别名一样没有意义。所以需要定义三个常量 FILE_NAMEINPUT_FILE_NAMEOUTPUT_FILE_NAME文章源自JAVA秀-https://www.javaxiu.com/25879.html

  3. 用自定义的文件类型 typedef FILE* File; 替代 FILE*,可避免遗漏指针。文章源自JAVA秀-https://www.javaxiu.com/25879.html

  4. 变量 e 是所有职员信息,把变量名改成 employees文章源自JAVA秀-https://www.javaxiu.com/25879.html

  5. 变量 i 是迭代过程的下标,把变量名改成 index文章源自JAVA秀-https://www.javaxiu.com/25879.html

  6. index 变量定义放到 for 语句中。文章源自JAVA秀-https://www.javaxiu.com/25879.html

  7. File 变量定义从顶部挪到各自使用之前的位置。文章源自JAVA秀-https://www.javaxiu.com/25879.html

  8. 对文件指针做异常检查,当文件无法打开时输出错误信息并提前终止程序。文章源自JAVA秀-https://www.javaxiu.com/25879.html

  9. 程序退出时用 <stdlib.h> 中更语义化的 EXIT_FAILURE,正常退出时用 EXIT_SUCCESS文章源自JAVA秀-https://www.javaxiu.com/25879.html

文章源自JAVA秀-https://www.javaxiu.com/25879.html

你可能会问:“数字30000和3000也是魔法数字,为什么不调整?”原因是此时它们即不重复也无歧义。整理后的完整代码如下:文章源自JAVA秀-https://www.javaxiu.com/25879.html

文章源自JAVA秀-https://www.javaxiu.com/25879.html

#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 (intindex = 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 (intindex = 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;}
文章源自JAVA秀-https://www.javaxiu.com/25879.html

文章源自JAVA秀-https://www.javaxiu.com/25879.html

将以上代码保存成2.c并执行 ./check.sh 2.c,得到期望的输出PASS,证明本次重构没有改变程序的行为。文章源自JAVA秀-https://www.javaxiu.com/25879.html

文章源自JAVA秀-https://www.javaxiu.com/25879.html

第三版:代码映射需求

文章源自JAVA秀-https://www.javaxiu.com/25879.html

经过第二版的优化,单行代码的意图已比较清晰,但还存在一些过早优化导致代码块的含义不清晰。文章源自JAVA秀-https://www.javaxiu.com/25879.html

文章源自JAVA秀-https://www.javaxiu.com/25879.html

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

文章源自JAVA秀-https://www.javaxiu.com/25879.html

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

文章源自JAVA秀-https://www.javaxiu.com/25879.html

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

文章源自JAVA秀-https://www.javaxiu.com/25879.html

  • 拆分耦合的循环代码块,每段代码块都只完成一件事情。文章源自JAVA秀-https://www.javaxiu.com/25879.html

  • 用注释明确标出每段代码块对应的需求。文章源自JAVA秀-https://www.javaxiu.com/25879.html

文章源自JAVA秀-https://www.javaxiu.com/25879.html

整理后的完整代码如下:文章源自JAVA秀-https://www.javaxiu.com/25879.html

文章源自JAVA秀-https://www.javaxiu.com/25879.html

    #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;intmain(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;}
    文章源自JAVA秀-https://www.javaxiu.com/25879.html

    文章源自JAVA秀-https://www.javaxiu.com/25879.html

    将以上代码保存成3.c并执行 ./check.sh 3.c,确保程序的行为没有改变。文章源自JAVA秀-https://www.javaxiu.com/25879.html

    文章源自JAVA秀-https://www.javaxiu.com/25879.html

    五  面向对象风格

    文章源自JAVA秀-https://www.javaxiu.com/25879.html

    第四版:职员对象抽象

    文章源自JAVA秀-https://www.javaxiu.com/25879.html

    经过两轮改造,代码结构已足够清晰;现在可以开始重构,来梳理代码层次。文章源自JAVA秀-https://www.javaxiu.com/25879.html

    文章源自JAVA秀-https://www.javaxiu.com/25879.html

    最显眼的就是格式化输出职员信息:除了输出流不同,格式、内容完全相同,四条需求中出现了三次。一般遇到相同/相似代码时,可以抽象出一个函数:相同的部分写在函数体中,不同的部分作为参数传入。此处,能抽象出一个以结构体数据和文件流为入参的函数,但目前这个结构体还是匿名的,无法作为函数的参数,所以第一步得先给匿名的职员结构体取一个合适的类型名称:文章源自JAVA秀-https://www.javaxiu.com/25879.html

    文章源自JAVA秀-https://www.javaxiu.com/25879.html

    typedefstruct _Employee {char name[8];int age;int salary;} *Employee;
    文章源自JAVA秀-https://www.javaxiu.com/25879.html

    文章源自JAVA秀-https://www.javaxiu.com/25879.html

    然后抽象公共函数用于格式化输出 EmployeeFile,这其中还耦合了两个功能:文章源自JAVA秀-https://www.javaxiu.com/25879.html

    文章源自JAVA秀-https://www.javaxiu.com/25879.html

    1. Employee 序列化成字符串。文章源自JAVA秀-https://www.javaxiu.com/25879.html

    2. 序列化结果输出到指定文件流。文章源自JAVA秀-https://www.javaxiu.com/25879.html

    文章源自JAVA秀-https://www.javaxiu.com/25879.html

    因为暂无独立使用某项功能的场景,目前无需进一步拆分:文章源自JAVA秀-https://www.javaxiu.com/25879.html

    文章源自JAVA秀-https://www.javaxiu.com/25879.html

    voidemployee_print(Employee employee, File ostream){fprintf(ostream, "%s %d %d\n", employee->name, employee->age, employee->salary);}
    文章源自JAVA秀-https://www.javaxiu.com/25879.html

    文章源自JAVA秀-https://www.javaxiu.com/25879.html

    Employee 结构体 + employee_print 函数很容易联想到面向对象的“类”。面向对象的本质是由一组功能独立的对象组成系统,对象之间通过发消息协作完成任务,不见得非要有 class 关键字,继承、封装、多态等语法糖。文章源自JAVA秀-https://www.javaxiu.com/25879.html

    文章源自JAVA秀-https://www.javaxiu.com/25879.html

    • 对象的“功能独立”,即高内聚,要求数据和操作数据的相关方法放在一起,大多数支持面向对象的编程语言都提供了 class 关键字,在语言层面强制捆绑,C语言并没有这样的语法,但可以制定编码规范,让数据结构与函数在物理上挨得更近。文章源自JAVA秀-https://www.javaxiu.com/25879.html

    • “给对象发消息”,不同的编程语言里表现形式各不相同,例如在Java中 foo.baz() 就是向 foo 对象发送baz消息,C++中等价的语法是 foo->baz(),Smalltalk中是 foo baz,C语言则是 baz(foo)文章源自JAVA秀-https://www.javaxiu.com/25879.html

    文章源自JAVA秀-https://www.javaxiu.com/25879.html

    综上所述,虽然C语言通常被认为不是面向对象的语言,其实它也能支持面向对象风格。沿上述思路,可以抽象出职员对象的四个方法:文章源自JAVA秀-https://www.javaxiu.com/25879.html

    文章源自JAVA秀-https://www.javaxiu.com/25879.html

    • employee_read:构造函数,分配空间、输入并反序列化,类似于Java的 new文章源自JAVA秀-https://www.javaxiu.com/25879.html

    • employee_free:析构函数,释放空间,即纯手工的 GC。文章源自JAVA秀-https://www.javaxiu.com/25879.html

    • employee_print:序列化并输出。文章源自JAVA秀-https://www.javaxiu.com/25879.html

    • employee_adjust_salary:调整职员薪资,唯一的业务逻辑。文章源自JAVA秀-https://www.javaxiu.com/25879.html

    文章源自JAVA秀-https://www.javaxiu.com/25879.html

    有了职员对象,程序不再只有一个 main 函数。假设把 main 函数看作应用层,其他函数看作类库、框架或中间件,这样程序有了层级,层间仅通过开放的接口通讯,即对象的封装性。文章源自JAVA秀-https://www.javaxiu.com/25879.html

    文章源自JAVA秀-https://www.javaxiu.com/25879.html

    在Java中有 publicprotecteddefaultprivate 四种可见性修饰符,C语言的函数默认是公开的,加上 static 关键字后只在当前文件可见。为避免应用层向对象随意发送消息,约定只有在应用层用到的函数才公开,所以额外定义了 publicprivate 两个修饰符,目前职员对象的四个方法都是公开的。文章源自JAVA秀-https://www.javaxiu.com/25879.html

    文章源自JAVA秀-https://www.javaxiu.com/25879.html

    重构之后的完整代码如下:文章源自JAVA秀-https://www.javaxiu.com/25879.html

    文章源自JAVA秀-https://www.javaxiu.com/25879.html

      #include<stdlib.h>#include<stdio.h>#define private static#define public#define RECORD_COUNT 4#define FILE_NAME "work.txt"#define INPUT_FILE_NAME FILE_NAME#define OUTPUT_FILE_NAME FILE_NAMEtypedef FILE *File;/* 职员对象 */typedefstruct _Employee {char name[8];int age;int salary;} *Employee;publicvoidemployee_free(Employee employee){free(employee);}public Employee employee_read(File istream){ Employee employee = (Employee) calloc(1, sizeof(struct _Employee));if (employee == NULL) {fprintf(stderr, "employee_read: out of memory\n");exit(EXIT_FAILURE); }if (fscanf(istream, "%s%d%d", employee->name, &employee->age, &employee->salary) != 3) { employee_free(employee);returnNULL; }return employee;}publicvoidemployee_print(Employee employee, File ostream){fprintf(ostream, "%s %d %d\n", employee->name, employee->age, employee->salary);}publicvoidemployee_adjust_salary(Employee employee){if (employee->salary < 30000) { employee->salary += 3000; }}/* 应用层 */intmain(void){ Employee 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++) { employees[index] = employee_read(istream); } fclose(istream);/* 1. 输出到屏幕 */for (int index = 0; index < RECORD_COUNT; index++) { employee_print(employees[index], stdout); }/* 2. 调整薪资 */for (int index = 0; index < RECORD_COUNT; index++) { employee_adjust_salary(employees[index]); }/* 3. 输出调整后的结果 */for (int index = 0; index < RECORD_COUNT; index++) { employee_print(employees[index], stdout); }/* 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++) { employee_print(employees[index], ostream); } fclose(ostream);/* 释放资源 */for (int index = 0; index < RECORD_COUNT; index++) { employee_free(employees[index]); }return EXIT_SUCCESS;}
      文章源自JAVA秀-https://www.javaxiu.com/25879.html

      文章源自JAVA秀-https://www.javaxiu.com/25879.html

      将代码保存为4.c,照例执行 ./check.sh 4.c,检测是否有改变程序行为。文章源自JAVA秀-https://www.javaxiu.com/25879.html

      文章源自JAVA秀-https://www.javaxiu.com/25879.html

      第五版:容器对象抽象

      文章源自JAVA秀-https://www.javaxiu.com/25879.html

      之前的重构,去除了词法和句法上的重复,就像一篇文章里的单词和语句,接着可以看段落有没有重复,即代码块。文章源自JAVA秀-https://www.javaxiu.com/25879.html

      文章源自JAVA秀-https://www.javaxiu.com/25879.html

      employee_print 类似,三段循环输出职员信息代码也是明显的重复,可以抽象出 employees_print,同时也抽象出另一个对象——职员列表—— Employees。参考职员对象,可以抽象出四个与之对应的函数:文章源自JAVA秀-https://www.javaxiu.com/25879.html

      文章源自JAVA秀-https://www.javaxiu.com/25879.html

      • employees_read:构造函数,分配列表空间,并依次创建职员对象。文章源自JAVA秀-https://www.javaxiu.com/25879.html

      • employees_free:析构函数,释放列表空间,以及职员对象的空间。文章源自JAVA秀-https://www.javaxiu.com/25879.html

      • employees_print:序列化并输出列表中每一位职员信息。文章源自JAVA秀-https://www.javaxiu.com/25879.html

      • employees_adjust_salary:调整所有符合要求职员的薪资。文章源自JAVA秀-https://www.javaxiu.com/25879.html

      文章源自JAVA秀-https://www.javaxiu.com/25879.html

      此时,main 函数只需调用职员列表对象的方法,不再直接调用职员对象的方法,所以后者可见性从 public 降为 private文章源自JAVA秀-https://www.javaxiu.com/25879.html

      文章源自JAVA秀-https://www.javaxiu.com/25879.html

      重构之后的完整代码如下:文章源自JAVA秀-https://www.javaxiu.com/25879.html

      文章源自JAVA秀-https://www.javaxiu.com/25879.html

      #include<stdlib.h>#include<stdio.h>#define private static#define public#define RECORD_COUNT 4#define FILE_NAME "work.txt"#define INPUT_FILE_NAME FILE_NAME#define OUTPUT_FILE_NAME FILE_NAMEtypedef FILE *File;/* 职员对象 */typedefstruct _Employee {char name[8];int age;int salary;} *Employee;privatevoidemployee_free(Employee employee){free(employee);}private Employee employee_read(File istream){ Employee employee = (Employee) calloc(1, sizeof(struct _Employee));if (employee == NULL) {fprintf(stderr, "employee_read: out of memory\n");exit(EXIT_FAILURE); }if (fscanf(istream, "%s%d%d", employee->name, &employee->age, &employee->salary) != 3) { employee_free(employee);returnNULL; }return employee;}privatevoidemployee_print(Employee employee, File ostream){fprintf(ostream, "%s %d %d\n", employee->name, employee->age, employee->salary);}privatevoidemployee_adjust_salary(Employee employee){if (employee->salary < 30000) { employee->salary += 3000; }}/* 职员列表对象 */typedef Employee* Employees;public Employees employees_read(File istream){ Employees employees = (Employees) calloc(RECORD_COUNT, sizeof(Employee));if (employees == NULL) {fprintf(stderr, "employees_read: out of memory\n");exit(EXIT_FAILURE); }for (int index = 0; index < RECORD_COUNT; index++) { employees[index] = employee_read(istream); }return employees;}publicvoidemployees_print(Employees employees, File ostream){for (int index = 0; index < RECORD_COUNT; index++) { employee_print(employees[index], ostream); }}publicvoidemployees_adjust_salary(Employees employees){for (int index = 0; index < RECORD_COUNT; index++) { employee_adjust_salary(employees[index]); }}publicvoidemployees_free(Employees employees){for (int index = 0; index < RECORD_COUNT; index++) { employee_free(employees[index]); }free(employees);}/* 应用层 */intmain(void){/* 从文件读入 */ 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); } Employees employees = employees_read(istream); fclose(istream);/* 1. 输出到屏幕 */ employees_print(employees, stdout);/* 2. 调整薪资 */ employees_adjust_salary(employees);/* 3. 输出调整后的结果 */ employees_print(employees, stdout);/* 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); } employees_print(employees, ostream); fclose(ostream);/* 释放资源 */ employees_free(employees);return EXIT_SUCCESS;}
      文章源自JAVA秀-https://www.javaxiu.com/25879.html

      不要忘记运行 ./check.sh 作回归测试。文章源自JAVA秀-https://www.javaxiu.com/25879.html

      文章源自JAVA秀-https://www.javaxiu.com/25879.html

      第六版:输入输出抽象

      文章源自JAVA秀-https://www.javaxiu.com/25879.html

      此时的 main 函数已经比较清爽,剩下一处明显的重复:打开文件并检查文件是否正常打开。这属于文件相关的操作,可以抽象出一个 file_open 代替 fopen文章源自JAVA秀-https://www.javaxiu.com/25879.html

      文章源自JAVA秀-https://www.javaxiu.com/25879.html

      private File file_open(char* filename, char* mode){ File stream = fopen(filename, mode);if (stream == NULL) {fprintf(stderr, "Cannot open %s with %s mode.\n", filename, mode);exit(EXIT_FAILURE); }return stream;}
      文章源自JAVA秀-https://www.javaxiu.com/25879.html

      文章源自JAVA秀-https://www.javaxiu.com/25879.html

      接着可以继续抽象职员列表对象的输入和输出方法:文章源自JAVA秀-https://www.javaxiu.com/25879.html

      文章源自JAVA秀-https://www.javaxiu.com/25879.html

      • employees_input:从文件中获取数据并创建职员列表对象。文章源自JAVA秀-https://www.javaxiu.com/25879.html

      • employees_output:将职员列表对象的内容输出到文件。文章源自JAVA秀-https://www.javaxiu.com/25879.html

      重构后 employees_read 不再被 main 访问,所以改成 private。重构后的完整代码如下:文章源自JAVA秀-https://www.javaxiu.com/25879.html

      文章源自JAVA秀-https://www.javaxiu.com/25879.html

        #include<stdlib.h>#include<stdio.h>#define private static#define public#define RECORD_COUNT 4#define FILE_NAME "work.txt"#define INPUT_FILE_NAME FILE_NAME#define OUTPUT_FILE_NAME FILE_NAMEtypedef FILE *File;typedefchar* String;/* 职员对象 */typedefstruct _Employee {char name[8];int age;int salary;} *Employee;privatevoidemployee_free(Employee employee){free(employee);}private Employee employee_read(File istream){ Employee employee = (Employee) calloc(1, sizeof(struct _Employee));if (employee == NULL) {fprintf(stderr, "employee_read: out of memory\n");exit(EXIT_FAILURE); }if (fscanf(istream, "%s%d%d", employee->name, &employee->age, &employee->salary) != 3) { employee_free(employee);returnNULL; }return employee;}privatevoidemployee_print(Employee employee, File ostream){fprintf(ostream, "%s %d %d\n", employee->name, employee->age, employee->salary);}privatevoidemployee_adjust_salary(Employee employee){if (employee->salary < 30000) { employee->salary += 3000; }}/* 职员列表对象 */typedef Employee* Employees;private Employees employees_read(File istream){ Employees employees = (Employees) calloc(RECORD_COUNT, sizeof(Employee));if (employees == NULL) {fprintf(stderr, "employees_read: out of memory\n");exit(EXIT_FAILURE); }for (int index = 0; index < RECORD_COUNT; index++) { employees[index] = employee_read(istream); }return employees;}publicvoidemployees_print(Employees employees, File ostream){for (int index = 0; index < RECORD_COUNT; index++) { employee_print(employees[index], ostream); }}publicvoidemployees_adjust_salary(Employees employees){for (int index = 0; index < RECORD_COUNT; index++) { employee_adjust_salary(employees[index]); }}publicvoidemployees_free(Employees employees){for (int index = 0; index < RECORD_COUNT; index++) { employee_free(employees[index]); }free(employees);}/* I/O层 */private File file_open(String filename, String mode){ File stream = fopen(filename, mode);if (stream == NULL) {fprintf(stderr, "Cannot open %s with %s mode.\n", filename, mode);exit(EXIT_FAILURE); }return stream;}public Employees employees_input(String filename){ File istream = file_open(filename, "r"); Employees employees = employees_read(istream); fclose(istream);return employees;}publicvoidemployees_output(Employees employees, String filename){ File ostream = file_open(filename, "w"); employees_print(employees, ostream); fclose(ostream);}/* 应用层 */intmain(void){ Employees employees = employees_input(INPUT_FILE_NAME); /* 从文件读入 */ employees_print(employees, stdout); /* 1. 输出到屏幕 */ employees_adjust_salary(employees); /* 2. 调整薪资 */ employees_print(employees, stdout);/* 3. 输出调整后的结果 */ employees_output(employees, OUTPUT_FILE_NAME);/* 4. 保存到文件 */ employees_free(employees); /* 释放资源 */return EXIT_SUCCESS;}
        文章源自JAVA秀-https://www.javaxiu.com/25879.html

        别忘记执行 ./check.sh文章源自JAVA秀-https://www.javaxiu.com/25879.html

        文章源自JAVA秀-https://www.javaxiu.com/25879.html

        六  函数式编程

        文章源自JAVA秀-https://www.javaxiu.com/25879.html

        第七版:容器迭代重用

        文章源自JAVA秀-https://www.javaxiu.com/25879.html

        现在,main 里只用到了职员列表相关的函数,且代码和需求几乎一一对应。这些函数可以看成职员管理领域的DSL,领域特定语言是业务和技术双方的共识,理论上需求不变,基于DSL开发的业务代码也不变。之前所有的改动仅要求 main 行为一致,后续的重构还要尽量保证 main 自身也无任何变化,即API向后兼容。文章源自JAVA秀-https://www.javaxiu.com/25879.html

        文章源自JAVA秀-https://www.javaxiu.com/25879.html

        回到继续挖掘代码中重复的问题上,其中职员列表方法中几乎都有一个 for 循环:for (int index = 0; index < RECORD_COUNT; index++) { ... },例如调整薪资和释放空间两段代码:文章源自JAVA秀-https://www.javaxiu.com/25879.html

        文章源自JAVA秀-https://www.javaxiu.com/25879.html

        for (intindex = 0; index < RECORD_COUNT; index++) { employee_adjust_salary(employees[index]);}for (intindex = 0; index < RECORD_COUNT; index++) { employee_free(employees[index]);}
        文章源自JAVA秀-https://www.javaxiu.com/25879.html

        文章源自JAVA秀-https://www.javaxiu.com/25879.html

        除了循环体中分别调用了 employee_adjust_salary 和 employee_free,其余都一摸一样,即它们的迭代规则相同,而循环体不同。是否有可能自定义一个 for 语句代替这些重复的迭代?文章源自JAVA秀-https://www.javaxiu.com/25879.html

        文章源自JAVA秀-https://www.javaxiu.com/25879.html

        在大多数编程语言中,iffor 等控制语句是一种特殊的存在,开发者通常无法自定义。这是 iffor 在大多数语言中的样子:文章源自JAVA秀-https://www.javaxiu.com/25879.html

        文章源自JAVA秀-https://www.javaxiu.com/25879.html

        if (condition) { ...}for (init; term; inc) { ...}
        文章源自JAVA秀-https://www.javaxiu.com/25879.html

        文章源自JAVA秀-https://www.javaxiu.com/25879.html

        如果把它们想象成是函数,语法可以改成更熟悉的函数调用形式:文章源自JAVA秀-https://www.javaxiu.com/25879.html

        文章源自JAVA秀-https://www.javaxiu.com/25879.html

        if (condition, { ...});for (init, term, inc, { ...});
        文章源自JAVA秀-https://www.javaxiu.com/25879.html

        和普通函数调用相比,唯一不同的是允许花括号包围的代码片段作为参数。因此,若编程语言允许代码作为函数的参数,那就能自定义新的控制语句!这句话隐含了两个语言特性:文章源自JAVA秀-https://www.javaxiu.com/25879.html

        文章源自JAVA秀-https://www.javaxiu.com/25879.html

        1. 代码是一种数据类型。文章源自JAVA秀-https://www.javaxiu.com/25879.html

        2. 代码类型的数据可作为函数的参数。文章源自JAVA秀-https://www.javaxiu.com/25879.html

        文章源自JAVA秀-https://www.javaxiu.com/25879.html

        所有编程语言都包含一套类型系统,它决定数据的类型,而数据的类型又决定数据的功能。例如,数值类型可以做四则运算;字符串类型的数据可以拼接、查找、替换等;代码如果也是一种数据类型,就可以随时“执行”它。C语言中具备“执行”能力的元素就是“函数”,函数之于代码类型,犹如 intdouble 之于数值类型,都只是C这个特定编程语言对特定类型的特定实现,换成Visual Basic改叫“过程”,换成Java又称作“成员方法”。文章源自JAVA秀-https://www.javaxiu.com/25879.html

        文章源自JAVA秀-https://www.javaxiu.com/25879.html

        至于特性#2,它正是函数式编程的本质!提到函数式风格,脑海中通常会闪过一些耳熟能详的词汇:无副作用、无状态、易于并行编程,甚至是Lisp那扭曲的前缀表达式。追根溯源,函数式编程源自λ演算——函数能作为值传递给其他函数或由其他函数返回——其本质是函数作为类型系统中的“第一等公民”(First-Class),符合以下四项要求:文章源自JAVA秀-https://www.javaxiu.com/25879.html

        文章源自JAVA秀-https://www.javaxiu.com/25879.html

        1. 可以用变量命名。文章源自JAVA秀-https://www.javaxiu.com/25879.html

        2. 可以提供给过程作为参数。文章源自JAVA秀-https://www.javaxiu.com/25879.html

        3. 可以由过程作为结果返回。文章源自JAVA秀-https://www.javaxiu.com/25879.html

        4. 可以包含在数据结构中。文章源自JAVA秀-https://www.javaxiu.com/25879.html

        文章源自JAVA秀-https://www.javaxiu.com/25879.html

        对照之下会惊讶地发现,C语言这门看似与函数式编程最远的上古编程语言,利用函数指针,居然也完全符合上述条件。观察 employee_adjust_salaryemployee_free 两个函数,都只有一个 Employee 类型的参数且没有返回值,翻译成C语言就是 typedef void (*EmployeeFn)(Employee),把它作为函数的参数,就能抽象出:文章源自JAVA秀-https://www.javaxiu.com/25879.html

        文章源自JAVA秀-https://www.javaxiu.com/25879.html

        private void employees_each(Employees employees, EmployeeFn fn) {for (intindex = 0; index < RECORD_COUNT; index++) { fn(employees[index]); }}
        文章源自JAVA秀-https://www.javaxiu.com/25879.html

        文章源自JAVA秀-https://www.javaxiu.com/25879.html

        在函数式语言中,这类将函数作为参数或返回值的函数称为高阶函数,C语言里称为控制语句。用这个自定义的控制语句代替原生的 for 循环,则代码可以简化成:文章源自JAVA秀-https://www.javaxiu.com/25879.html

        文章源自JAVA秀-https://www.javaxiu.com/25879.html

          employees_each(employees, employee_adjust_salary);employees_each(employees, employee_free);
          文章源自JAVA秀-https://www.javaxiu.com/25879.html

          不过,此时还只解决了一半问题:employees_reademployees_print 中依然有重复的 for 循环,并无法用 employees_each 简化。原因是这些循环体中函数调用的参数数目与类型和 EmployeeFn 不兼容:文章源自JAVA秀-https://www.javaxiu.com/25879.html

          文章源自JAVA秀-https://www.javaxiu.com/25879.html

          • employee_read:包含 File 类型的参数,返回 Employee  类型。文章源自JAVA秀-https://www.javaxiu.com/25879.html

          • employee_print:包含 EmployeeFile 两类参数,无返回值。文章源自JAVA秀-https://www.javaxiu.com/25879.html

          • EmployeeFn:包含 Employee 类型的参数,无返回值。文章源自JAVA秀-https://www.javaxiu.com/25879.html

          想涵盖所有场景,最简单的方法就是提取一个参数与返回结果的全集——Employee (*EmployeeFn)(Employee, File)——包含 EmployeeFile 两个类型的参数,且返回 Employee 类型的结果。用新接口重构 Employee 的四个方法:文章源自JAVA秀-https://www.javaxiu.com/25879.html

          文章源自JAVA秀-https://www.javaxiu.com/25879.html

          • 忽略无用的参数。文章源自JAVA秀-https://www.javaxiu.com/25879.html

          • 除了employee_free 返回 NULL,其他都返回 Employee 入参。文章源自JAVA秀-https://www.javaxiu.com/25879.html

          同时,需要改造 employees_each 去适应新接口:加入 File 参数,以及返回处理结果。在编程的语义中,单纯利用副作用的迭代被称为 foreach,而关注迭代每个元素的处理结果则称为 map,即映射。因此,用 employees_map 取代之前的 employees_each文章源自JAVA秀-https://www.javaxiu.com/25879.html

          文章源自JAVA秀-https://www.javaxiu.com/25879.html

          private Employees employees_map(Employees employees, File stream, EmployeeFn fn) {for (intindex = 0; index < RECORD_COUNT; index++) { employees[index] = fn(employees[index], stream); }return employees;}
          文章源自JAVA秀-https://www.javaxiu.com/25879.html

          文章源自JAVA秀-https://www.javaxiu.com/25879.html

          重构后的完整代码如下:文章源自JAVA秀-https://www.javaxiu.com/25879.html

          文章源自JAVA秀-https://www.javaxiu.com/25879.html

            #include<stdlib.h>#include<stdio.h>#define private static#define public#define RECORD_COUNT 4#define FILE_NAME "work.txt"#define INPUT_FILE_NAME FILE_NAME#define OUTPUT_FILE_NAME FILE_NAMEtypedef FILE *File;typedefchar* String;/* 职员对象 */typedefstruct _Employee {char name[8];int age;int salary;} *Employee;typedefEmployee(*EmployeeFn)(Employee, File);private Employee employee_free(Employee employee, File stream){free(employee);returnNULL;}private Employee employee_read(Employee employee, File istream){ employee = (Employee) calloc(1, sizeof(struct _Employee));if (employee == NULL) {fprintf(stderr, "employee_read: out of memory\n");exit(EXIT_FAILURE); }if (fscanf(istream, "%s%d%d", employee->name, &employee->age, &employee->salary) != 3) { employee_free(employee, NULL);returnNULL; }return employee;}private Employee employee_print(Employee employee, File ostream){fprintf(ostream, "%s %d %d\n", employee->name, employee->age, employee->salary);return employee;}private Employee employee_adjust_salary(Employee employee, File stream){if (employee->salary < 30000) { employee->salary += 3000; }return employee;}/* 职员列表对象 */typedef Employee* Employees;private Employees employees_map(Employees employees, File stream, EmployeeFn fn){for (int index = 0; index < RECORD_COUNT; index++) { employees[index] = fn(employees[index], stream); }return employees;}private Employees employees_read(File istream){ Employees employees = (Employees) calloc(RECORD_COUNT, sizeof(Employee));if (employees == NULL) {fprintf(stderr, "employees_read: out of memory\n");exit(EXIT_FAILURE); }return employees_map(employees, istream, employee_read);}publicvoidemployees_print(Employees employees, File ostream){ employees_map(employees, ostream, employee_print);}publicvoidemployees_adjust_salary(Employees employees){ employees_map(employees, NULL, employee_adjust_salary);}publicvoidemployees_free(Employees employees){ employees_map(employees, NULL, employee_free);free(employees);}/* I/O层 */private File file_open(String filename, String mode){ File stream = fopen(filename, mode);if (stream == NULL) {fprintf(stderr, "Cannot open %s with %s mode.\n", filename, mode);exit(EXIT_FAILURE); }return stream;}public Employees employees_input(String filename){ File istream = file_open(filename, "r"); Employees employees = employees_read(istream); fclose(istream);return employees;}publicvoidemployees_output(Employees employees, String filename){ File ostream = file_open(filename, "w"); employees_print(employees, ostream); fclose(ostream);}/* 应用层 */intmain(void){ Employees employees = employees_input(INPUT_FILE_NAME); /* 从文件读入 */ employees_print(employees, stdout); /* 1. 输出到屏幕 */ employees_adjust_salary(employees); /* 2. 调整薪资 */ employees_print(employees, stdout);/* 3. 输出调整后的结果 */ employees_output(employees, OUTPUT_FILE_NAME);/* 4. 保存到文件 */ employees_free(employees); /* 释放资源 */return EXIT_SUCCESS;}
            文章源自JAVA秀-https://www.javaxiu.com/25879.html

            文章源自JAVA秀-https://www.javaxiu.com/25879.html

            文章源自JAVA秀-https://www.javaxiu.com/25879.html

            文章源自JAVA秀-https://www.javaxiu.com/25879.html

            这一系列的改造展示了“代码即数据”的一些好处:使用不支持函数式编程的语言开发,将迫使我们永远在语言恰好提供的基础功能上工作;而“代码即数据”让我们摆脱这样的束缚,允许自定义控制语句。例如,Java 5引入 foreach 语法糖、Java 7引入 try-with-resource 语法糖,在Java 8之前想要任何新的语言特性只能等Oracle大发慈悲,Java 8之后想要任何语言特性就可以自给自足!文章源自JAVA秀-https://www.javaxiu.com/25879.html

            文章源自JAVA秀-https://www.javaxiu.com/25879.html

            经过这么大的改造,切勿忘记测试!文章源自JAVA秀-https://www.javaxiu.com/25879.html

            文章源自JAVA秀-https://www.javaxiu.com/25879.html

            第八版:动态作用域与上下文包装

            文章源自JAVA秀-https://www.javaxiu.com/25879.html

            上一版本的代码虽然可以工作,但也暴露出一个常见问题:函数的参数不断膨胀。这个问题在程序的层次不断增加过程会慢慢滋生。例如函数 A 会调用 BB 又调用 C,假设 C 需要一个文件对象,假设 B 中并不创建文件对象,就得从 A 依次传递到 B 再传递到 C。函数调用的层次越深,数据逐层传递的问题就越严重,上层函数的入参就会爆炸!文章源自JAVA秀-https://www.javaxiu.com/25879.html

            文章源自JAVA秀-https://www.javaxiu.com/25879.html

            这类函数参数过多且逐层传递的问题,最简单的解决方法就是使用全局变量。例如定义一个全局的文件对象,指向当前输入/输出的目标,这样就能去除所有的文件对象入参。全局变量的弊端是很难判断它的影响范围,不加限制地使用全局变量就和无约束地使用goto一样,代码会迅速变成意大利面条。所以,建议有节制地使用全局变量:用完之后及时将值恢复。例如以下代码:文章源自JAVA秀-https://www.javaxiu.com/25879.html

            文章源自JAVA秀-https://www.javaxiu.com/25879.html

              int is_debug = 0;voida(){if (is_debug == 1) {printf("debug is enable\n"); }printf("call a()\n");}voidb(){ a();printf("call b()\n");}voidc(){int original = is_debug; is_debug = 1; b(); is_debug = original;}
              文章源自JAVA秀-https://www.javaxiu.com/25879.html

              文章源自JAVA秀-https://www.javaxiu.com/25879.html

              其中函数 c 临时开启了调试选项,并在退出前恢复成原始值。一旦忘记恢复,后续所有调试信息就都会输出,恶梦就会开始。为避免这种尴尬问题,可以利用上一版本中提到的函数式编程的方法,将重复的开启选项、恢复工作抽象成函数:文章源自JAVA秀-https://www.javaxiu.com/25879.html

              文章源自JAVA秀-https://www.javaxiu.com/25879.html

                typedefvoid(*Callback)(void);voidwith_debug(Callback fn){int original = is_debug; is_debug = 1; fn(); is_debug = original;}voidc(){ with_debug(b);}
                文章源自JAVA秀-https://www.javaxiu.com/25879.html

                像 with_debug 这种负责资源分配再自动回收(或资源修改再自动恢复)工作的函数称为上下文包装器(wrapper),开启调试选项是一个常见的应用场景,还可以用于自动关闭打开的文件对象(例如Java 7的try-with-resources)。不过,目前的解决方案在多线程环境下依然有问题,为避免不同的线程之间相互冲突,理想的方案是采用类似Java中的 ThreadLocal 包装所有全局变量,C语言的多线程方案POSIX thread有Thread Specific组件实现类似的线程特有数据功能,此处就不展开讨论。文章源自JAVA秀-https://www.javaxiu.com/25879.html

                文章源自JAVA秀-https://www.javaxiu.com/25879.html

                综上所述,我们真正需要的功能似乎是一种代码的包装能力:全局变量某个特定的值只在指定范围内生效(包括范围内代码调用的函数、调用函数的调用等等),类似于会话级别的变量。这种功能被裁剪的全局变量在编程语言中称为动态作用域(Dynamic Scope)变量。文章源自JAVA秀-https://www.javaxiu.com/25879.html

                文章源自JAVA秀-https://www.javaxiu.com/25879.html

                大多数主流编程语言只支持静态作用域——也叫词法作用域——在编译时静态确定的作用域;但动态作用域是在运行过程中动态确定的。简言之,静态作用域由代码的层次结构决定,动态作用域由调用的堆栈层次结构决定。以下代码是Perl语言动态作用域变量的示例,保存成demo.pl,执行 perl demo.pl 能输出 $v = 1文章源自JAVA秀-https://www.javaxiu.com/25879.html

                文章源自JAVA秀-https://www.javaxiu.com/25879.html

                subfoo{print"\$v = $v\n";}subbaz{local $v = 1; foo;}baz;
                文章源自JAVA秀-https://www.javaxiu.com/25879.html

                文章源自JAVA秀-https://www.javaxiu.com/25879.html

                回到重构问题,利用动态作用域的思路,可以抽象出一个文件对象包装器:用指定文件替换全局的文件流,退出时恢复。C语言提供了打开指定文件并替代标准输入输出流的函数——freopen——但却没自带恢复的功能,因此不同的平台恢复方法不同,本文以类UNIX环境为例,在unistd.h包下有 dupfdopen 两个函数,分别用于克隆和恢复文件句柄。示例代码如下:文章源自JAVA秀-https://www.javaxiu.com/25879.html

                文章源自JAVA秀-https://www.javaxiu.com/25879.html

                  voidfile_with(String filename, String mode){int handler = dup(mode[0] == 'r'? 0: 1); /* 克隆文件句柄 */ File stream = freopen(filename, mode, mode[0] == 'r'? stdin: stdout);if (stream == NULL) {fprintf(stderr, "Cannot open %s with %s mode.\n", filename, mode);exit(EXIT_FAILURE); }/* TODO */ fclose(stream);  fdopen(handler, mode);                   /* 完成后恢复标准IO */}
                  文章源自JAVA秀-https://www.javaxiu.com/25879.html

                  有了这个功能,可以删除掉所有函数和接口的 File file 参数!唯一真正和文件相关的只剩下 employees_inputemployees_output,它们分别调用 Employees employees_read()void employees_print(Employees),为了使用 file_with 做统一的重定向,利用上一版接口全集的方法,把它们的接口统一改成 typedef Employees (*EmployeesFn)(Employees);。最终,重构后的完整代码如下:文章源自JAVA秀-https://www.javaxiu.com/25879.html

                  文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    #include<stdlib.h>#include<stdio.h>#include<unistd.h>#define private static#define public#define RECORD_COUNT 4#define FILE_NAME "work.txt"#define INPUT_FILE_NAME FILE_NAME#define OUTPUT_FILE_NAME FILE_NAMEtypedef FILE *File;typedefchar* String;/* 职员对象 */typedefstruct _Employee {char name[8];int age;int salary;} *Employee;typedefEmployee(*EmployeeFn)(Employee);private Employee employee_free(Employee employee){free(employee);returnNULL;}private Employee employee_read(Employee employee){ employee = (Employee) calloc(1, sizeof(struct _Employee));if (employee == NULL) {fprintf(stderr, "employee_read: out of memory\n");exit(EXIT_FAILURE); }if (scanf("%s%d%d", employee->name, &employee->age, &employee->salary) != 3) { employee_free(employee);returnNULL; }return employee;}private Employee employee_print(Employee employee){printf("%s %d %d\n", employee->name, employee->age, employee->salary);return employee;}private Employee employee_adjust_salary(Employee employee){if (employee->salary < 30000) { employee->salary += 3000; }return employee;}/* 职员列表对象 */typedef Employee* Employees;typedefEmployees(*EmployeesFn)(Employees);private Employees employees_map(Employees employees, EmployeeFn fn){for (int index = 0; index < RECORD_COUNT; index++) { employees[index] = fn(employees[index]); }return employees;}private Employees employees_read(Employees employees){ employees = (Employees) calloc(RECORD_COUNT, sizeof(Employee));if (employees == NULL) {fprintf(stderr, "employees_read: out of memory\n");exit(EXIT_FAILURE); }return employees_map(employees, employee_read);}public Employees employees_print(Employees employees){return employees_map(employees, employee_print);}publicvoidemployees_adjust_salary(Employees employees){ employees_map(employees, employee_adjust_salary);}publicvoidemployees_free(Employees employees){ employees_map(employees, employee_free);free(employees);}/* I/O层 */private File file_open(String filename, String mode){ File stream = freopen(filename, mode, mode[0] == 'r'? stdin: stdout);if (stream == NULL) {fprintf(stderr, "Cannot open %s with %s mode.\n", filename, mode);exit(EXIT_FAILURE); }return stream;}private Employees file_with(String filename, String mode, Employees employees, EmployeesFn fn){int handler = dup(mode[0] == 'r'? 0: 1); /* 克隆文件句柄 */ File stream = file_open(filename, mode); employees = fn(employees); fclose(stream); fdopen(handler, mode); /* 完成后恢复标准IO */return employees;}public Employees employees_input(String filename){return file_with(filename, "r", NULL, employees_read);}publicvoidemployees_output(Employees employees, String filename){ file_with(filename, "w", employees, employees_print);}/* 应用层 */intmain(void){ Employees employees = employees_input(INPUT_FILE_NAME); /* 从文件读入 */ employees_print(employees); /* 1. 输出到屏幕 */ employees_adjust_salary(employees); /* 2. 调整薪资 */ employees_print(employees); /* 3. 输出调整后的结果 */ employees_output(employees, OUTPUT_FILE_NAME); /* 4. 保存到文件 */ employees_free(employees); /* 释放资源 */return EXIT_SUCCESS;}
                    文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    这一版本改动非常大,连应用层接口都有不向下兼容的改动,所以不要忘记回归测试。文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    本节介绍了一个重构的黑科技——动态作用域。它很有用,Web系统中 Session 变量就是动态作用域;但它也会加大判断代码所处上下文的难度,导致行为不易预测。比如JavaScript中的 this 是JS中唯一一个动态作用域的变量,看看社区对 this 的抱怨就知道它的可怕了,它的值由函数的调用方决定,很难预测后续的系统维护者会把这个函数绑定到哪个对象上。文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    简言之,动态有风险,入坑需谨慎!文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    第九版:数据结构替换

                    文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    前文都在讨论如何让代码变得更抽象、更加可维护,但到底有没有取得期望的效果,需要一个例子来证明。文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    之前的版本中,职员列表对象采用的底层存储方案是固定长度为 4 的数组结构,如果未来"work.txt"文件中的记录数不固定,希望把底层的数据结构从数组改成更合适的单链表结构。这个需求是底层数据结构的改造,理论上与应用层无关,类似从MySQL迁移到Oracle,理论上至多只能影响持久层代码,业务逻辑层等不相关的代码是不应该有任何修改的。所以,先评估一下这个需求涉及的变更点:文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    • 数据结构变化,职员列表结构体 struct _Employees 必然发生变化。文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    • 接着,职员列表对象的构造函数 employees_read 也会发生变化。文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    • 然后,与构造函数对应的析构函数 employees_print 也会变化。文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    • 最后,数据结构的迭代方法也会变化 employees_map文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    除了以上四点,其他任何与数据结构本身无关的代码都不应该发生变化。所以,代码重构完并通过测试之后,如果所有的改动范围确实只出现在上述四点中,证明前文所有的改造有效——只改动与需求相关的代码段;否则,证明代码抽象程度依旧不够,一段代码中还耦合着多个业务逻辑,依旧牵一发动全身。文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    最终重构后的完整代码如下,改造过程此处就不再详述,大家可以一起动手试着重构看看。文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    #include<stdlib.h>#include<stdio.h>#include<unistd.h>#define private static#define public#define FILE_NAME "work.txt"#define INPUT_FILE_NAME FILE_NAME#define OUTPUT_FILE_NAME FILE_NAMEtypedef FILE *File;typedefchar* String;/* 职员对象 */typedefstruct _Employee {char name[8];int age;int salary;} *Employee;typedefEmployee(*EmployeeFn)(Employee);private Employee employee_free(Employee employee){free(employee);returnNULL;}private Employee employee_read(Employee employee){ employee = (Employee) calloc(1, sizeof(struct _Employee));if (employee == NULL) {fprintf(stderr, "employee_read: out of memory\n");exit(EXIT_FAILURE); }if (scanf("%s%d%d", employee->name, &employee->age, &employee->salary) != 3) { employee_free(employee);returnNULL; }return employee;}private Employee employee_print(Employee employee){printf("%s %d %d\n", employee->name, employee->age, employee->salary);return employee;}private Employee employee_adjust_salary(Employee employee){if (employee->salary < 30000) { employee->salary += 3000; }return employee;}/* 职员列表对象 */typedefstruct _Employees { Employee employee;struct _Employees *next;} *Employees;typedefEmployees(*EmployeesFn)(Employees);private Employees employees_map(Employees employees, EmployeeFn fn){for (Employees p = employees; p; p = p->next) { p->employee = fn(p->employee); }return employees;}private Employees employees_read(Employees head){ Employees tail = NULL;for (;;) { Employee employee = employee_read(NULL);if (employee == NULL) {return head; } Employees employees = (Employees) calloc(1, sizeof(Employees));if (employees == NULL) {fprintf(stderr, "employees_read: out of memory\n");exit(EXIT_FAILURE); }if (tail == NULL) { head = tail = employees; } else { tail->next = employees; tail = tail->next; } tail->employee = employee; }}public Employees employees_print(Employees employees){return employees_map(employees, employee_print);}publicvoidemployees_adjust_salary(Employees employees){ employees_map(employees, employee_adjust_salary);}publicvoidemployees_free(Employees employees){ employees_map(employees, employee_free);while (employees) { Employees e = employees; employees = employees->next;free(e); }}/* I/O层 */private File file_open(String filename, String mode){ File stream = freopen(filename, mode, mode[0] == 'r'? stdin: stdout);if (stream == NULL) {fprintf(stderr, "Cannot open %s with %s mode.\n", filename, mode);exit(EXIT_FAILURE); }return stream;}private Employees file_with(String filename, String mode, Employees employees, EmployeesFn fn){int handler = dup(mode[0] == 'r'? 0: 1); /* 克隆文件句柄 */ File stream = file_open(filename, mode); employees = fn(employees); fclose(stream); fdopen(handler, mode); /* 完成后恢复标准IO */return employees;}public Employees employees_input(String filename){return file_with(filename, "r", NULL, employees_read);}publicvoidemployees_output(Employees employees, String filename){ file_with(filename, "w", employees, employees_print);}/* 应用层 */intmain(void){ Employees employees = employees_input(INPUT_FILE_NAME); /* 从文件读入 */ employees_print(employees); /* 1. 输出到屏幕 */ employees_adjust_salary(employees); /* 2. 调整薪资 */ employees_print(employees); /* 3. 输出调整后的结果 */ employees_output(employees, OUTPUT_FILE_NAME); /* 4. 保存到文件 */ employees_free(employees); /* 释放资源 */return EXIT_SUCCESS;}
                    文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    首先执行 check.sh 检查功能是否正确,然后执行 diff 检查修改点是否有超出预期。文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    七  总结

                    文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    本文对代码做了多次迭代,介绍如何使用面向对象、函数式编程、动态作用域等方法不断抽象其中重复的代码。通过这个过程,可以看到面向对象编程和函数式编程两者并非对立,都是为了提高代码的抽象,可以相辅相成:文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    1. 函数式编程重点是增强类型系统:常见的数据类型有数值型、字符串型等,函数式编程要求函数也是一种数据类型,即代码也是一种数据。文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    2. 面向对象风格侧重于代码的组织形式:把数据和操作数据的函数组织在类中,提高内聚;对象之间通过调用开放的接口通讯,降低耦合。文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    本文只是抛砖引玉,并不是标准答案,所以并不是要求后续所有的代码都要抽象多少次才能提交。因此,首次交付出去的代码,到底要到达第几版本,这个问题留给大家自己思考。文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    在说再见之前,再分享两个关于识别重复、抽象重用的tips。文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    编码规范

                    文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    编码规范在很多地方被反复强调,也特别容易引发争论(如花括号的位置);在我看来,编码规范最大的价值是便于发现代码中的重复!文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    编程语言本身或多或少会有一些约束,例如文件必须先 openclose,这类问题一般不容易出现不一致;更多的问题并不会在语言层面做约束,例如 if else 中异常处理是放在if代码块中还是 else,这类问题没有标准答案,公说公有理婆说婆有理。编程规范用于解决第二类问题:TOOWTDI(There is Only One Way To Do It)。文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    只有统一才能清晰,清晰的代码不一定是短的代码,但啰嗦的代码一定是不清晰的,勿忘清晰是重构的基础。文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    重构顺序

                    文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    开始重构时,切记重构的元素一定要从小到大!文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    就像文章的元素,从单词、句子、段落依次递增,重构时也应遵循从小到大的原则,依次解决重复的常量/变量、语句、代码块、函数、类、库……发现重复不能只浮于表面相同,得理解其背后的意义,只有后续需要一起变化的重复才是真正的重复。从小到大的重构顺序能帮助理解每一个重复的细节,而反之却容易导致忽略这些背后的细节。文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    还记得"work.txt"这个重复的文件名吗?如果采用从大到小的重构顺序,极有可能马上抽象了一个重用的 file_open,把文件名写死在这个公共函数里。这样做的确解决了重复问题,整段代码只有这一处出现"work.txt";但是一旦输入输出的文件名变得不同,这个公共函数只能弃用。文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    传递接力棒

                    文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    本文第九版的代码远不是完美的代码,还存在不少重复:文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    • employee_reademployees_read 中都用到 calloc 分配内存空间,并检查是否分配成功。文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    • employees_print 之于 employee_printemployees_adjust_salary 之于employee_adjust_salary,区别只是前者名称多了一个s,是否有可能根据这个规则自动为 Employees 生成与 Employee 一一对应的函数?文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    • ……文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    试试有什么办法继续抽象。第二个问题是让代码生成代码,给个提示,可以用“宏”。文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    附录I:Common Lisp的解决方案

                    文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    从函数式风格重构的过程中能体会到,如果C语言能支持动态类型,就不必在 employee_read 中做强制转换;如果C语言支持匿名函数,亦不用写这么多小函数;如果C语言除了能读入整型、字符串等基础类型,还能直接读入数组、结构体等复合类型,就无需 employee_reademployee_print 等输入输出函数……文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    其实许多编程语言(如Python、Ruby、Lisp等)已经让这些“如果”变成现实!让看看Common Lisp的解决方案:文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    ;; 从文件读入(defparameter employees (with-open-file (file #P"work.lisp") ; 内置文件环绕包装 (read file))) ; 内置读取列表等复杂结构;; 1. 输出到屏幕(print employees) ; 内置输出列表等复杂结构;; 2. 调整薪资(dolist (employee employees) (if (< (third employee) 30000) (incf (third employee) 3000))) ; 就地修改;; 3. 输出调整后的结果(print employees);; 4. 保存到文件(with-open-file (file #P"work.lisp" :direction :output :if-exists :overwrite) (print employees file)) ; print是多态函数,file取代默认标准输出流
                    文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    其中work.lisp的内容是:文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    ((William 35 25000) (Kishore 41 35000) (Wallace 37 30000) (Bruce 39 29999))
                    文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    数据文件的格式是Common Lisp的列表结构,Lisp支持直接从流中读取 sexp 复杂结构,犹如JavaScript直接读写JSON结构数据。文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    2021阿里云峰会暨开发者大会文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    从重复到重用文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    大数据+AI将会产生怎样的火花?云上的大数据开发该怎么玩?开发者如何加入大数据开源社区?5月29日,阿里云开发者大会特邀阿里巴巴集团副总裁、阿里云智能计算平台事业部高级研究员贾扬清和你分享《云上大数据与AI开发范式的演进》。点击“阅读原文”,立即参与吧文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    阅读原文文章源自JAVA秀-https://www.javaxiu.com/25879.html

                    继续阅读
                    速蛙云 - 极致体验,强烈推荐!!!购买套餐就免费送各大视频网站会员!快速稳定、独家福利社、流媒体稳定解锁!速度快,全球上网、视频、游戏加速、独立IP均支持!基础套餐性价比很高!这里不多说,我一直正在使用,推荐购买:https://www.javaxiu.com/59919.html
                    weinxin
                    资源分享QQ群
                    本站是JAVA秀团队的技术分享社区, 会经常分享资源和教程; 分享的时代, 请别再沉默!
                    沙海
                    • 版权声明:本站是JAVA秀团队的技术分享社区,我们会经常分享资源和教程。
                    • 转载请注明:从重复到重用 - JAVA秀 ☜(ˆ▽ˆ)
                    匿名

                    发表评论

                    匿名网友 填写信息

                    :?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

                    确定