从重复到重用

张泽鹏
2018-01-22程序员

前言

本文是我以前写的文章——《你试过这样写C程序吗》——的第二版,并把文章名改为更贴切的“从重复到重用”。算法

开发技术的发展,从第一次提出“函数/子程序”,实现代码级重用;到面向对象的“类”,重用数据结构与算法;再到“动态连接库”、“控件”等重用模块;到现在流行的云计算、微服务可重用整个系统。技术发展虽然突飞猛进,但本质都是重用,只是粒度不一样。因此写代码的动机都应是把重复的工做变成可重用的方案,其中重复的工做包括业务上重复的场景、技术上重复的代码等。合格的系统能够简化当下重复的工做;优秀的系统还能预见将来重复的工做。编程

本文不谈框架、不谈架构,就谈写代码的那些事儿!后文始终围绕一个问题的解决方案,不断发现其中“重复”的代码,并提炼出“可重用”的抽象,持续“重构”。但愿经过这个过程和你们分享一些发现重复代码和提炼可重用抽象的方法。数组

问题

做为贯穿全文的主线,这有一个任务须要开发一个程序来完成:有一份存有职员信息(姓名、年龄、工资)的文件“work.txt”,内容以下:数据结构

William 35 25000
Kishore 41 35000
Wallace 37 30000
Bruce 39 29999
  1. 要求从文件(work.txt)中读取员工薪酬,并输出到屏幕上。
  2. 为全部工资小于三万的员工涨 3000 元。
  3. 在屏幕上输出薪资调整后的结果。
  4. 把调整后的结果保存到原始文件。

即运行的结果是屏幕上要有八行输出,“work.txt”的内容将变成:多线程

William 35 28000
Kishore 41 35000
Wallace 37 30000
Bruce 39 32999

测试

在明确了需求以后,第一步要作的是写测试代码,而不是写功能代码。《重构》一书中对重构的定义是:“在不改变代码外在行为的前提下,对代码作出修改,以改进程序的内部结构。”其中明确指出“代码外在行为”是不改变的!在不断迭代重构时,“保证每次重构的行为不变”也是一项重复的工做,因此测试先行不只能尽早地校验对需求理解的正确性、还能避免重复测试。本文经过一段Shell脚本完成如下工做:架构

  • 初始化work.txt文件。
  • 检查标准输出的内容与指望的结果是否一致。
  • 检查修改后work.txt文件的内容是否与指望一致。
  • 清理现场。
#!/bin/sh

if [ $# -eq 0 ]; then
    echo "usage: $0 <c-source-file>" >&2
    exit -1
fi

input=$(cat <<EOF
William 35 25000
Kishore 41 35000
Wallace 37 30000
Bruce 39 29999
EOF
)

output=$(cat <<EOF
William 35 28000
Kishore 41 35000
Wallace 37 30000
Bruce 39 32999
EOF
)

echo "$input" > work.txt
echo "$input" > .expect.stdout.txt
echo "$output" >> .expect.stdout.txt
echo "$output" > .expect.work.txt
(gcc "$1" -o main && ./main | diff .expect.stdout.txt - && diff .expect.work.txt work.txt) && echo PASS || echo FAIL
rm -f main work.txt .expect.work.txt .expect.stdout.txt

将上述代码保存成check.sh,待测试的源文件名做为参数。若是程序经过,会显示“PASS”,不然会输出不一样的行以及“FAIL”。app

第一部分:可维护代码

初版:It works

每位熟练的程序员都能快速地给出本身的实现。本文示例代码使用ANSI C99编写,Mac下用gcc能正常编译运行,其余环境未测试。选择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;
}

其中第一个循环从 work.txt 中读取4行数据,并把信息输出到屏幕(需求#1);同时为薪资小于三万的职员增长三千元(需求#2);第二个循环遍历全部数据,把调整后的结果输出屏幕(需求#3),并保存结果到 work.txt(需求#4)。

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

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

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

  1. 魔法常量4重复出现,后续负责维护的程序员没法判断它们是碰巧相等仍是有其余缘由必需相等。
  2. 文件名work.txt重复出现。
  3. 重复且不清晰的文件指针类型定义,容易忽略ostream前面的*
  4. ei变量命名不顾名思义。
  5. 变量的定义与使用离得太远。
  6. 无异常处理,文件可能不可读。

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

  1. 确认数字4在三处的意义都是员工记录数,所以定义共享常量#define RECORD_COUNT 4
  2. 常量"work.txt"4不一样,内容虽然相同但意义不一样:一个做输入,一个做输出。若是也只简单的定义一个常量FILE_NAME共用,后续二者独立变化时,工做量并没减小。因此去除重复代码时,切忌只看表面相同,背后意义相同的才是真正的相同,不然就像给全部常量1定义ONE别名同样没有意义。因此须要定义三个常量FILE_NAMEINPUT_FILE_NAMEOUTPUT_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_NAME

typedef 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_NAME

typedef 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,确保程序的行为没有改变。

第二部分:面向对象风格

第四版:职员对象抽象

通过两轮改造,代码结构已足够清晰;如今能够开始重构,来梳理代码层次。

最显眼的就是格式化输出职员信息:除了输出流不一样,格式、内容彻底相同,四条需求中出现了三次。通常遇到相同/类似代码时,能够抽象出一个函数:相同的部分写在函数体中,不一样的部分做为参数传入。此处,能抽象出一个以结构体数据和文件流为入参的函数,但目前这个结构体仍是匿名的,没法做为函数的参数,因此第一步得先给匿名的职员结构体取一个合适的类型名称:

typedef struct _Employee {
  char name[8];
  int age;
  int salary;
} *Employee;

而后抽象公共函数用于格式化输出EmployeeFile,这其中还耦合了两个功能:

  1. Employee序列化成字符串。
  2. 序列化结果输出到指定文件流。

由于暂无独立使用某项功能的场景,目前无需进一步拆分:

void employee_print(Employee employee, File ostream) {
  fprintf(ostream, "%s %d %d\n", employee->name, employee->age, employee->salary);
}

Employee结构体+employee_print函数很容易联想到面向对象的“类”。面向对象的本质是由一组功能独立的对象组成系统,对象之间经过发消息协做完成任务,不见得非要有class关键字,继承、封装、多态等语法糖。

  • 对象的“功能独立”,即高内聚,要求数据和操做数据的相关方法放在一块儿,大多数支持面向对象的编程语言都提供了class关键字,在语言层面强制捆绑,C语言并无这样的语法,但能够制定编码规范,让数据结构与函数在物理上挨得更近。
  • “给对象发消息”,不一样的编程语言里表现形式各不相同,例如在Javafoo.baz()就是向foo对象发送baz消息,C++中等价的语法是foo->baz()Smalltalk中是foo bazC语言则是baz(foo)

综上所述,虽然C语言一般被认为不是面向对象的语言,其实它也能支持面向对象风格。沿上述思路,能够抽象出职员对象的四个方法:

  • employee_read:构造函数,分配空间、输入并反序列化,相似于Javanew
  • employee_free:析构函数,释放空间,即纯手工的GC
  • employee_print:序列化并输出。
  • employee_adjust_salary:调整职员薪资,惟一的业务逻辑。

有了职员对象,程序再也不只有一个main函数。假设把main函数看做应用层,其余函数看做类库、框架或中间件,这样程序有了层级,层间仅经过开放的接口通信,即对象的封装性。

Java中有publicprotecteddefaultprivate四种可见性修饰符,C语言的函数默认是公开的,加上static关键字后只在当前文件可见。为避免应用层向对象随意发送消息,约定只有在应用层用到的函数才公开,因此额外定义了publicprivate两个修饰符,目前职员对象的四个方法都是公开的。

重构以后的完整代码以下:

#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_NAME

typedef FILE *File;

/* 职员对象 */

typedef struct _Employee {
  char name[8];
  int age;
  int salary;
} *Employee;

public void employee_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);
    return NULL;
  }
  return employee;
}

public void employee_print(Employee employee, File ostream) {
  fprintf(ostream, "%s %d %d\n", employee->name, employee->age, employee->salary);
}

public void employee_adjust_salary(Employee employee) {
  if (employee->salary < 30000) {
    employee->salary += 3000;
  }
}

/* 应用层 */

int main(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;
}

将代码保存为4.c,照例执行./check.sh 4.c检测是否有改变程序行为。

第五版:容器对象抽象

以前的重构,去除了词法和句法上的重复,就像一篇文章里的单词和语句,接着能够看段落有没有重复,即代码块。

employee_print相似,三段循环输出职员信息代码也是明显的重复,能够抽象出employees_print,同时也抽象出另外一个对象——职员列表——Employees。参考职员对象,能够抽象出四个与之对应的函数:

  • employees_read:构造函数,分配列表空间,并依次建立职员对象。
  • employees_free:析构函数,释放列表空间,以及职员对象的空间。
  • employees_print:序列化并输出列表中每一位职员信息。
  • employees_adjust_salary:调整全部符合要求职员的薪资。

此时,main函数只需调用职员列表对象的方法,再也不直接调用职员对象的方法,因此后者可见性从public降为private

重构以后的完整代码以下:

#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_NAME

typedef FILE *File;

/* 职员对象 */

typedef struct _Employee {
  char name[8];
  int age;
  int salary;
} *Employee;

private void employee_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);
    return NULL;
  }
  return employee;
}

private void employee_print(Employee employee, File ostream) {
  fprintf(ostream, "%s %d %d\n", employee->name, employee->age, employee->salary);
}

private void employee_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;
}

public void employees_print(Employees employees, File ostream) {
  for (int index = 0; index < RECORD_COUNT; index++) {
    employee_print(employees[index], ostream);
  }
}

public void employees_adjust_salary(Employees employees) {
  for (int index = 0; index < RECORD_COUNT; index++) {
    employee_adjust_salary(employees[index]);
  }
}

public void employees_free(Employees employees) {
  for (int index = 0; index < RECORD_COUNT; index++) {
    employee_free(employees[index]);
  }
  free(employees);
}

/* 应用层 */

int main(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;
}

不要忘记运行./check.sh做回归测试。

第六版:输入输出抽象

此时的main函数已经比较清爽,剩下一处明显的重复:打开文件并检查文件是否正常打开。这属于文件相关的操做,能够抽象出一个file_open代替fopen

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;
}

接着能够继续抽象职员列表对象的输入和输出方法:

  • employees_input:从文件中获取数据并建立职员列表对象。
  • employees_output:将职员列表对象的内容输出到文件。

重构后employees_read再也不被main访问,因此改为private。重构后的完整代码以下:

#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_NAME

typedef FILE *File;
typedef char* String;

/* 职员对象 */

typedef struct _Employee {
  char name[8];
  int age;
  int salary;
} *Employee;

private void employee_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);
    return NULL;
  }
  return employee;
}

private void employee_print(Employee employee, File ostream) {
  fprintf(ostream, "%s %d %d\n", employee->name, employee->age, employee->salary);
}

private void employee_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;
}

public void employees_print(Employees employees, File ostream) {
  for (int index = 0; index < RECORD_COUNT; index++) {
    employee_print(employees[index], ostream);
  }
}

public void employees_adjust_salary(Employees employees) {
  for (int index = 0; index < RECORD_COUNT; index++) {
    employee_adjust_salary(employees[index]);
  }
}

public void employees_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;
}

public void employees_output(Employees employees, String filename) {
  File ostream = file_open(filename, "w");
  employees_print(employees, ostream);
  fclose(ostream);
}

/* 应用层 */

int main(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;
}

别忘记执行./check.sh

第三部分:函数式编程

第七版:容器迭代重用

如今,main里只用到了职员列表相关的函数,且代码和需求几乎一一对应。这些函数能够当作职员管理领域的DSL,领域特定语言是业务和技术双方的共识,理论上需求不变,基于DSL开发的业务代码也不变。以前全部的改动仅要求main行为一致,后续的重构还要尽可能保证main自身也无任何变化,即API向后兼容。

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

for (int index = 0; index < RECORD_COUNT; index++) {
  employee_adjust_salary(employees[index]);
}

for (int index = 0; index < RECORD_COUNT; index++) {
  employee_free(employees[index]);
}

除了循环体中分别调用了employee_adjust_salaryemployee_free,其他都一摸同样,即它们的迭代规则相同,而循环体不一样。是否有可能自定义一个for语句代替这些重复的迭代?

在大多数编程语言中,iffor等控制语句是一种特殊的存在,开发者一般没法自定义。这是iffor在大多数语言中的样子:

if (condition) {
  ...
}

for (init; term; inc) {
  ...
}

若是把它们想象成是函数,语法能够改为更熟悉的函数调用形式:

if (condition, {
  ...
});

for (init, term, inc, {
  ...
});

和普通函数调用相比,惟一不一样的是容许花括号包围的代码片断做为参数。所以,若编程语言容许代码做为函数的参数,那就能自定义新的控制语句!这句话隐含了两个语言特性:

  1. 代码是一种数据类型。
  2. 代码类型的数据可做为函数的参数。

全部编程语言都包含一套类型系统,它决定数据的类型,而数据的类型又决定数据的功能。例如,数值类型能够作四则运算;字符串类型的数据能够拼接、查找、替换等;代码若是也是一种数据类型,就能够随时“执行”它。C语言中具有“执行”能力的元素就是“函数”,函数之于代码类型,犹如intdouble之于数值类型,都只是C这个特定编程语言对特定类型的特定实现,换成Visual Basic改叫“过程”,换成Java又称做“成员方法”。

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

  1. 能够用变量命名。
  2. 能够提供给过程做为参数。
  3. 能够由过程做为结果返回。
  4. 能够包含在数据结构中。

对照之下会惊讶地发现,C语言这门看似与函数式编程最远的上古编程语言,利用函数指针,竟然也彻底符合上述条件。观察employee_adjust_salaryemployee_free两个函数,都只有一个Employee类型的参数且没有返回值,翻译成C语言就是typedef void (*EmployeeFn)(Employee),把它做为函数的参数,就能抽象出:

private void employees_each(Employees employees, EmployeeFn fn) {
  for (int index = 0; index < RECORD_COUNT; index++) {
    fn(employees[index]);
  }
}

在函数式语言中,这类将函数做为参数或返回值的函数称为高阶函数,C语言里称为控制语句。用这个自定义的控制语句代替原生的for循环,则代码能够简化成:

employees_each(employees, employee_adjust_salary);
employees_each(employees, employee_free);

不过,此时还只解决了一半问题:employees_reademployees_print中依然有重复的for循环,并没有法用employees_each简化。缘由是这些循环体中函数调用的参数数目与类型和EmployeeFn不兼容:

  • employee_read:包含File类型的参数,返回Employee类型。
  • employee_print:包含EmployeeFile两类参数,无返回值。
  • EmployeeFn:包含Employee类型的参数,无返回值。

想涵盖全部场景,最简单的方法就是提取一个参数与返回结果的全集——Employee (*EmployeeFn)(Employee, File)——包含EmployeeFile两个类型的参数,且返回Employee类型的结果。用新接口重构Employee的四个方法:

  • 忽略无用的参数。
  • 除了employee_free返回NULL,其余都返回Employee入参。

同时,须要改造employees_each去适应新接口:加入File参数,以及返回处理结果。在编程的语义中,单纯利用反作用的迭代被称为foreach,而关注迭代每一个元素的处理结果则称为map,即映射。所以,用employees_map取代以前的employees_each

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;
}

重构后的完整代码以下:

#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_NAME

typedef FILE *File;
typedef char* String;

/* 职员对象 */

typedef struct _Employee {
  char name[8];
  int age;
  int salary;
} *Employee;

typedef Employee (*EmployeeFn)(Employee, File);

private Employee employee_free(Employee employee, File stream) {
  free(employee);
  return NULL;
}

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);
    return NULL;
  }
  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);
}

public void employees_print(Employees employees, File ostream) {
  employees_map(employees, ostream, employee_print);
}

public void employees_adjust_salary(Employees employees) {
  employees_map(employees, NULL, employee_adjust_salary);
}

public void employees_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;
}

public void employees_output(Employees employees, String filename) {
  File ostream = file_open(filename, "w");
  employees_print(employees, ostream);
  fclose(ostream);
}

/* 应用层 */

int main(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 5引入foreach语法糖、Java 7引入try-with-resource语法糖,在Java 8以前想要任何新的语言特性只能等Oracle大发慈悲,Java 8以后想要任何语言特性就能够自给自足!

通过这么大的改造,切勿忘记测试!

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

上一版本的代码虽然能够工做,但也暴露出一个常见问题:函数的参数不断膨胀。这个问题在程序的层次不断增长过程会慢慢滋生。例如函数A会调用BB又调用C,假设C须要一个文件对象,假设B中并不建立文件对象,就得从A依次传递到B再传递到C。函数调用的层次越深,数据逐层传递的问题就越严重,上层函数的入参就会爆炸!

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

int is_debug = 0;

void a() {
  if (is_debug == 1) {
    printf("debug is enable\n");
  }
  printf("call a()\n");
}

void b() {
  a();
  printf("call b()\n");
}

void c() {
  int original = is_debug;
  is_debug = 1;
  b();
  is_debug = original;
}

其中函数c临时开启了调试选项,并在退出前恢复成原始值。一旦忘记恢复,后续全部调试信息就都会输出,恶梦就会开始。为避免这种尴尬问题,能够利用上一版本中提到的函数式编程的方法,将重复的开启选项、恢复工做抽象成函数:

typedef void (*Callback)(void);

void with_debug(Callback fn) {
  int original = is_debug;
  is_debug = 1;
  fn();
  is_debug = original;
}

void c() {
  with_debug(b);
}

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

综上所述,咱们真正须要的功能彷佛是一种代码的包装能力:全局变量某个特定的值只在指定范围内生效(包括范围内代码调用的函数、调用函数的调用等等),相似于会话级别的变量。这种功能被裁剪的全局变量在编程语言中称为动态做用域(Dynamic Scope)变量。

大多数主流编程语言只支持静态做用域——也叫词法做用域——在编译时静态肯定的做用域;但动态做用域是在运行过程当中动态肯定的。简言之,静态做用域由代码的层次结构决定,动态做用域由调用的堆栈层次结构决定。如下代码是Perl语言动态做用域变量的示例,保存成demo.pl,执行perl demo.pl能输出$v = 1

sub foo {
    print "\$v = $v\n";
}

sub baz {
    local $v = 1;
    foo;
}

baz;

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

void file_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 */
}

有了这个功能,能够删除掉全部函数和接口的File file参数!惟一真正和文件相关的只剩下employees_inputemployees_output,它们分别调用Employees employees_read()void employees_print(Employees),为了使用file_with作统一的重定向,利用上一版接口全集的方法,把它们的接口统一改为typedef Employees (*EmployeesFn)(Employees);。最终,重构后的完整代码以下:

#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_NAME

typedef FILE *File;
typedef char* String;

/* 职员对象 */

typedef struct _Employee {
  char name[8];
  int age;
  int salary;
} *Employee;

typedef Employee (*EmployeeFn)(Employee);

private Employee employee_free(Employee employee) {
  free(employee);
  return NULL;
}

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);
    return NULL;
  }
  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;

typedef Employees (*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);
}

public void employees_adjust_salary(Employees employees) {
  employees_map(employees, employee_adjust_salary);
}

public void employees_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);
}

public void employees_output(Employees employees, String filename) {
  file_with(filename, "w", employees, employees_print);
}

/* 应用层 */

int main(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;
}

这一版本改动很是大,连应用层接口都有不向下兼容的改动,因此不要忘记回归测试。

本节介绍了一个重构的黑科技——动态做用域。它颇有用,Web系统中Session变量就是动态做用域;但它也会加大判断代码所处上下文的难度,致使行为不易预测。好比JavaScript中的thisJS中惟一一个动态做用域的变量,看看社区对this的抱怨就知道它的可怕了,它的值由函数的调用方决定,很难预测后续的系统维护者会把这个函数绑定到哪一个对象上。

简言之,动态有风险,入坑需谨慎!

第九版:数据结构替换

前文都在讨论如何让代码变得更抽象、更加可维护,但到底有没有取得指望的效果,须要一个例子来证实。

以前的版本中,职员列表对象采用的底层存储方案是固定长度为4的数组结构,若是将来"work.txt"文件中的记录数不固定,但愿把底层的数据结构从数组改为更合适的单链表结构。这个需求是底层数据结构的改造,理论上与应用层无关,相似从MySQL迁移到Oracle,理论上至多只能影响持久层代码,业务逻辑层等不相关的代码是不该该有任何修改的。因此,先评估一下这个需求涉及的变动点:

  • 数据结构变化,职员列表结构体struct _Employees必然发生变化。
  • 接着,职员列表对象的构造函数employees_read也会发生变化。
  • 而后,与构造函数对应的析构函数employees_print也会变化。
  • 最后,数据结构的迭代方法也会变化employees_map

除了以上四点,其余任何与数据结构自己无关的代码都不该该发生变化。因此,代码重构完并经过测试以后,若是全部的改动范围确实只出如今上述四点中,证实前文全部的改造有效——只改动与需求相关的代码段;不然,证实代码抽象程度依旧不够,一段代码中还耦合着多个业务逻辑,依旧牵一发动全身。

最终重构后的完整代码以下,改造过程此处就再也不详述,你们能够一块儿动手试着重构看看。

#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_NAME

typedef FILE *File;
typedef char* String;

/* 职员对象 */

typedef struct _Employee {
  char name[8];
  int age;
  int salary;
} *Employee;

typedef Employee (*EmployeeFn)(Employee);

private Employee employee_free(Employee employee) {
  free(employee);
  return NULL;
}

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);
    return NULL;
  }
  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 struct _Employees {
  Employee employee;
  struct _Employees *next;
} *Employees;

typedef Employees (*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);
}

public void employees_adjust_salary(Employees employees) {
  employees_map(employees, employee_adjust_salary);
}

public void employees_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);
}

public void employees_output(Employees employees, String filename) {
  file_with(filename, "w", employees, employees_print);
}

/* 应用层 */

int main(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;
}

首先执行check.sh检查功能是否正确,而后执行diff检查修改点是否有超出预期。

总结

本文对代码作了屡次迭代,介绍如何使用面向对象、函数式编程、动态做用域等方法不断抽象其中重复的代码。经过这个过程,能够看到面向对象编程和函数式编程二者并不是对立,都是为了提升代码的抽象,能够相辅相成:

  1. 函数式编程重点是加强类型系统:常见的数据类型有数值型、字符串型等,函数式编程要求函数也是一种数据类型,即代码也是一种数据。
  2. 面向对象风格侧重于代码的组织形式:把数据和操做数据的函数组织在类中,提升内聚;对象之间经过调用开放的接口通信,下降耦合。

本文只是抛砖引玉,并非标准答案,因此并非要求后续全部的代码都要抽象多少次才能提交。所以,首次交付出去的代码,到底要到达第几版本,这个问题留给你们本身思考。

在说再见以前,再分享两个关于识别重复、抽象重用的tips。

编码规范

编码规范在不少地方被反复强调,也特别容易引起圣战(如花括号的位置);在我看来,编码规范最大的价值是便于发现代码中的重复!

编程语言自己或多或少会有一些约束,例如文件必须先openclose,这类问题通常不容易出现不一致;更多的问题并不会在语言层面作约束,例如if else中异常处理是放在if代码块中仍是else,这类问题没有标准答案,公说公有理婆说婆有理。编程规范用于解决第二类问题:TOOWTDI(There is Only One Way To Do It)。

只有统一才能清晰,清晰的代码不必定是短的代码,但啰嗦的代码必定是不清晰的,勿忘清晰是重构的基础。

重构顺序

开始重构时,切记重构的元素必定要从小到大!

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

还记得"work.txt"这个重复的文件名吗?若是采用从大到小的重构顺序,极有可能立刻抽象了一个重用的file_open,把文件名写死在这个公共函数里。这样作的确解决了重复问题,整段代码只有这一处出现"work.txt";可是一旦输入输出的文件名变得不一样,这个公共函数只能弃用。

传递接力棒

本文第九版的代码远不是完美的代码,还存在很多重复:

  • employee_reademployees_read中都用到calloc分配内存空间,并检查是否分配成功。
  • employees_print之于employee_printemployees_adjust_salary之于employee_adjust_salary,区别只是前者名称多了一个s,是否有可能根据这个规则自动为Employees生成与Employee一一对应的函数?
  • ……

试试有什么办法继续抽象。第二个问题是让代码生成代码,给个提示,能够用“宏”。

附录I:Common Lisp的解决方案

从函数式风格重构的过程当中能体会到,若是C语言能支持动态类型,就没必要在employee_read中作强制转换;若是C语言支持匿名函数,亦不用写这么多小函数;若是C语言除了能读入整型、字符串等基础类型,还能直接读入数组、结构体等复合类型,就无需employee_reademployee_print等输入输出函数……

其实许多编程语言(如PythonRubyLisp等)已经让这些“若是”变成现实!让看看Common Lisp的解决方案:

;; 从文件读入
(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)
  (print employees file)) ; print是多态函数,file取代默认标准输出流

其中work.lisp的内容是:

((William 35 25000)
 (Kishore 41 35000)
 (Wallace 37 30000)
 (Bruce 39 29999))

数据文件的格式是Common Lisp的列表结构,Lisp支持直接从流中读取sexp复杂结构,犹如JavaScript直接读写JSON结构数据。

相关文章
相关标签/搜索