日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程资源 > 编程问答 >内容正文

编程问答

从重复到重用

發(fā)布時(shí)間:2024/8/23 编程问答 38 豆豆
生活随笔 收集整理的這篇文章主要介紹了 从重复到重用 小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

簡(jiǎn)介:?開(kāi)發(fā)技術(shù)的發(fā)展,從第一次提出“函數(shù)/子程序”,實(shí)現(xiàn)代碼級(jí)重用;到面向?qū)ο蟮摹邦?lèi)”,重用數(shù)據(jù)結(jié)構(gòu)與算法;再到“動(dòng)態(tài)鏈接庫(kù)”、“控件”等重用模塊;到如今流行的云計(jì)算、微服務(wù)可重用整個(gè)系統(tǒng)。技術(shù)發(fā)展雖然日新月異,但本質(zhì)都是重用,只是粒度不同。所以寫(xiě)代碼的動(dòng)機(jī)都應(yīng)是把重復(fù)的工作變成可重用的方案,其中重復(fù)的工作包括業(yè)務(wù)上重復(fù)的場(chǎng)景、技術(shù)上重復(fù)的代碼等。合格的系統(tǒng)可以簡(jiǎn)化當(dāng)下重復(fù)的工作;優(yōu)秀的系統(tǒng)還能預(yù)見(jiàn)未來(lái)重復(fù)的工作。

作者 | 技師
來(lái)源 | 阿里技術(shù)公眾號(hào)

溫馨提示:本文較長(zhǎng),同學(xué)們可收藏后再看 :)

一 前言

開(kāi)發(fā)技術(shù)的發(fā)展,從第一次提出“函數(shù)/子程序”,實(shí)現(xiàn)代碼級(jí)重用;到面向?qū)ο蟮摹邦?lèi)”,重用數(shù)據(jù)結(jié)構(gòu)與算法;再到“動(dòng)態(tài)鏈接庫(kù)”、“控件”等重用模塊;到如今流行的云計(jì)算、微服務(wù)可重用整個(gè)系統(tǒng)。技術(shù)發(fā)展雖然日新月異,但本質(zhì)都是重用,只是粒度不同。所以寫(xiě)代碼的動(dòng)機(jī)都應(yīng)是把重復(fù)的工作變成可重用的方案,其中重復(fù)的工作包括業(yè)務(wù)上重復(fù)的場(chǎng)景、技術(shù)上重復(fù)的代碼等。合格的系統(tǒng)可以簡(jiǎn)化當(dāng)下重復(fù)的工作;優(yōu)秀的系統(tǒng)還能預(yù)見(jiàn)未來(lái)重復(fù)的工作。

本文不談框架、不談架構(gòu),就談寫(xiě)代碼的那些事兒!后文始終圍繞一個(gè)問(wèn)題的解決方案,不斷發(fā)現(xiàn)其中“重復(fù)”的代碼,并提煉出“可重用”的抽象,持續(xù)“重構(gòu)”。希望通過(guò)這個(gè)過(guò)程和大家分享一些發(fā)現(xiàn)重復(fù)代碼和提煉可重用抽象的方法。

二 問(wèn)題

作為貫穿全文的主線,這有一個(gè)任務(wù)需要開(kāi)發(fā)一個(gè)程序來(lái)完成:有一份存有職員信息(姓名、年齡、工資)的文件“work.txt”,內(nèi)容如下:

William 35 25000 Kishore 41 35000 Wallace 37 30000 Bruce 39 29999
  • 要求從文件(work.txt)中讀取員工薪酬,并輸出到屏幕上。
  • 為所有工資小于三萬(wàn)的員工漲 3000 元。
  • 在屏幕上輸出薪資調(diào)整后的結(jié)果。
  • 把調(diào)整后的結(jié)果保存到原始文件。
  • 即運(yùn)行的結(jié)果是屏幕上要有八行輸出,“work.txt”的內(nèi)容將變成:

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

    三 測(cè)試

    在明確了需求之后,第一步要做的是寫(xiě)測(cè)試代碼,而不是寫(xiě)功能代碼。《重構(gòu)》一書(shū)中對(duì)重構(gòu)的定義是:“在不改變代碼外在行為的前提下,對(duì)代碼做出修改,以改進(jìn)程序的內(nèi)部結(jié)構(gòu)?!逼渲忻鞔_指出“代碼外在行為”是不改變的!在不斷迭代重構(gòu)時(shí),“保證每次重構(gòu)的行為不變”也是一項(xiàng)重復(fù)的工作,所以測(cè)試先行不僅能盡早地校驗(yàn)對(duì)需求理解的正確性、還能避免重復(fù)測(cè)試。本文通過(guò)一段 Shell 腳本完成以下工作:

    • 初始化work.txt文件。
    • 檢查標(biāo)準(zhǔn)輸出的內(nèi)容與期望的結(jié)果是否一致。
    • 檢查修改后work.txt文件的內(nèi)容是否與期望一致。
    • 清理現(xiàn)場(chǎng)。

    將上述代碼保存成check.sh,待測(cè)試的源文件名作為參數(shù)。如果程序通過(guò),會(huì)顯示“PASS”,否則會(huì)輸出不同的行以及“FAIL”。

    四 可維護(hù)代碼

    第一版:It works

    每位熟練的程序員都能快速地給出自己的實(shí)現(xiàn)。本文示例代碼使用ANSI C99編寫(xiě),Mac下用gcc能正常編譯運(yùn)行,其他環(huán)境未測(cè)試。選擇C語(yǔ)言是因?yàn)橹髁骶幊陶Z(yǔ)言都或多或少借鑒它的語(yǔ)法,同時(shí)它的語(yǔ)法特性也足夠用于演示。

    問(wèn)題很簡(jiǎn)單,簡(jiǎn)單到把所有代碼都塞到 main 函數(shù)里也不覺(jué)得長(zhǎng):

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

    其中第一個(gè)循環(huán)從work.txt中讀取4行數(shù)據(jù),并把信息輸出到屏幕(需求#1);同時(shí)為薪資小于三萬(wàn)的職員增加三千元(需求#2);第二個(gè)循環(huán)遍歷所有數(shù)據(jù),把調(diào)整后的結(jié)果輸出屏幕(需求#3),并保存結(jié)果到 work.txt(需求#4)。

    試試將上述代碼保存成1.c并執(zhí)行 ./check.sh 1.c,屏幕上會(huì)輸出“PASS”,即通過(guò)測(cè)試。

    第二版:清晰的代碼,重構(gòu)的基礎(chǔ)

    第一版代碼解決了問(wèn)題,讓原來(lái)重復(fù)的調(diào)薪工作變成簡(jiǎn)便的、可反復(fù)使用的程序。如果它是C語(yǔ)言課堂作業(yè)的答案,看起來(lái)還不錯(cuò)——至少縮進(jìn)一致,也沒(méi)混用空格和制表符;但從軟件工程的角度來(lái)講,它簡(jiǎn)直糟糕透了,因?yàn)闆](méi)有清晰的表達(dá)意圖:

  • 魔法常量 4 重復(fù)出現(xiàn),后續(xù)負(fù)責(zé)維護(hù)的程序員無(wú)法判斷它們是碰巧相等還是有其他原因必需相等。
  • 文件名work.txt重復(fù)出現(xiàn)。
  • 重復(fù)且不清晰的文件指針類(lèi)型定義,容易忽略 ostream 前面的 *。
  • e 和 i 變量命名不顧名思義。
  • 變量的定義與使用離得太遠(yuǎn)。
  • 無(wú)異常處理,文件可能不可讀。
  • 借喬老爺子的話(huà)說(shuō):“看不見(jiàn)的地方也要用心做好”——這些代碼的問(wèn)題用戶(hù)雖然看不見(jiàn)也不在乎,但也要用心做好——已有幾處顯眼的地方出現(xiàn)重復(fù)。不過(guò),在代碼變得清晰之前,不應(yīng)急著動(dòng)手去重構(gòu),因?yàn)榍逦拇a更容易找出重復(fù)!針對(duì)上述意圖不明的問(wèn)題,準(zhǔn)備對(duì)代碼做以下調(diào)整:

  • 確認(rèn)數(shù)字 4 在三處的意義都是員工記錄數(shù),因此定義共享常量 #define RECORD_COUNT 4。
  • 常量"work.txt"和 4 不同,內(nèi)容雖然相同但意義不同:一個(gè)作輸入,一個(gè)作輸出。如果也只簡(jiǎn)單的定義一個(gè)常量 FILE_NAME 共用,后續(xù)兩者獨(dú)立變化時(shí),工作量并沒(méi)減少。所以去除重復(fù)代碼時(shí),切忌只看表面相同,背后意義相同的才是真正的相同,否則就像給所有常量 1 定義 ONE 別名一樣沒(méi)有意義。所以需要定義三個(gè)常量 FILE_NAME、INPUT_FILE_NAME 和 OUTPUT_FILE_NAME。
  • 用自定義的文件類(lèi)型 typedef FILE?File; 替代 FILE,可避免遺漏指針。
  • 變量 e 是所有職員信息,把變量名改成 employees。
  • 變量 i 是迭代過(guò)程的下標(biāo),把變量名改成 index。
  • 將 index 變量定義放到 for 語(yǔ)句中。
  • 將 File 變量定義從頂部挪到各自使用之前的位置。
  • 對(duì)文件指針做異常檢查,當(dāng)文件無(wú)法打開(kāi)時(shí)輸出錯(cuò)誤信息并提前終止程序。
  • 程序退出時(shí)用 < stdlib.h> 中更語(yǔ)義化的 EXIT_FAILURE,正常退出時(shí)用 EXIT_SUCCESS。
  • 你可能會(huì)問(wèn):“數(shù)字30000和3000也是魔法數(shù)字,為什么不調(diào)整?”原因是此時(shí)它們即不重復(fù)也無(wú)歧義。整理后的完整代碼如下:

    #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 (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并執(zhí)行 ./check.sh 2.c,得到期望的輸出PASS,證明本次重構(gòu)沒(méi)有改變程序的行為。

    第三版:代碼映射需求

    經(jīng)過(guò)第二版的優(yōu)化,單行代碼的意圖已比較清晰,但還存在一些過(guò)早優(yōu)化導(dǎo)致代碼塊的含義不清晰。

    例如第一個(gè)循環(huán)中耦合了“輸出到屏幕”和“調(diào)整薪資”兩個(gè)功能,好處是可減少一次循環(huán),性能也許有些提升;但這兩個(gè)功能在需求中是相互獨(dú)立的,后續(xù)獨(dú)立變化的可能性更大。假設(shè)新需求是第一步輸出到屏幕后,要求用戶(hù)輸入命令,再?zèng)Q定是否要進(jìn)行薪資調(diào)整工作。此時(shí),對(duì)需求方而言只新增一個(gè)步驟,只有一個(gè)改動(dòng);但到了代碼層面,卻不是新增一個(gè)步驟對(duì)應(yīng)新增一塊代碼,還會(huì)牽涉理論上不相關(guān)的代碼塊;負(fù)責(zé)維護(hù)的程序員在不了解背景時(shí),就不確定這兩段代碼放在一起有沒(méi)有歷史原因,也就不敢輕易將它們拆開(kāi)。當(dāng)系統(tǒng)規(guī)模越大,這種與需求不是一一對(duì)應(yīng)的代碼就越讓維護(hù)人員手足無(wú)措!

    回想日常開(kāi)發(fā),需求改動(dòng)很小而代碼卻牽一發(fā)動(dòng)全身,根源往往就是過(guò)早優(yōu)化?!皟?yōu)化”和“通用”往往是對(duì)立的,優(yōu)化的越徹底就與業(yè)務(wù)場(chǎng)景結(jié)合越緊密,通用性也越差。比如某個(gè)系統(tǒng)會(huì)在緩沖隊(duì)列中對(duì)收到的消息進(jìn)行排序,上線運(yùn)行后發(fā)現(xiàn)因?yàn)楫a(chǎn)品設(shè)計(jì)等外部原因,消息可能天然接近排好序,于是用插入排序代替快速排序等更通用的排序算法,這就是一次不通用的優(yōu)化:它讓系統(tǒng)的性能更好,但系統(tǒng)的適用面更窄。過(guò)早的優(yōu)化就是過(guò)早的給系統(tǒng)能力設(shè)置天花板。

    理想情況是代碼塊與需求功能點(diǎn)一一對(duì)應(yīng),例如當(dāng)前需求有4個(gè)功能點(diǎn),得有4個(gè)獨(dú)立的代碼塊與之對(duì)應(yīng)。這樣做的好處是:當(dāng)需求發(fā)生變化時(shí),代碼的修改也相對(duì)集中。因此,基于第二版本代碼準(zhǔn)備做以下調(diào)整:

    • 拆分耦合的循環(huán)代碼塊,每段代碼塊都只完成一件事情。
    • 用注釋明確標(biāo)出每段代碼塊對(duì)應(yīng)的需求。

    整理后的完整代碼如下:

    #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 (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. 調(diào)整薪資 */for (int index = 0; index < RECORD_COUNT; index++) {if (employees[index].salary < 30000) {employees[index].salary += 3000;}}/* 3. 輸出調(diào)整后的結(jié)果 */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并執(zhí)行 ./check.sh 3.c,確保程序的行為沒(méi)有改變。

    五 面向?qū)ο箫L(fēng)格

    第四版:職員對(duì)象抽象

    經(jīng)過(guò)兩輪改造,代碼結(jié)構(gòu)已足夠清晰;現(xiàn)在可以開(kāi)始重構(gòu),來(lái)梳理代碼層次。

    最顯眼的就是格式化輸出職員信息:除了輸出流不同,格式、內(nèi)容完全相同,四條需求中出現(xiàn)了三次。一般遇到相同/相似代碼時(shí),可以抽象出一個(gè)函數(shù):相同的部分寫(xiě)在函數(shù)體中,不同的部分作為參數(shù)傳入。此處,能抽象出一個(gè)以結(jié)構(gòu)體數(shù)據(jù)和文件流為入?yún)⒌暮瘮?shù),但目前這個(gè)結(jié)構(gòu)體還是匿名的,無(wú)法作為函數(shù)的參數(shù),所以第一步得先給匿名的職員結(jié)構(gòu)體取一個(gè)合適的類(lèi)型名稱(chēng):

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

    然后抽象公共函數(shù)用于格式化輸出 Employee 到 File,這其中還耦合了兩個(gè)功能:

  • Employee 序列化成字符串。
  • 序列化結(jié)果輸出到指定文件流。
  • 因?yàn)闀簾o(wú)獨(dú)立使用某項(xiàng)功能的場(chǎng)景,目前無(wú)需進(jìn)一步拆分:

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

    Employee 結(jié)構(gòu)體 + employee_print 函數(shù)很容易聯(lián)想到面向?qū)ο蟮摹邦?lèi)”。面向?qū)ο蟮谋举|(zhì)是由一組功能獨(dú)立的對(duì)象組成系統(tǒng),對(duì)象之間通過(guò)發(fā)消息協(xié)作完成任務(wù),不見(jiàn)得非要有 class 關(guān)鍵字,繼承、封裝、多態(tài)等語(yǔ)法糖。

    • 對(duì)象的“功能獨(dú)立”,即高內(nèi)聚,要求數(shù)據(jù)和操作數(shù)據(jù)的相關(guān)方法放在一起,大多數(shù)支持面向?qū)ο蟮木幊陶Z(yǔ)言都提供了 class 關(guān)鍵字,在語(yǔ)言層面強(qiáng)制捆綁,C語(yǔ)言并沒(méi)有這樣的語(yǔ)法,但可以制定編碼規(guī)范,讓數(shù)據(jù)結(jié)構(gòu)與函數(shù)在物理上挨得更近。
    • “給對(duì)象發(fā)消息”,不同的編程語(yǔ)言里表現(xiàn)形式各不相同,例如在Java中 foo.baz() 就是向 foo 對(duì)象發(fā)送baz消息,C++中等價(jià)的語(yǔ)法是 foo->baz(),Smalltalk中是 foo baz,C語(yǔ)言則是 baz(foo)。

    綜上所述,雖然C語(yǔ)言通常被認(rèn)為不是面向?qū)ο蟮恼Z(yǔ)言,其實(shí)它也能支持面向?qū)ο箫L(fēng)格。沿上述思路,可以抽象出職員對(duì)象的四個(gè)方法:

    • employee_read:構(gòu)造函數(shù),分配空間、輸入并反序列化,類(lèi)似于Java的 new。
    • employee_free:析構(gòu)函數(shù),釋放空間,即純手工的 GC。
    • employee_print:序列化并輸出。
    • employee_adjust_salary:調(diào)整職員薪資,唯一的業(yè)務(wù)邏輯。

    有了職員對(duì)象,程序不再只有一個(gè) main 函數(shù)。假設(shè)把 main 函數(shù)看作應(yīng)用層,其他函數(shù)看作類(lèi)庫(kù)、框架或中間件,這樣程序有了層級(jí),層間僅通過(guò)開(kāi)放的接口通訊,即對(duì)象的封裝性。

    在Java中有 public、protected、default 和 private 四種可見(jiàn)性修飾符,C語(yǔ)言的函數(shù)默認(rèn)是公開(kāi)的,加上 static 關(guān)鍵字后只在當(dāng)前文件可見(jiàn)。為避免應(yīng)用層向?qū)ο箅S意發(fā)送消息,約定只有在應(yīng)用層用到的函數(shù)才公開(kāi),所以額外定義了 public 和 private 兩個(gè)修飾符,目前職員對(duì)象的四個(gè)方法都是公開(kāi)的。

    重構(gòu)之后的完整代碼如下:

    #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;/* 職員對(duì)象 */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;} }/* 應(yīng)用層 */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. 調(diào)整薪資 */for (int index = 0; index < RECORD_COUNT; index++) {employee_adjust_salary(employees[index]);}/* 3. 輸出調(diào)整后的結(jié)果 */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,照例執(zhí)行 ./check.sh 4.c,檢測(cè)是否有改變程序行為。

    第五版:容器對(duì)象抽象

    之前的重構(gòu),去除了詞法和句法上的重復(fù),就像一篇文章里的單詞和語(yǔ)句,接著可以看段落有沒(méi)有重復(fù),即代碼塊。

    與 employee_print 類(lèi)似,三段循環(huán)輸出職員信息代碼也是明顯的重復(fù),可以抽象出 employees_print,同時(shí)也抽象出另一個(gè)對(duì)象——職員列表—— Employees。參考職員對(duì)象,可以抽象出四個(gè)與之對(duì)應(yīng)的函數(shù):

    • employees_read:構(gòu)造函數(shù),分配列表空間,并依次創(chuàng)建職員對(duì)象。
    • employees_free:析構(gòu)函數(shù),釋放列表空間,以及職員對(duì)象的空間。
    • employees_print:序列化并輸出列表中每一位職員信息。
    • employees_adjust_salary:調(diào)整所有符合要求職員的薪資。

    此時(shí),main 函數(shù)只需調(diào)用職員列表對(duì)象的方法,不再直接調(diào)用職員對(duì)象的方法,所以后者可見(jiàn)性從 public 降為 private。

    重構(gòu)之后的完整代碼如下:

    #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;/* 職員對(duì)象 */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;} }/* 職員列表對(duì)象 */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); }/* 應(yīng)用層 */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. 調(diào)整薪資 */employees_adjust_salary(employees);/* 3. 輸出調(diào)整后的結(jié)果 */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; }

    不要忘記運(yùn)行 ./check.sh 作回歸測(cè)試。

    第六版:輸入輸出抽象

    此時(shí)的 main 函數(shù)已經(jīng)比較清爽,剩下一處明顯的重復(fù):打開(kāi)文件并檢查文件是否正常打開(kāi)。這屬于文件相關(guān)的操作,可以抽象出一個(gè) 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; }

    接著可以繼續(xù)抽象職員列表對(duì)象的輸入和輸出方法:

    • employees_input:從文件中獲取數(shù)據(jù)并創(chuàng)建職員列表對(duì)象。
    • employees_output:將職員列表對(duì)象的內(nèi)容輸出到文件。

    重構(gòu)后 employees_read 不再被 main 訪問(wèn),所以改成 private。重構(gòu)后的完整代碼如下:

    #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; typedef char* String;/* 職員對(duì)象 */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;} }/* 職員列表對(duì)象 */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); }/* 應(yīng)用層 */int main(void) {Employees employees = employees_input(INPUT_FILE_NAME); /* 從文件讀入 */employees_print(employees, stdout); /* 1. 輸出到屏幕 */employees_adjust_salary(employees); /* 2. 調(diào)整薪資 */employees_print(employees, stdout);/* 3. 輸出調(diào)整后的結(jié)果 */employees_output(employees, OUTPUT_FILE_NAME);/* 4. 保存到文件 */employees_free(employees); /* 釋放資源 */return EXIT_SUCCESS; }

    別忘記執(zhí)行 ./check.sh。

    六 函數(shù)式編程

    第七版:容器迭代重用

    現(xiàn)在,main 里只用到了職員列表相關(guān)的函數(shù),且代碼和需求幾乎一一對(duì)應(yīng)。這些函數(shù)可以看成職員管理領(lǐng)域的DSL,領(lǐng)域特定語(yǔ)言是業(yè)務(wù)和技術(shù)雙方的共識(shí),理論上需求不變,基于DSL開(kāi)發(fā)的業(yè)務(wù)代碼也不變。之前所有的改動(dòng)僅要求 main 行為一致,后續(xù)的重構(gòu)還要盡量保證 main 自身也無(wú)任何變化,即API向后兼容。

    回到繼續(xù)挖掘代碼中重復(fù)的問(wèn)題上,其中職員列表方法中幾乎都有一個(gè) for 循環(huán):for (int index = 0; index < RECORD_COUNT; index++) { ... },例如調(diào)整薪資和釋放空間兩段代碼:

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

    除了循環(huán)體中分別調(diào)用了 employee_adjust_salary 和 employee_free,其余都一摸一樣,即它們的迭代規(guī)則相同,而循環(huán)體不同。是否有可能自定義一個(gè) for 語(yǔ)句代替這些重復(fù)的迭代?

    在大多數(shù)編程語(yǔ)言中,if、for 等控制語(yǔ)句是一種特殊的存在,開(kāi)發(fā)者通常無(wú)法自定義。這是 if 和 for 在大多數(shù)語(yǔ)言中的樣子:

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

    如果把它們想象成是函數(shù),語(yǔ)法可以改成更熟悉的函數(shù)調(diào)用形式:

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

    和普通函數(shù)調(diào)用相比,唯一不同的是允許花括號(hào)包圍的代碼片段作為參數(shù)。因此,若編程語(yǔ)言允許代碼作為函數(shù)的參數(shù),那就能自定義新的控制語(yǔ)句!這句話(huà)隱含了兩個(gè)語(yǔ)言特性:

  • 代碼是一種數(shù)據(jù)類(lèi)型。
  • 代碼類(lèi)型的數(shù)據(jù)可作為函數(shù)的參數(shù)。
  • 所有編程語(yǔ)言都包含一套類(lèi)型系統(tǒng),它決定數(shù)據(jù)的類(lèi)型,而數(shù)據(jù)的類(lèi)型又決定數(shù)據(jù)的功能。例如,數(shù)值類(lèi)型可以做四則運(yùn)算;字符串類(lèi)型的數(shù)據(jù)可以拼接、查找、替換等;代碼如果也是一種數(shù)據(jù)類(lèi)型,就可以隨時(shí)“執(zhí)行”它。C語(yǔ)言中具備“執(zhí)行”能力的元素就是“函數(shù)”,函數(shù)之于代碼類(lèi)型,猶如 int、double 之于數(shù)值類(lèi)型,都只是C這個(gè)特定編程語(yǔ)言對(duì)特定類(lèi)型的特定實(shí)現(xiàn),換成Visual Basic改叫“過(guò)程”,換成Java又稱(chēng)作“成員方法”。

    至于特性#2,它正是函數(shù)式編程的本質(zhì)!提到函數(shù)式風(fēng)格,腦海中通常會(huì)閃過(guò)一些耳熟能詳?shù)脑~匯:無(wú)副作用、無(wú)狀態(tài)、易于并行編程,甚至是Lisp那扭曲的前綴表達(dá)式。追根溯源,函數(shù)式編程源自λ演算——函數(shù)能作為值傳遞給其他函數(shù)或由其他函數(shù)返回——其本質(zhì)是函數(shù)作為類(lèi)型系統(tǒng)中的“第一等公民”(First-Class),符合以下四項(xiàng)要求:

  • 可以用變量命名。
  • 可以提供給過(guò)程作為參數(shù)。
  • 可以由過(guò)程作為結(jié)果返回。
  • 可以包含在數(shù)據(jù)結(jié)構(gòu)中。
  • 對(duì)照之下會(huì)驚訝地發(fā)現(xiàn),C語(yǔ)言這門(mén)看似與函數(shù)式編程最遠(yuǎn)的上古編程語(yǔ)言,利用函數(shù)指針,居然也完全符合上述條件。觀察 employee_adjust_salary 和 employee_free 兩個(gè)函數(shù),都只有一個(gè) Employee 類(lèi)型的參數(shù)且沒(méi)有返回值,翻譯成C語(yǔ)言就是 typedef void (*EmployeeFn)(Employee),把它作為函數(shù)的參數(shù),就能抽象出:

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

    在函數(shù)式語(yǔ)言中,這類(lèi)將函數(shù)作為參數(shù)或返回值的函數(shù)稱(chēng)為高階函數(shù),C語(yǔ)言里稱(chēng)為控制語(yǔ)句。用這個(gè)自定義的控制語(yǔ)句代替原生的 for 循環(huán),則代碼可以簡(jiǎn)化成:

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

    不過(guò),此時(shí)還只解決了一半問(wèn)題:employees_read 和 employees_print 中依然有重復(fù)的 for 循環(huán),并無(wú)法用 employees_each 簡(jiǎn)化。原因是這些循環(huán)體中函數(shù)調(diào)用的參數(shù)數(shù)目與類(lèi)型和 EmployeeFn 不兼容:

    • employee_read:包含 File 類(lèi)型的參數(shù),返回 Employee 類(lèi)型。
    • employee_print:包含 Employee 和 File 兩類(lèi)參數(shù),無(wú)返回值。
    • EmployeeFn:包含 Employee 類(lèi)型的參數(shù),無(wú)返回值。

    想涵蓋所有場(chǎng)景,最簡(jiǎn)單的方法就是提取一個(gè)參數(shù)與返回結(jié)果的全集——Employee (*EmployeeFn)(Employee, File)——包含 Employee 和 File 兩個(gè)類(lèi)型的參數(shù),且返回 Employee 類(lèi)型的結(jié)果。用新接口重構(gòu) Employee 的四個(gè)方法:

    • 忽略無(wú)用的參數(shù)。
    • 除了employee_free 返回 NULL,其他都返回 Employee 入?yún)ⅰ?/li>

    同時(shí),需要改造 employees_each 去適應(yīng)新接口:加入 File 參數(shù),以及返回處理結(jié)果。在編程的語(yǔ)義中,單純利用副作用的迭代被稱(chēng)為 foreach,而關(guān)注迭代每個(gè)元素的處理結(jié)果則稱(chēng)為 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; }

    重構(gòu)后的完整代碼如下:

    #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; typedef char* String;/* 職員對(duì)象 */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; }/* 職員列表對(duì)象 */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); }/* 應(yīng)用層 */int main(void) {Employees employees = employees_input(INPUT_FILE_NAME); /* 從文件讀入 */employees_print(employees, stdout); /* 1. 輸出到屏幕 */employees_adjust_salary(employees); /* 2. 調(diào)整薪資 */employees_print(employees, stdout);/* 3. 輸出調(diào)整后的結(jié)果 */employees_output(employees, OUTPUT_FILE_NAME);/* 4. 保存到文件 */employees_free(employees); /* 釋放資源 */return EXIT_SUCCESS; }

    這一系列的改造展示了“代碼即數(shù)據(jù)”的一些好處:使用不支持函數(shù)式編程的語(yǔ)言開(kāi)發(fā),將迫使我們永遠(yuǎn)在語(yǔ)言恰好提供的基礎(chǔ)功能上工作;而“代碼即數(shù)據(jù)”讓我們擺脫這樣的束縛,允許自定義控制語(yǔ)句。例如,Java 5引入 foreach 語(yǔ)法糖、Java 7引入 try-with-resource 語(yǔ)法糖,在Java 8之前想要任何新的語(yǔ)言特性只能等Oracle大發(fā)慈悲,Java 8之后想要任何語(yǔ)言特性就可以自給自足!

    經(jīng)過(guò)這么大的改造,切勿忘記測(cè)試!

    第八版:動(dòng)態(tài)作用域與上下文包裝

    上一版本的代碼雖然可以工作,但也暴露出一個(gè)常見(jiàn)問(wèn)題:函數(shù)的參數(shù)不斷膨脹。這個(gè)問(wèn)題在程序的層次不斷增加過(guò)程會(huì)慢慢滋生。例如函數(shù) A 會(huì)調(diào)用 B、B 又調(diào)用 C,假設(shè) C 需要一個(gè)文件對(duì)象,假設(shè) B 中并不創(chuàng)建文件對(duì)象,就得從 A 依次傳遞到 B 再傳遞到 C。函數(shù)調(diào)用的層次越深,數(shù)據(jù)逐層傳遞的問(wèn)題就越嚴(yán)重,上層函數(shù)的入?yún)⒕蜁?huì)爆炸!

    這類(lèi)函數(shù)參數(shù)過(guò)多且逐層傳遞的問(wèn)題,最簡(jiǎn)單的解決方法就是使用全局變量。例如定義一個(gè)全局的文件對(duì)象,指向當(dāng)前輸入/輸出的目標(biāo),這樣就能去除所有的文件對(duì)象入?yún)?。全局變量的弊端是很難判斷它的影響范圍,不加限制地使用全局變量就和無(wú)約束地使用goto一樣,代碼會(huì)迅速變成意大利面條。所以,建議有節(jié)制地使用全局變量:用完之后及時(shí)將值恢復(fù)。例如以下代碼:

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

    其中函數(shù) c 臨時(shí)開(kāi)啟了調(diào)試選項(xiàng),并在退出前恢復(fù)成原始值。一旦忘記恢復(fù),后續(xù)所有調(diào)試信息就都會(huì)輸出,惡夢(mèng)就會(huì)開(kāi)始。為避免這種尷尬問(wèn)題,可以利用上一版本中提到的函數(shù)式編程的方法,將重復(fù)的開(kāi)啟選項(xiàng)、恢復(fù)工作抽象成函數(shù):

    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 這種負(fù)責(zé)資源分配再自動(dòng)回收(或資源修改再自動(dòng)恢復(fù))工作的函數(shù)稱(chēng)為上下文包裝器(wrapper),開(kāi)啟調(diào)試選項(xiàng)是一個(gè)常見(jiàn)的應(yīng)用場(chǎng)景,還可以用于自動(dòng)關(guān)閉打開(kāi)的文件對(duì)象(例如Java 7的try-with-resources)。不過(guò),目前的解決方案在多線程環(huán)境下依然有問(wèn)題,為避免不同的線程之間相互沖突,理想的方案是采用類(lèi)似Java中的 ThreadLocal 包裝所有全局變量,C語(yǔ)言的多線程方案POSIX thread有Thread Specific組件實(shí)現(xiàn)類(lèi)似的線程特有數(shù)據(jù)功能,此處就不展開(kāi)討論。

    綜上所述,我們真正需要的功能似乎是一種代碼的包裝能力:全局變量某個(gè)特定的值只在指定范圍內(nèi)生效(包括范圍內(nèi)代碼調(diào)用的函數(shù)、調(diào)用函數(shù)的調(diào)用等等),類(lèi)似于會(huì)話(huà)級(jí)別的變量。這種功能被裁剪的全局變量在編程語(yǔ)言中稱(chēng)為動(dòng)態(tài)作用域(Dynamic Scope)變量。

    大多數(shù)主流編程語(yǔ)言只支持靜態(tài)作用域——也叫詞法作用域——在編譯時(shí)靜態(tài)確定的作用域;但動(dòng)態(tài)作用域是在運(yùn)行過(guò)程中動(dòng)態(tài)確定的。簡(jiǎn)言之,靜態(tài)作用域由代碼的層次結(jié)構(gòu)決定,動(dòng)態(tài)作用域由調(diào)用的堆棧層次結(jié)構(gòu)決定。以下代碼是Perl語(yǔ)言動(dòng)態(tài)作用域變量的示例,保存成demo.pl,執(zhí)行 perl demo.pl 能輸出 $v = 1:

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

    回到重構(gòu)問(wèn)題,利用動(dòng)態(tài)作用域的思路,可以抽象出一個(gè)文件對(duì)象包裝器:用指定文件替換全局的文件流,退出時(shí)恢復(fù)。C語(yǔ)言提供了打開(kāi)指定文件并替代標(biāo)準(zhǔn)輸入輸出流的函數(shù)——freopen——但卻沒(méi)自帶恢復(fù)的功能,因此不同的平臺(tái)恢復(fù)方法不同,本文以類(lèi)UNIX環(huán)境為例,在unistd.h包下有 dup 和 fdopen 兩個(gè)函數(shù),分別用于克隆和恢復(fù)文件句柄。示例代碼如下:

    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); /* 完成后恢復(fù)標(biāo)準(zhǔn)IO */}

    有了這個(gè)功能,可以刪除掉所有函數(shù)和接口的 File file 參數(shù)!唯一真正和文件相關(guān)的只剩下 employees_input 和 employees_output,它們分別調(diào)用 Employees employees_read() 和 void employees_print(Employees),為了使用 file_with 做統(tǒng)一的重定向,利用上一版接口全集的方法,把它們的接口統(tǒng)一改成 typedef Employees (*EmployeesFn)(Employees);。最終,重構(gòu)后的完整代碼如下:

    #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; typedef char* String;/* 職員對(duì)象 */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; }/* 職員列表對(duì)象 */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); /* 完成后恢復(fù)標(biāo)準(zhǔn)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); }/* 應(yīng)用層 */int main(void) {Employees employees = employees_input(INPUT_FILE_NAME); /* 從文件讀入 */employees_print(employees); /* 1. 輸出到屏幕 */employees_adjust_salary(employees); /* 2. 調(diào)整薪資 */employees_print(employees); /* 3. 輸出調(diào)整后的結(jié)果 */employees_output(employees, OUTPUT_FILE_NAME); /* 4. 保存到文件 */employees_free(employees); /* 釋放資源 */return EXIT_SUCCESS; }

    這一版本改動(dòng)非常大,連應(yīng)用層接口都有不向下兼容的改動(dòng),所以不要忘記回歸測(cè)試。

    本節(jié)介紹了一個(gè)重構(gòu)的黑科技——?jiǎng)討B(tài)作用域。它很有用,Web系統(tǒng)中 Session 變量就是動(dòng)態(tài)作用域;但它也會(huì)加大判斷代碼所處上下文的難度,導(dǎo)致行為不易預(yù)測(cè)。比如JavaScript中的 this 是JS中唯一一個(gè)動(dòng)態(tài)作用域的變量,看看社區(qū)對(duì) this 的抱怨就知道它的可怕了,它的值由函數(shù)的調(diào)用方?jīng)Q定,很難預(yù)測(cè)后續(xù)的系統(tǒng)維護(hù)者會(huì)把這個(gè)函數(shù)綁定到哪個(gè)對(duì)象上。

    簡(jiǎn)言之,動(dòng)態(tài)有風(fēng)險(xiǎn),入坑需謹(jǐn)慎!

    第九版:數(shù)據(jù)結(jié)構(gòu)替換

    前文都在討論如何讓代碼變得更抽象、更加可維護(hù),但到底有沒(méi)有取得期望的效果,需要一個(gè)例子來(lái)證明。

    之前的版本中,職員列表對(duì)象采用的底層存儲(chǔ)方案是固定長(zhǎng)度為 4 的數(shù)組結(jié)構(gòu),如果未來(lái)"work.txt"文件中的記錄數(shù)不固定,希望把底層的數(shù)據(jù)結(jié)構(gòu)從數(shù)組改成更合適的單鏈表結(jié)構(gòu)。這個(gè)需求是底層數(shù)據(jù)結(jié)構(gòu)的改造,理論上與應(yīng)用層無(wú)關(guān),類(lèi)似從MySQL遷移到Oracle,理論上至多只能影響持久層代碼,業(yè)務(wù)邏輯層等不相關(guān)的代碼是不應(yīng)該有任何修改的。所以,先評(píng)估一下這個(gè)需求涉及的變更點(diǎn):

    • 數(shù)據(jù)結(jié)構(gòu)變化,職員列表結(jié)構(gòu)體 struct _Employees 必然發(fā)生變化。
    • 接著,職員列表對(duì)象的構(gòu)造函數(shù) employees_read 也會(huì)發(fā)生變化。
    • 然后,與構(gòu)造函數(shù)對(duì)應(yīng)的析構(gòu)函數(shù) employees_print 也會(huì)變化。
    • 最后,數(shù)據(jù)結(jié)構(gòu)的迭代方法也會(huì)變化 employees_map。

    除了以上四點(diǎn),其他任何與數(shù)據(jù)結(jié)構(gòu)本身無(wú)關(guān)的代碼都不應(yīng)該發(fā)生變化。所以,代碼重構(gòu)完并通過(guò)測(cè)試之后,如果所有的改動(dòng)范圍確實(shí)只出現(xiàn)在上述四點(diǎn)中,證明前文所有的改造有效——只改動(dòng)與需求相關(guān)的代碼段;否則,證明代碼抽象程度依舊不夠,一段代碼中還耦合著多個(gè)業(yè)務(wù)邏輯,依舊牽一發(fā)動(dòng)全身。

    最終重構(gòu)后的完整代碼如下,改造過(guò)程此處就不再詳述,大家可以一起動(dòng)手試著重構(gòu)看看。

    #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; typedef char* String;/* 職員對(duì)象 */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; }/* 職員列表對(duì)象 */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); /* 完成后恢復(fù)標(biāo)準(zhǔn)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); }/* 應(yīng)用層 */int main(void) {Employees employees = employees_input(INPUT_FILE_NAME); /* 從文件讀入 */employees_print(employees); /* 1. 輸出到屏幕 */employees_adjust_salary(employees); /* 2. 調(diào)整薪資 */employees_print(employees); /* 3. 輸出調(diào)整后的結(jié)果 */employees_output(employees, OUTPUT_FILE_NAME); /* 4. 保存到文件 */employees_free(employees); /* 釋放資源 */return EXIT_SUCCESS; }

    首先執(zhí)行 check.sh 檢查功能是否正確,然后執(zhí)行 diff 檢查修改點(diǎn)是否有超出預(yù)期。

    七 總結(jié)

    本文對(duì)代碼做了多次迭代,介紹如何使用面向?qū)ο?、函?shù)式編程、動(dòng)態(tài)作用域等方法不斷抽象其中重復(fù)的代碼。通過(guò)這個(gè)過(guò)程,可以看到面向?qū)ο缶幊毯秃瘮?shù)式編程兩者并非對(duì)立,都是為了提高代碼的抽象,可以相輔相成:

  • 函數(shù)式編程重點(diǎn)是增強(qiáng)類(lèi)型系統(tǒng):常見(jiàn)的數(shù)據(jù)類(lèi)型有數(shù)值型、字符串型等,函數(shù)式編程要求函數(shù)也是一種數(shù)據(jù)類(lèi)型,即代碼也是一種數(shù)據(jù)。
  • 面向?qū)ο箫L(fēng)格側(cè)重于代碼的組織形式:把數(shù)據(jù)和操作數(shù)據(jù)的函數(shù)組織在類(lèi)中,提高內(nèi)聚;對(duì)象之間通過(guò)調(diào)用開(kāi)放的接口通訊,降低耦合。
  • 本文只是拋磚引玉,并不是標(biāo)準(zhǔn)答案,所以并不是要求后續(xù)所有的代碼都要抽象多少次才能提交。因此,首次交付出去的代碼,到底要到達(dá)第幾版本,這個(gè)問(wèn)題留給大家自己思考。

    在說(shuō)再見(jiàn)之前,再分享兩個(gè)關(guān)于識(shí)別重復(fù)、抽象重用的tips。

    編碼規(guī)范

    編碼規(guī)范在很多地方被反復(fù)強(qiáng)調(diào),也特別容易引發(fā)爭(zhēng)論(如花括號(hào)的位置);在我看來(lái),編碼規(guī)范最大的價(jià)值是便于發(fā)現(xiàn)代碼中的重復(fù)!

    編程語(yǔ)言本身或多或少會(huì)有一些約束,例如文件必須先 open 再 close,這類(lèi)問(wèn)題一般不容易出現(xiàn)不一致;更多的問(wèn)題并不會(huì)在語(yǔ)言層面做約束,例如 if else 中異常處理是放在if代碼塊中還是 else,這類(lèi)問(wèn)題沒(méi)有標(biāo)準(zhǔn)答案,公說(shuō)公有理婆說(shuō)婆有理。編程規(guī)范用于解決第二類(lèi)問(wèn)題:TOOWTDI(There is Only One Way To Do It)。

    只有統(tǒng)一才能清晰,清晰的代碼不一定是短的代碼,但啰嗦的代碼一定是不清晰的,勿忘清晰是重構(gòu)的基礎(chǔ)。

    重構(gòu)順序

    開(kāi)始重構(gòu)時(shí),切記重構(gòu)的元素一定要從小到大!

    就像文章的元素,從單詞、句子、段落依次遞增,重構(gòu)時(shí)也應(yīng)遵循從小到大的原則,依次解決重復(fù)的常量/變量、語(yǔ)句、代碼塊、函數(shù)、類(lèi)、庫(kù)……發(fā)現(xiàn)重復(fù)不能只浮于表面相同,得理解其背后的意義,只有后續(xù)需要一起變化的重復(fù)才是真正的重復(fù)。從小到大的重構(gòu)順序能幫助理解每一個(gè)重復(fù)的細(xì)節(jié),而反之卻容易導(dǎo)致忽略這些背后的細(xì)節(jié)。

    還記得"work.txt"這個(gè)重復(fù)的文件名嗎?如果采用從大到小的重構(gòu)順序,極有可能馬上抽象了一個(gè)重用的 file_open,把文件名寫(xiě)死在這個(gè)公共函數(shù)里。這樣做的確解決了重復(fù)問(wèn)題,整段代碼只有這一處出現(xiàn)"work.txt";但是一旦輸入輸出的文件名變得不同,這個(gè)公共函數(shù)只能棄用。

    傳遞接力棒

    本文第九版的代碼遠(yuǎn)不是完美的代碼,還存在不少重復(fù):

    • employee_read 和 employees_read 中都用到 calloc 分配內(nèi)存空間,并檢查是否分配成功。
    • employees_print 之于 employee_print 和 employees_adjust_salary 之于employee_adjust_salary,區(qū)別只是前者名稱(chēng)多了一個(gè)s,是否有可能根據(jù)這個(gè)規(guī)則自動(dòng)為 Employees 生成與 Employee 一一對(duì)應(yīng)的函數(shù)?
    • ……

    試試有什么辦法繼續(xù)抽象。第二個(gè)問(wèn)題是讓代碼生成代碼,給個(gè)提示,可以用“宏”。

    附錄I:Common Lisp的解決方案

    從函數(shù)式風(fēng)格重構(gòu)的過(guò)程中能體會(huì)到,如果C語(yǔ)言能支持動(dòng)態(tài)類(lèi)型,就不必在 employee_read 中做強(qiáng)制轉(zhuǎn)換;如果C語(yǔ)言支持匿名函數(shù),亦不用寫(xiě)這么多小函數(shù);如果C語(yǔ)言除了能讀入整型、字符串等基礎(chǔ)類(lèi)型,還能直接讀入數(shù)組、結(jié)構(gòu)體等復(fù)合類(lèi)型,就無(wú)需 employee_read 和 employee_print 等輸入輸出函數(shù)……

    其實(shí)許多編程語(yǔ)言(如Python、Ruby、Lisp等)已經(jīng)讓這些“如果”變成現(xiàn)實(shí)!讓看看Common Lisp的解決方案:

    ;; 從文件讀入 (defparameter employees(with-open-file (file #P"work.lisp") ; 內(nèi)置文件環(huán)繞包裝(read file))) ; 內(nèi)置讀取列表等復(fù)雜結(jié)構(gòu);; 1. 輸出到屏幕 (print employees) ; 內(nèi)置輸出列表等復(fù)雜結(jié)構(gòu);; 2. 調(diào)整薪資 (dolist (employee employees)(if (< (third employee) 30000)(incf (third employee) 3000))) ; 就地修改;; 3. 輸出調(diào)整后的結(jié)果 (print employees);; 4. 保存到文件 (with-open-file (file #P"work.lisp" :direction :output :if-exists :overwrite)(print employees file)) ; print是多態(tài)函數(shù),file取代默認(rèn)標(biāo)準(zhǔn)輸出流

    其中work.lisp的內(nèi)容是:

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

    數(shù)據(jù)文件的格式是Common Lisp的列表結(jié)構(gòu),Lisp支持直接從流中讀取 sexp 復(fù)雜結(jié)構(gòu),猶如JavaScript直接讀寫(xiě)JSON結(jié)構(gòu)數(shù)據(jù)。

    原文鏈接

    本文為阿里云原創(chuàng)內(nèi)容,未經(jīng)允許不得轉(zhuǎn)載。

    總結(jié)

    以上是生活随笔為你收集整理的从重复到重用的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。

    如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。