你試過這樣寫C程序嗎?
本文將采用C語言解決一個問題,圍繞這個問題不斷地變化需求、重構代碼,分別展示兩種風格如何從不同的側面提高代碼的可維護性。
摘要
面向對象風格和函數式編程風格是編寫代碼的兩種風格,面向對象風格早為大眾所認知,函數式風格也漸漸受到大家的關注。網上為其布道的文章不少,有人贊揚有人不屑,但鮮有展示一個完整例子的。例如很多人對函數式風格的印象只是“有人說它很好,但不清楚到底好在哪兒,更不知如何在實際的項目中獲益”。
本文將采用C語言解決一個問題,圍繞這個問題不斷地變化需求、重構代碼,分別展示兩種風格如何從不同的側面提高代碼的可維護性。如果你沒有耐心讀完這篇長文章,可以參見:[附錄II]直接看代碼,但這篇文章會向你解釋為什么代碼會寫成這樣,以及寫成這樣的好處。
注:本文純屬個人觀點,如有雷同,非常榮幸!
關鍵字:C語言; 結構化編程; 面向對象編程; 函數式編程
什么是函數式風格?
面向對象風格大家都耳熟能詳,而提到函數式風格,腦海中或多或少會閃過一些耳熟能詳的名詞:無副作用、無狀態、易于并行編程,甚至是Lisp那扭曲的前綴表達式。追根溯源,函數式風格源自λ演算:函數能作為值傳遞給其他函數或由其他函數返回。其中“函數”是一種抽象的概念,可以理解成代碼塊,在C語言里叫函數或過程,在Java中叫成員方法……因此,函數式風格的本質是函數作為“第一等公民”。在我看來,諸如閉包、匿名函數等特性僅是添頭,例如Emacs Lisp最初不支持閉包,但不影響它是一門支持函數式風格的編程語言。
有些人會把函數式風格與面向對象風格對立起來,但在我看來這兩種風格都是為了提高代碼的可維護性,可以相輔相成:
- 函數式風格重點是增強類型系統:一些編程語言提供的基礎數據類型僅有數值型和字符串型,函數式風格要求函數也是基礎數據類型,即代碼也是一種數據;
- 面向對象風格側重代碼的組織形式:要求把數據和操作這些數據的函數組織在同一個類中,提高內聚;對象之間通過調用開放的接口通訊,降低耦合。
代碼即數據的作用?
使用不支持函數式風格的編程語言開發,將迫使我們永遠在語言恰好提供的基礎功能上工作。例如迭代只能使用for、while等關鍵字;讀寫文件每次都要寫fopen、fclose;并行加鎖也少不了lock和unlock。面對這些大同小異的冗余代碼總會很無奈:如果XX語言能提供XX特性該多好啊!
代碼即數據讓這一切成為可能,它允許你自定義控制語句。如果語言不支持某個期望的特性,那就自己動手加一個!后文將展示如何自定義控制語句,以及它如何提高代碼的可維護性。
為什么選C語言?
函數若要作為“第一等公民”,至少需要滿足以下四條特權:
- 可以用變量命名;
- 可以提供給過程作為參數;
- 可以由過程作為結果返回;
- 可以包含在數據結構中。
對照之下會驚訝地發現,C這門看似離函數式風格最遠的編程語言居然也符合上述條件;此外,相比其他對函數式風格支持更好的語言(如Lisp、Haskell等),至少C的語法不那么古怪;何況熟悉C語系(如Java、C#等)語法的同學也更多,方便大家用自己熟悉的語言實踐。
問題描述
作為貫穿全文的主線,這有一個問題需要你開發一個C程序來完成任務:有一個存有職員信息(姓名、年齡、工資)的文件“work.txt”,內容如下:
William 35 25000 Kishore 41 35000 Wallace 37 20000 Bruce 39 15000
- 要求從文件中讀取這些信息,并輸出到屏幕上;
- 為所有工資小于三萬的員工漲3000元;
- 在屏幕上輸出薪資調整后的結果;
- 把調整后的結果保存到原始文件。
即運行的結果是屏幕上要有八行輸出,“work.txt”的內容將變成:
William 35 28000 Kishore 41 35000 Wallace 37 23000 Bruce 39 18000
快速實現
這個問題很簡單,簡單到把所有代碼都塞到main函數里也不覺得太長:
#include <stdio.h>
int main(void) {
struct {
char name[8];
int age;
int salary;
} e[4];
FILE *istream, *ostream;
int i, length;
istream = fopen("work.txt", "r");
for (i = 0; fscanf(istream, "%s%d%d", e[i].name, &e[i].age, &e[i].salary) == 3; i++)
printf("%s %d %d\n", e[i].name, e[i].age, e[i].salary);
length = i;
fclose(istream);
ostream = fopen("work.txt", "w");
for (i = 0; i < length; i++) {
if (e[i].salary < 30000)
e[i].salary += 3000;
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中讀數據,直到文件末尾,同時把信息輸出到屏幕,即實現了需求#1;第二個循環遍歷所有數據,為薪資小于三萬的職員增加三千元(需求#2),并把調整后的結果輸出屏幕(需求#3)和work.txt中(需求#4)。
當變化來臨時
上面的代碼簡潔明了,而且運行良好,作為應付無需維護、需求亦不會變化的課后作業綽綽有余。可惜,我們沒有活在新聞聯播里,需求總在不斷地變化,以至于要不停地維護代碼。下面從維護的角度羅列幾個問題,并嘗試重構。
當文件打開失敗時
程序發布之后,就面臨各種苛刻的運行環境,例如文件work.txt可能沒有讀或寫權限。代碼的維護者需要通過錯誤日志里的信息定位出錯的位置,但不是所有環境都會提供充足的信息,例如Linux下,沒有讀或寫權限都只輸出“Segmentation fault”,僅憑這段錯誤信息無法確定是哪一句fopen出錯。
當職員信息數量變化時
樣例中只有4條記錄,不意味著真實環境中永遠只有4條記錄,甚至可以認為記錄的數目是不確定的。臆斷結構體數組的最大長度是4或其他數值都是不合適的,需要能自適應不同的數目。
當字段類型變化時
雖然樣例中工資都是整數,但真實環境中工資很可能是浮點數。把int salary改成float salary意味著所有涉及輸入輸出的地方都要修改:%d換成%f。
在短短不到30行的代碼里尚且有4處需要修改,換成龐大的項目,維護成本將不可估量。
當字段數目變化時
客戶提出職員信息中需新增一列,保存員工入職的年份。這帶來的影響和上個問題一樣。
當業務邏輯變化時
本例的業務邏輯就是調薪和輸出,幾乎都集中在了第二個循環體中。如果不斷地增加新的業務邏輯,循環體就會爆炸式地增長。而且業務邏輯可能需要相互組合,代碼就變得雜亂無章。
面向對象風格
上節提到的變化都很常見,你肯定還能想出更多。它們綜合的維護成本已不比完全重寫低,即代碼應對合理需求變化的能力差,可維護性低。究其原因,是相同或相似的代碼散落在多處,因此一個變化就引起多處更改,誤改或漏改都在所難免。
回顧前文面向對象風格的宗旨:把數據和操作數據的函數集中在一起,開放操作數據的接口供其它對象或方法調用。這恰好能解決把操作數據的方法散落在各處的問題,下面就用面向對象的思想重構代碼。
抽象數據結構
首先需要抽象出要處理的對象類型,此處為結構體命名即可:
typedef struct _Employee {
...
} *Employee; 需要注意的是,Employee是結構體_Employee的指針,因為操作結構體,使用指針比直接使用對象更頻繁。
接著要選擇一種數據結構作為容器,由于數據是一組個數不確定的線性結構,單鏈表正好適合這樣的場景:
typedef struct _Employee {
String name;
int age;
int salary;
struct _Employee *next;
} *Employee; 開放接口
根據需求,職員對象至少提供從文件中讀取信息、輸出到屏幕、保存到文件、調整薪資四項功能。其中輸出到屏幕和保存到文件可以合并成輸出到輸出流中,因此它將開放以下四個接口:
employee_read:批量從輸入流中讀取職員信息并返回employee_free:批量釋放動態申請的空間employee_print:批量輸出職員信息到輸出流employee_adjust_salary:遍歷職員信息并調整薪資
構造函數
即創建并初始化對象的函數。
static Employee employee_read_node(File istream) {
Employee e = (Employee)calloc(1, sizeof(struct _Employee));
if (e != NULL && fscanf(istream, "%s%d%d", e->name, &e->age, &e->salary) != 3) {
employee_free(e);
e = NULL;
}
return e;
} 構造函數先通過calloc申請了一片內存空間(并自動初始化為0),再從給定的輸入流中讀取職員信息來初始化對象,如果輸入流中沒有更多的數據,就釋放空間并返回空指針。
該函數只能構造單個對象,而文件中有一組對象,且需要串聯成單鏈表結構,因此接口employee_read的工作就是組織這些對象:
Employee employee_read(File istream) {
Employee e = NULL, head = NULL, tail = NULL;
while (e = employee_read_node(istream)) {
if (head != NULL) {
tail->next = e;
tail = e;
} else {
head = tail = e;
}
}
return head;
} 由于employee_read_node是一個輔助函數,不是對外開放的接口,所以使用static修飾把作用域限定在當前文件。
析構函數
因為對象的空間是動態申請的,需要提供手工釋放的析構函數,即employee_free:
void employee_free(Employee e) {
Employee p;
while (p = e) {
e = e->next;
free(p);
}
} 輸出
如果說輸入是把字符串反序列化成對象的過程,那輸出就是輸入的逆運算,即把對象序列化成字符串的過程。因此,輸出的要求是格式必須和輸入文件保持一致,允許程序多次處理。此處的輸出就是遍歷整個集合并輸出到輸出流:
void employee_print(File ostream, Employee e) {
for (; e; e = e->next) {
fprintf(ostream, "%s %d %d\n", e->name, e->age, e->salary);
}
} 核心業務邏輯:調整薪資
與輸出類似,調整薪資也是遍歷整個集合,為符合要求的職員調薪:
void employee_adjust_salary(Employee e) {
for (; e; e = e->next) {
if (e->salary < 30000) {
e->salary += 3000;
}
}
} 解決問題
經過以上幾個步驟,為職員信息管理這個領域定義了一套方便的接口。此時的main函數不用再操心數據具體以什么形式組織、如何獲取、如何輸出,只需向Employee對象發送消息(調用接口)即可完成任務。
int main(void) {
File istream, ostream;
Employee e = NULL;
istream = fopen("work.txt", "r");
if (istream == NULL) {
fprintf(stderr, "Cannot open work.txt with r mode.\n");
exit(EXIT_FAILURE);
}
e = employee_read(istream);
fclose(istream);
employee_print(stdout, e);
employee_adjust_salary(e);
employee_print(stdout, e);
ostream = fopen("work.txt", "w");
if (ostream == NULL) {
fprintf(stderr, "Cannot open work.txt with w mode.\n");
exit(EXIT_FAILURE);
}
employee_print(ostream, e);
fclose(ostream);
employee_free(e);
return EXIT_SUCCESS;
} 重構后的代碼與需求的描述更接近,雖然代碼量膨脹了三倍,但能解決前文的問題:
- 文件指針做空指針檢查
- 單鏈表容量能自動擴展
- 字段類型或數目變化時僅修改輸入和輸出兩處
- 每項業務邏輯為獨立的函數,易擴展且組合靈活
完整的代碼請參見:[附錄I]
歡迎變化再次光臨
經過重構的代碼可維護性更好,因為每個函數的職責是單一的:
employee_read_node:應對輸入源的變化,如列的順序改變;employee_read:應對集合結構的變化,如單向鏈表改成雙向鏈表;employee_print:應對輸出格式的變化,如輸出成CSV結構;employee_adjust_salary:應對業務邏輯的變化,如調薪幅度增大。
不過,代碼仍有不少重復之處,“重復”是維護性的大敵。想想你會如何應付下面這些問題?
數據源升級
上游系統在升級后,work.txt的第一行提供了行數:
4 William 35 25000 Kishore 41 35000 Wallace 37 20000 Bruce 39 15000
而且,原系統頻繁地申請空間也影響到性能。經過權衡,決定用數組取代單鏈表,這樣只需一次性申請足夠大的空間。
憑借面向對象風格的優勢,對接口的實現的修改不會影響接口的使用,因此main函數無需任何修改。但對Employee對象而言卻是災難:每個接口的實現都與內部數據結構緊緊地綁在一起。幾乎所有實現里都用for或while循環遍歷整個鏈表,底層數據結構的變化意味著遍歷方式的變化,即所有接口的實現全部需要重寫!
優雅的訪問文件
但凡涉及訪問文件的代碼,都需要fopen、檢查文件指針、存取數據、fclose,這幾乎成了一種魔咒。比如main函數中,建立文件訪問上下文的代碼占去近一半的代碼量。考慮規避這種魔咒,自動管理文件資源,在操作完成后自動關閉。
函數式風格
以上兩個需求又足以讓整個工程推倒重來。需求#1要求抽象出遍歷集合的方法,在迭代的過程中執行各自的循環體處理數據;需求#2則要創建一種上下文,能自動打開文件,在執行訪問操作完成后自動關閉。它們都涉及將代碼塊作為函數參數,在某個時刻調用,這正是函數式風格擅長的領域。
C語言中,函數指針類型的變量可以指向參數類型與返回值類型都兼容的函數。雖然C不允許嵌套地定義函數或定義匿名函數,但確實允許將函數作為值傳遞,例如qsort的比較函數。
自定義遍歷語句
先試著從employee_print和employee_adjust_salary中抽象出迭代過程:
typedef void (*Callback)(Employee);
void foreach(Employee e, Callback fn) {
for (; e; e = e->next) {
fn(e);
}
} 其中Callback是自定義的函數指針類型,能接收一個Employee類型的參數,并且無返回值。
上述過程照搬了兩個函數中相同的代碼,但作為通用的迭代方法,這個實現有一個bug:當fn的調用破壞了e->next的值時(例如調用free),e = e->next的值就變得未知。為了規避這個問題,需引入一個額外的變量:
void foreach(Employee e, Callback fn) {
Employee p;
while (p = e) {
e = e->next;
fn(p);
}
} 在fn破壞節點內容前先獲得next節點的引用,這樣就能避免free這樣具破壞性的過程影響遍歷。使用這個自定義的控制語句(或高階函數)重構employee_free函數,讓它從遍歷的細節中解放:
static void employee_free_node(Employee e) {
if (e != NULL) {
free(e);
}
}
void employee_free(Employee e) {
foreach(e, employee_free_node);
} 由于C不支持定義匿名函數,因此不得不定義一個釋放單個節點的輔助函數。重構employee_adjust_salary與此類似:
static void employee_adjust_salary_node(Employee e) {
if (e->salary < 30000) {
e->salary += 3000;
}
}
void employee_adjust_salary(Employee e) {
foreach(e, employee_adjust_salary_node);
} 文件訪問上下文
但是,重構employee_print的過程遇到了障礙:它需要一個額外的輸出流,造成接口與Callback不兼容。似乎只能再為IO接口額外定義能接收兩個參數的IoCallback接口,但如此一來又不得不實現一套專門處理它的io_foreach,這是無法接受的!其實,利用偏函數技術能很優雅地解決這個問題,可惜C語言不允許定義匿名函數,也不支持閉包,只能感嘆:臣妾做不到啊!由此可見匿名函數與閉包對函數式風格的友好性。
經過深思熟慮,我做出了一個很艱難地決定:使用freopen重定向標準輸入輸出流。這就能使用printf輸出,無需提供額外的文件流。
void employee_print(Employee e) {
for (; e; e = e->next) {
printf("%s %d %d\n", e->name, e->age, e->salary);
}
}
...
ostream = freopen("work.txt", "w", stdout);
...
employee_print(e); 如此,employee_print的接口與Callback也兼容了,可以使用foreach來重構:
static void employee_print_node(Employee e) {
printf("%s %d %d\n", e->name, e->age, e->salary);
}
void employee_print(Employee e) {
foreach(e, employee_print_node);
} 有了以上基礎,創建上下文的方法就呼之欲出了:
void with_open_file(String filename, String mode, Callback fn, Employee e) {
File file = freopen(filename, mode, (mode[0] == 'r'? stdin: stdout));
if (file == NULL) {
fprintf(stderr, "Cannot open %s with %s mode.\n", filename, mode);
exit(EXIT_FAILURE);
}
fn(e);
fclose(file);
} 先把重定文件流到標準輸入或輸出流;執行回調函數;關閉文件流。如此,保存數據到文件的代碼將簡化成一句話:
with_open_file("work.txt", "w", employee_print, e); 對我而言,這樣的代碼很優雅,我迫不及待地希望employee_read也支持這種方式!
但employee_read又是一塊難啃的骨頭:它不僅參數類型與Callback不兼容,連返回值類型也不同。為將返回值重構成void,不得不提供一個額外參數保存返回值,并且類型是Employee*:
static void employee_read_node(File istream, Employee* head) {
Employee e = NULL;
e = *head = (Employee)calloc(1, sizeof(struct _Employee));
if (e != NULL && fscanf(istream, "%s%d%d", e->name, &e->age, &e->salary) != 3) {
employee_free(e);
*head = NULL;
}
}
void employee_read(File istream, Employee* head) {
Employee e = NULL, tail = NULL;
*head = NULL;
while (employee_read_node(istream, &e), e) {
if (*head != NULL) {
tail->next = e;
tail = e;
} else {
*head = tail = e;
}
}
} 看起來這樣就可以使用與employee_print相同的技巧去除istream這個參數了。其實不然,Callback的參數是Employee,不是Employee*,接口依舊不兼容。這也是靜態類型對函數式風格不友好的一個例子,靜態類型在編譯期就確定變量的類型,限制越多則靈活性越差,相應的受眾面也越窄。
當然,C語言也提供了一個替代方案,使用萬能指針void*作為Callback的參數(例如qsort就是這么做的)。但這樣做,要么所有實現都要改成void*,然后在函數里使用強制轉換;要么得忍受編譯器一堆類型不匹配的warning。權衡再三,還是決定讓employee_read犧牲小我,Callback接口繼續使用Employee做參數類型,在employee_read中將參數類型強制轉換成Employee*。
static void employee_read_node(Employee node) {
Employee e = NULL, *head = (Employee*) node;
e = *head = (Employee)calloc(1, sizeof(struct _Employee));
...
}
void employee_read(Employee list) {
Employee e = NULL, *head = (Employee*) list, tail = NULL;
*head = NULL;
while (employee_read_node((Employee)&e), e) {
...
}
} 解決問題
重構后的主函數變得愈加簡潔,沒有啰嗦的文件操作,甚至可以看成描述原始需求的偽代碼。
int main(void) {
Employee e = NULL;
with_open_file("work.txt", "r", employee_read, (Employee)&e);
employee_print(e);
employee_adjust_salary(e);
employee_print(e);
with_open_file("work.txt", "w", employee_print, e);
employee_free(e);
return EXIT_SUCCESS;
} 重構后的完整代碼請參見:[附錄II]。回到需求#1,切換數據結構。從單鏈表切換成數組,結構體需把“struct _Employee *next”替換成“int length”。而用于創建集合的employee_read更是責不旁貸:
void employee_read(Employee list) {
Employee e = NULL;
int size;
scanf("%d", &size);
*((Employee*) list) = e = (Employee)calloc(size, sizeof(struct _Employee));
e->length = size;
foreach(e, employee_read_node);
} 與之前逐個為對象申請不同,現在一次性申請,即簡化了代碼又提高了性能。由于內存申請被轉移出,employee_read_node也得到極大的簡化:
void employee_read_node(Employee e) {
scanf("%s%d%d", e->name, &e->age, &e->salary);
} 因為空間已提前申請好,因此無需傳入指針。伴隨空間申請方式的改變,空間釋放的方式也要相應調整:
void employee_free(Employee e) {
free(e);
} 不再需要輔助函數employee_free_node。接著是遍歷:
void foreach(Employee e, Callback fn) {
int i, length = e->length;
for (i = 0; i < length; i++) {
fn(e++);
}
} 同樣需要額外的變量length保存最初長度的信息。最后,還有一個可能意想不到的改動——employee_print。前文提過,“輸出”是“輸入”的逆操作,它的職責除了展示和保存數據,還要保持格式與輸入兼容,即輸出的數據還能再次被輸入處理。因此需要在開頭輸出行數:
void employee_print(Employee e) {
printf("%d\n", e->length);
foreach(e, employee_print_node);
} 修改后的代碼不僅實現了需求,而且變得愈加簡潔!重點是無需修改業務處理的代碼,因此業務邏輯也繁雜,函數式風格的優勢越明顯。完整代碼請參見:[附錄III]。
輪到你了!
客戶對這次重構非常滿意!這一回,他們希望foreach能改成并行,即每個循環體都在獨立的線程中執行,那效率又會得到飛躍。
現在輪到你了,你會如何實現客戶的需求?
附錄I:面向對象風格代碼
#include <stdlib.h>
#include <stdio.h>
typedef char String[32];
typedef FILE* File;
typedef struct _Employee {
String name;
int age;
int salary;
struct _Employee *next;
} *Employee;
/* Destructor */
void employee_free(Employee e) {
Employee p;
while (p = e) {
e = e->next;
free(p);
}
}
/* Input */
static Employee employee_read_node(File istream) {
Employee e = (Employee)calloc(1, sizeof(struct _Employee));
if (e != NULL && fscanf(istream, "%s%d%d", e->name, &e->age, &e->salary) != 3) {
employee_free(e);
e = NULL;
}
return e;
}
Employee employee_read(File istream) {
Employee e = NULL, head = NULL, tail = NULL;
while (e = employee_read_node(istream)) {
if (head != NULL) {
tail->next = e;
tail = e;
} else {
head = tail = e;
}
}
return head;
}
/* Output */
void employee_print(File ostream, Employee e) {
for (; e; e = e->next) {
fprintf(ostream, "%s %d %d\n", e->name, e->age, e->salary);
}
}
/* Business Logic */
void employee_adjust_salary(Employee e) {
for (; e; e = e->next) {
if (e->salary < 30000) {
e->salary += 3000;
}
}
}
int main(void) {
File istream, ostream;
Employee e = NULL;
istream = fopen("work.txt", "r");
if (istream == NULL) {
fprintf(stderr, "Cannot open work.txt with r mode.\n");
exit(EXIT_FAILURE);
}
e = employee_read(istream);
fclose(istream);
employee_print(stdout, e);
employee_adjust_salary(e);
employee_print(stdout, e);
ostream = fopen("work.txt", "w");
if (ostream == NULL) {
fprintf(stderr, "Cannot open work.txt with w mode.\n");
exit(EXIT_FAILURE);
}
employee_print(ostream, e);
fclose(ostream);
employee_free(e);
return EXIT_SUCCESS;
} 附錄II:函數式風格代碼
#include <stdlib.h>
#include <stdio.h>
typedef char String[32];
typedef FILE* File;
typedef struct _Employee {
String name;
int age;
int salary;
struct _Employee *next;
} *Employee;
typedef void (*Callback)(Employee);
/* High Order Functions */
void foreach(Employee e, Callback fn) {
Employee p;
while (p = e) {
e = e->next; /* Avoid *next be changed in fn */
fn(p);
}
}
void with_open_file(String filename, String mode, Callback fn, Employee e) {
File file = freopen(filename, mode, (mode[0] == 'r'? stdin: stdout));
if (file == NULL) {
fprintf(stderr, "Cannot open %s with %s mode.\n", filename, mode);
exit(EXIT_FAILURE);
}
fn(e);
fclose(file);
}
/* Destructor */
static void employee_free_node(Employee e) {
if (e != NULL) {
free(e);
}
}
void employee_free(Employee e) {
foreach(e, employee_free_node);
}
/* Input */
static void employee_read_node(Employee node) {
Employee e = NULL, *head = (Employee*) node;
e = *head = (Employee)calloc(1, sizeof(struct _Employee));
if (e != NULL && scanf("%s%d%d", e->name, &e->age, &e->salary) != 3) {
employee_free(e);
*head = NULL;
}
}
void employee_read(Employee list) {
Employee e = NULL, *head = (Employee*) list, tail = NULL;
*head = NULL;
while (employee_read_node((Employee)&e), e) {
if (*head != NULL) {
tail->next = e;
tail = e;
} else {
*head = tail = e;
}
}
}
/* Output */
static void employee_print_node(Employee e) {
printf("%s %d %d\n", e->name, e->age, e->salary);
}
void employee_print(Employee e) {
foreach(e, employee_print_node);
}
/* Business Logic */
static void employee_adjust_salary_node(Employee e) {
if (e->salary < 30000) {
e->salary += 3000;
}
}
void employee_adjust_salary(Employee e) {
foreach(e, employee_adjust_salary_node);
}
int main(void) {
Employee e = NULL;
with_open_file("work.txt", "r", employee_read, (Employee)&e);
employee_print(e);
employee_adjust_salary(e);
employee_print(e);
with_open_file("work.txt", "w", employee_print, e);
employee_free(e);
return EXIT_SUCCESS;
} 附錄III:改造成用數組的代碼
#include <stdlib.h>
#include <stdio.h>
typedef char String[32];
typedef FILE* File;
typedef struct _Employee {
String name;
int age;
int salary;
int length;
} *Employee;
typedef void (*Callback)(Employee);
/* High Order Functions */
void foreach(Employee e, Callback fn) {
int i, length = e->length;
for (i = 0; i < length; i++) {
fn(e++);
}
}
void with_open_file(String filename, String mode, Callback fn, Employee e) {
File file = freopen(filename, mode, (mode[0] == 'r'? stdin: stdout));
if (file == NULL) {
fprintf(stderr, "Cannot open %s with %s mode.\n", filename, mode);
exit(EXIT_FAILURE);
}
fn(e);
fclose(file);
}
/* Destructor */
void employee_free(Employee e) {
free(e);
}
/* Input */
void employee_read_node(Employee e) {
scanf("%s%d%d", e->name, &e->age, &e->salary);
}
void employee_read(Employee list) {
Employee e = NULL;
int size;
scanf("%d", &size);
*((Employee*) list) = e = (Employee)calloc(size, sizeof(struct _Employee));
e->length = size;
foreach(e, employee_read_node);
}
/* Output */
void employee_print_node(Employee e) {
printf("%s %d %d\n", e->name, e->age, e->salary);
}
void employee_print(Employee e) {
printf("%d\n", e->length);
foreach(e, employee_print_node);
}
/* Business Logic */
void employee_adjust_salary_node(Employee e) {
if (e->salary < 30000) {
e->salary += 3000;
}
}
void employee_adjust_salary(Employee e) {
foreach(e, employee_adjust_salary_node);
}
int main(void) {
Employee e = NULL;
with_open_file("work.array", "r", employee_read, (Employee)&e);
employee_print(e);
employee_adjust_salary(e);
employee_print(e);
with_open_file("work.array", "w", employee_print, e);
employee_free(e);
return EXIT_SUCCESS;
} 附錄IV:Common Lisp的解決方案
從函數式風格重構的過程能體會到,如果C語言能支持動態類型,那就不必在employee_read中做強制轉換;如果C語言支持匿名函數,亦不用寫這么多小函數;如果C語言除了能讀入整型、字符串等基礎類型,還能只能讀入數組、結構體等復合類型,就無需employee_read和employee_print等輸入輸出函數……
許多對函數式風格支持更好的編程語言(如Python、Ruby、Lisp等)已經讓這些“如果”變成現實!看看Common Lisp的解決方案:
(defparameter e (with-open-file (f #P"work.lisp") (read f)))
(print e)
(dolist (p e)
(if (< (third p) 30000)
(incf (third p) 3000)))
(print e)
(with-open-file (f #P"work.lisp" :direction :output) (print e f)) 嘗試用你自己熟悉的編程語言解決這個問題,并評估它的可維護性。
來自:http://my.oschina.net/redraiment/blog/188571