從重複到重用 - 上

前言

本文是我之前寫的文章——《你試過這樣寫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」。

第一部分:可維護代碼

第一版: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
", 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
", e[i].name, e[i].age, e[i].salary);
fprintf(ostream, "%s %d %d
", 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.
", 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
", 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.
", OUTPUT_FILE_NAME);
exit(EXIT_FAILURE);
}
for (int index = 0; index < RECORD_COUNT; index++) {
printf("%s %d %d
", employees[index].name, employees[index].age, employees[index].salary);
fprintf(ostream, "%s %d %d
", 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.
", 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
", 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
", 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.
", OUTPUT_FILE_NAME);
exit(EXIT_FAILURE);
}
for (int index = 0; index < RECORD_COUNT; index++) {
fprintf(ostream, "%s %d %d
", 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
", 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
");
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
", 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.
", 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.
", 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
");
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
", 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
");
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.
", 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.
", 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.
", 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
");
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
", 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
");
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.
", 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

文章為簡譯,更為詳細的內容,請查看原文。

更多技術乾貨敬請關注云棲社區知乎機構號:阿里云云棲社區 - 知乎

本文為雲棲社區原創內容,未經允許不得轉載。

推薦閱讀:

TAG:程序 | 編程 | C(編程語言) |