你試過這樣寫C程序嗎?

jopen 12年前發布 | 14K 次閱讀 C語言 C/C++開發

本文將采用C語言解決一個問題,圍繞這個問題不斷地變化需求、重構代碼,分別展示兩種風格如何從不同的側面提高代碼的可維護性。

摘要

面向對象風格和函數式編程風格是編寫代碼的兩種風格,面向對象風格早為大眾所認知,函數式風格也漸漸受到大家的關注。網上為其布道的文章不少,有人贊揚有人不屑,但鮮有展示一個完整例子的。例如很多人對函數式風格的印象只是“有人說它很好,但不清楚到底好在哪兒,更不知如何在實際的項目中獲益”。

本文將采用C語言解決一個問題,圍繞這個問題不斷地變化需求、重構代碼,分別展示兩種風格如何從不同的側面提高代碼的可維護性。如果你沒有耐心讀完這篇長文章,可以參見:[附錄II]直接看代碼,但這篇文章會向你解釋為什么代碼會寫成這樣,以及寫成這樣的好處。

注:本文純屬個人觀點,如有雷同,非常榮幸!

關鍵字:C語言; 結構化編程; 面向對象編程; 函數式編程

什么是函數式風格?

面向對象風格大家都耳熟能詳,而提到函數式風格,腦海中或多或少會閃過一些耳熟能詳的名詞:無副作用、無狀態、易于并行編程,甚至是Lisp那扭曲的前綴表達式。追根溯源,函數式風格源自λ演算:函數能作為值傳遞給其他函數或由其他函數返回。其中“函數”是一種抽象的概念,可以理解成代碼塊,在C語言里叫函數或過程,在Java中叫成員方法……因此,函數式風格的本質是函數作為“第一等公民”。在我看來,諸如閉包、匿名函數等特性僅是添頭,例如Emacs Lisp最初不支持閉包,但不影響它是一門支持函數式風格的編程語言。

有些人會把函數式風格與面向對象風格對立起來,但在我看來這兩種風格都是為了提高代碼的可維護性,可以相輔相成:

  • 函數式風格重點是增強類型系統:一些編程語言提供的基礎數據類型僅有數值型和字符串型,函數式風格要求函數也是基礎數據類型,即代碼也是一種數據;
  • 面向對象風格側重代碼的組織形式:要求把數據和操作這些數據的函數組織在同一個類中,提高內聚;對象之間通過調用開放的接口通訊,降低耦合。

代碼即數據的作用?

使用不支持函數式風格的編程語言開發,將迫使我們永遠在語言恰好提供的基礎功能上工作。例如迭代只能使用for、while等關鍵字;讀寫文件每次都要寫fopen、fclose;并行加鎖也少不了lock和unlock。面對這些大同小異的冗余代碼總會很無奈:如果XX語言能提供XX特性該多好啊!

代碼即數據讓這一切成為可能,它允許你自定義控制語句。如果語言不支持某個期望的特性,那就自己動手加一個!后文將展示如何自定義控制語句,以及它如何提高代碼的可維護性。

為什么選C語言?

函數若要作為“第一等公民”,至少需要滿足以下四條特權:

  1. 可以用變量命名;
  2. 可以提供給過程作為參數;
  3. 可以由過程作為結果返回;
  4. 可以包含在數據結構中。

對照之下會驚訝地發現,C這門看似離函數式風格最遠的編程語言居然也符合上述條件;此外,相比其他對函數式風格支持更好的語言(如Lisp、Haskell等),至少C的語法不那么古怪;何況熟悉C語系(如Java、C#等)語法的同學也更多,方便大家用自己熟悉的語言實踐。

問題描述

作為貫穿全文的主線,這有一個問題需要你開發一個C程序來完成任務:有一個存有職員信息(姓名、年齡、工資)的文件“work.txt”,內容如下:

William 35 25000
Kishore 41 35000
Wallace 37 20000
Bruce 39 15000
  1. 要求從文件中讀取這些信息,并輸出到屏幕上;
  2. 為所有工資小于三萬的員工漲3000元;
  3. 在屏幕上輸出薪資調整后的結果;
  4. 把調整后的結果保存到原始文件。

即運行的結果是屏幕上要有八行輸出,“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;

開放接口

根據需求,職員對象至少提供從文件中讀取信息、輸出到屏幕、保存到文件、調整薪資四項功能。其中輸出到屏幕和保存到文件可以合并成輸出到輸出流中,因此它將開放以下四個接口:

  1. employee_read:批量從輸入流中讀取職員信息并返回
  2. employee_free:批量釋放動態申請的空間
  3. employee_print:批量輸出職員信息到輸出流
  4. 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;
}

重構后的代碼與需求的描述更接近,雖然代碼量膨脹了三倍,但能解決前文的問題:

  1. 文件指針做空指針檢查
  2. 單鏈表容量能自動擴展
  3. 字段類型或數目變化時僅修改輸入和輸出兩處
  4. 每項業務邏輯為獨立的函數,易擴展且組合靈活

完整的代碼請參見:[附錄I]

歡迎變化再次光臨

經過重構的代碼可維護性更好,因為每個函數的職責是單一的:

  1. employee_read_node:應對輸入源的變化,如列的順序改變;
  2. employee_read:應對集合結構的變化,如單向鏈表改成雙向鏈表;
  3. employee_print:應對輸出格式的變化,如輸出成CSV結構;
  4. employee_adjust_salary:應對業務邏輯的變化,如調薪幅度增大。

不過,代碼仍有不少重復之處,“重復”是維護性的大敵。想想你會如何應付下面這些問題?

數據源升級

上游系統在升級后,work.txt的第一行提供了行數:

4
William 35 25000
Kishore 41 35000
Wallace 37 20000
Bruce 39 15000

而且,原系統頻繁地申請空間也影響到性能。經過權衡,決定用數組取代單鏈表,這樣只需一次性申請足夠大的空間。

憑借面向對象風格的優勢,對接口的實現的修改不會影響接口的使用,因此main函數無需任何修改。但對Employee對象而言卻是災難:每個接口的實現都與內部數據結構緊緊地綁在一起。幾乎所有實現里都用forwhile循環遍歷整個鏈表,底層數據結構的變化意味著遍歷方式的變化,即所有接口的實現全部需要重寫!

優雅的訪問文件

但凡涉及訪問文件的代碼,都需要fopen、檢查文件指針、存取數據、fclose,這幾乎成了一種魔咒。比如main函數中,建立文件訪問上下文的代碼占去近一半的代碼量。考慮規避這種魔咒,自動管理文件資源,在操作完成后自動關閉。

函數式風格

以上兩個需求又足以讓整個工程推倒重來。需求#1要求抽象出遍歷集合的方法,在迭代的過程中執行各自的循環體處理數據;需求#2則要創建一種上下文,能自動打開文件,在執行訪問操作完成后自動關閉。它們都涉及將代碼塊作為函數參數,在某個時刻調用,這正是函數式風格擅長的領域。

C語言中,函數指針類型的變量可以指向參數類型與返回值類型都兼容的函數。雖然C不允許嵌套地定義函數或定義匿名函數,但確實允許將函數作為值傳遞,例如qsort的比較函數。

自定義遍歷語句

先試著從employee_printemployee_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_reademployee_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

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!