日韩av黄I国产麻豆传媒I国产91av视频在线观看I日韩一区二区三区在线看I美女国产在线I麻豆视频国产在线观看I成人黄色短片

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

从重复到重用

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

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

作者 | 技師
來源 | 阿里技術公眾號

溫馨提示:本文較長,同學們可收藏后再看 :)

一 前言

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

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

二 問題

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

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

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

    三 測試

    在明確了需求之后,第一步要做的是寫測試代碼,而不是寫功能代碼。《重構》一書中對重構的定義是:“在不改變代碼外在行為的前提下,對代碼做出修改,以改進程序的內部結構。”其中明確指出“代碼外在行為”是不改變的!在不斷迭代重構時,“保證每次重構的行為不變”也是一項重復的工作,所以測試先行不僅能盡早地校驗對需求理解的正確性、還能避免重復測試。本文通過一段 Shell 腳本完成以下工作:

    • 初始化work.txt文件。
    • 檢查標準輸出的內容與期望的結果是否一致。
    • 檢查修改后work.txt文件的內容是否與期望一致。
    • 清理現(xiàn)場。

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

    四 可維護代碼

    第一版:It works

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

    問題很簡單,簡單到把所有代碼都塞到 main 函數(shù)里也不覺得長:

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

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

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

    第二版:清晰的代碼,重構的基礎

    第一版代碼解決了問題,讓原來重復的調薪工作變成簡便的、可反復使用的程序。如果它是C語言課堂作業(yè)的答案,看起來還不錯——至少縮進一致,也沒混用空格和制表符;但從軟件工程的角度來講,它簡直糟糕透了,因為沒有清晰的表達意圖:

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

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

    #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,證明本次重構沒有改變程序的行為。

    第三版:代碼映射需求

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

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

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

    理想情況是代碼塊與需求功能點一一對應,例如當前需求有4個功能點,得有4個獨立的代碼塊與之對應。這樣做的好處是:當需求發(fā)生變化時,代碼的修改也相對集中。因此,基于第二版本代碼準備做以下調整:

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

    整理后的完整代碼如下:

    #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. 調整薪資 */for (int index = 0; index < RECORD_COUNT; index++) {if (employees[index].salary < 30000) {employees[index].salary += 3000;}}/* 3. 輸出調整后的結果 */for (int index = 0; index < RECORD_COUNT; index++) {printf("%s %d %d\n", employees[index].name, employees[index].age, employees[index].salary);}/* 4. 保存到文件 */File ostream = fopen(OUTPUT_FILE_NAME, "w");if (ostream == NULL) {fprintf(stderr, "Cannot open %s with w mode.\n", OUTPUT_FILE_NAME);exit(EXIT_FAILURE);}for (int index = 0; index < RECORD_COUNT; index++) {fprintf(ostream, "%s %d %d\n", employees[index].name, employees[index].age, employees[index].salary);}fclose(ostream);return EXIT_SUCCESS; }

    將以上代碼保存成3.c并執(zhí)行 ./check.sh 3.c,確保程序的行為沒有改變。

    五 面向對象風格

    第四版:職員對象抽象

    經(jīng)過兩輪改造,代碼結構已足夠清晰;現(xiàn)在可以開始重構,來梳理代碼層次。

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

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

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

  • Employee 序列化成字符串。
  • 序列化結果輸出到指定文件流。
  • 因為暫無獨立使用某項功能的場景,目前無需進一步拆分:

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

    Employee 結構體 + employee_print 函數(shù)很容易聯(lián)想到面向對象的“類”。面向對象的本質是由一組功能獨立的對象組成系統(tǒng),對象之間通過發(fā)消息協(xié)作完成任務,不見得非要有 class 關鍵字,繼承、封裝、多態(tài)等語法糖。

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

    綜上所述,雖然C語言通常被認為不是面向對象的語言,其實它也能支持面向對象風格。沿上述思路,可以抽象出職員對象的四個方法:

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

    有了職員對象,程序不再只有一個 main 函數(shù)。假設把 main 函數(shù)看作應用層,其他函數(shù)看作類庫、框架或中間件,這樣程序有了層級,層間僅通過開放的接口通訊,即對象的封裝性。

    在Java中有 public、protected、default 和 private 四種可見性修飾符,C語言的函數(shù)默認是公開的,加上 static 關鍵字后只在當前文件可見。為避免應用層向對象隨意發(fā)送消息,約定只有在應用層用到的函數(shù)才公開,所以額外定義了 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_NAMEtypedef FILE *File;/* 職員對象 */typedef struct _Employee {char name[8];int age;int salary; } *Employee;public void employee_free(Employee employee) {free(employee); }public Employee employee_read(File istream) {Employee employee = (Employee) calloc(1, sizeof(struct _Employee));if (employee == NULL) {fprintf(stderr, "employee_read: out of memory\n");exit(EXIT_FAILURE);}if (fscanf(istream, "%s%d%d", employee->name, &employee->age, &employee->salary) != 3) {employee_free(employee);return NULL;}return employee; }public void employee_print(Employee employee, File ostream) {fprintf(ostream, "%s %d %d\n", employee->name, employee->age, employee->salary); }public void employee_adjust_salary(Employee employee) {if (employee->salary < 30000) {employee->salary += 3000;} }/* 應用層 */int main(void) {Employee employees[RECORD_COUNT];/* 從文件讀入 */File istream = fopen(INPUT_FILE_NAME, "r");if (istream == NULL) {fprintf(stderr, "Cannot open %s with r mode.\n", INPUT_FILE_NAME);exit(EXIT_FAILURE);}for (int index = 0; index < RECORD_COUNT; index++) {employees[index] = employee_read(istream);}fclose(istream);/* 1. 輸出到屏幕 */for (int index = 0; index < RECORD_COUNT; index++) {employee_print(employees[index], stdout);}/* 2. 調整薪資 */for (int index = 0; index < RECORD_COUNT; index++) {employee_adjust_salary(employees[index]);}/* 3. 輸出調整后的結果 */for (int index = 0; index < RECORD_COUNT; index++) {employee_print(employees[index], stdout);}/* 4. 保存到文件 */File ostream = fopen(OUTPUT_FILE_NAME, "w");if (ostream == NULL) {fprintf(stderr, "Cannot open %s with w mode.\n", OUTPUT_FILE_NAME);exit(EXIT_FAILURE);}for (int index = 0; index < RECORD_COUNT; index++) {employee_print(employees[index], ostream);}fclose(ostream);/* 釋放資源 */for (int index = 0; index < RECORD_COUNT; index++) {employee_free(employees[index]);}return EXIT_SUCCESS; }

    將代碼保存為4.c,照例執(zhí)行 ./check.sh 4.c,檢測是否有改變程序行為。

    第五版:容器對象抽象

    之前的重構,去除了詞法和句法上的重復,就像一篇文章里的單詞和語句,接著可以看段落有沒有重復,即代碼塊。

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

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

    此時,main 函數(shù)只需調用職員列表對象的方法,不再直接調用職員對象的方法,所以后者可見性從 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_NAMEtypedef FILE *File;/* 職員對象 */typedef struct _Employee {char name[8];int age;int salary; } *Employee;private void employee_free(Employee employee) {free(employee); }private Employee employee_read(File istream) {Employee employee = (Employee) calloc(1, sizeof(struct _Employee));if (employee == NULL) {fprintf(stderr, "employee_read: out of memory\n");exit(EXIT_FAILURE);}if (fscanf(istream, "%s%d%d", employee->name, &employee->age, &employee->salary) != 3) {employee_free(employee);return NULL;}return employee; }private void employee_print(Employee employee, File ostream) {fprintf(ostream, "%s %d %d\n", employee->name, employee->age, employee->salary); }private void employee_adjust_salary(Employee employee) {if (employee->salary < 30000) {employee->salary += 3000;} }/* 職員列表對象 */typedef Employee* Employees;public Employees employees_read(File istream) {Employees employees = (Employees) calloc(RECORD_COUNT, sizeof(Employee));if (employees == NULL) {fprintf(stderr, "employees_read: out of memory\n");exit(EXIT_FAILURE);}for (int index = 0; index < RECORD_COUNT; index++) {employees[index] = employee_read(istream);}return employees; }public void employees_print(Employees employees, File ostream) {for (int index = 0; index < RECORD_COUNT; index++) {employee_print(employees[index], ostream);} }public void employees_adjust_salary(Employees employees) {for (int index = 0; index < RECORD_COUNT; index++) {employee_adjust_salary(employees[index]);} }public void employees_free(Employees employees) {for (int index = 0; index < RECORD_COUNT; index++) {employee_free(employees[index]);}free(employees); }/* 應用層 */int main(void) {/* 從文件讀入 */File istream = fopen(INPUT_FILE_NAME, "r");if (istream == NULL) {fprintf(stderr, "Cannot open %s with r mode.\n", INPUT_FILE_NAME);exit(EXIT_FAILURE);}Employees employees = employees_read(istream);fclose(istream);/* 1. 輸出到屏幕 */employees_print(employees, stdout);/* 2. 調整薪資 */employees_adjust_salary(employees);/* 3. 輸出調整后的結果 */employees_print(employees, stdout);/* 4. 保存到文件 */File ostream = fopen(OUTPUT_FILE_NAME, "w");if (ostream == NULL) {fprintf(stderr, "Cannot open %s with w mode.\n", OUTPUT_FILE_NAME);exit(EXIT_FAILURE);}employees_print(employees, ostream);fclose(ostream);/* 釋放資源 */employees_free(employees);return EXIT_SUCCESS; }

    不要忘記運行 ./check.sh 作回歸測試。

    第六版:輸入輸出抽象

    此時的 main 函數(shù)已經(jīng)比較清爽,剩下一處明顯的重復:打開文件并檢查文件是否正常打開。這屬于文件相關的操作,可以抽象出一個 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ù)抽象職員列表對象的輸入和輸出方法:

    • employees_input:從文件中獲取數(shù)據(jù)并創(chuàng)建職員列表對象。
    • 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_NAMEtypedef FILE *File; typedef char* String;/* 職員對象 */typedef struct _Employee {char name[8];int age;int salary; } *Employee;private void employee_free(Employee employee) {free(employee); }private Employee employee_read(File istream) {Employee employee = (Employee) calloc(1, sizeof(struct _Employee));if (employee == NULL) {fprintf(stderr, "employee_read: out of memory\n");exit(EXIT_FAILURE);}if (fscanf(istream, "%s%d%d", employee->name, &employee->age, &employee->salary) != 3) {employee_free(employee);return NULL;}return employee; }private void employee_print(Employee employee, File ostream) {fprintf(ostream, "%s %d %d\n", employee->name, employee->age, employee->salary); }private void employee_adjust_salary(Employee employee) {if (employee->salary < 30000) {employee->salary += 3000;} }/* 職員列表對象 */typedef Employee* Employees;private Employees employees_read(File istream) {Employees employees = (Employees) calloc(RECORD_COUNT, sizeof(Employee));if (employees == NULL) {fprintf(stderr, "employees_read: out of memory\n");exit(EXIT_FAILURE);}for (int index = 0; index < RECORD_COUNT; index++) {employees[index] = employee_read(istream);}return employees; }public void employees_print(Employees employees, File ostream) {for (int index = 0; index < RECORD_COUNT; index++) {employee_print(employees[index], ostream);} }public void employees_adjust_salary(Employees employees) {for (int index = 0; index < RECORD_COUNT; index++) {employee_adjust_salary(employees[index]);} }public void employees_free(Employees employees) {for (int index = 0; index < RECORD_COUNT; index++) {employee_free(employees[index]);}free(employees); }/* I/O層 */private File file_open(String filename, String mode) {File stream = fopen(filename, mode);if (stream == NULL) {fprintf(stderr, "Cannot open %s with %s mode.\n", filename, mode);exit(EXIT_FAILURE);}return stream; }public Employees employees_input(String filename) {File istream = file_open(filename, "r");Employees employees = employees_read(istream);fclose(istream);return employees; }public void employees_output(Employees employees, String filename) {File ostream = file_open(filename, "w");employees_print(employees, ostream);fclose(ostream); }/* 應用層 */int main(void) {Employees employees = employees_input(INPUT_FILE_NAME); /* 從文件讀入 */employees_print(employees, stdout); /* 1. 輸出到屏幕 */employees_adjust_salary(employees); /* 2. 調整薪資 */employees_print(employees, stdout);/* 3. 輸出調整后的結果 */employees_output(employees, OUTPUT_FILE_NAME);/* 4. 保存到文件 */employees_free(employees); /* 釋放資源 */return EXIT_SUCCESS; }

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

    六 函數(shù)式編程

    第七版:容器迭代重用

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

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

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

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

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

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

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

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

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

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

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

  • 可以用變量命名。
  • 可以提供給過程作為參數(shù)。
  • 可以由過程作為結果返回。
  • 可以包含在數(shù)據(jù)結構中。
  • 對照之下會驚訝地發(fā)現(xiàn),C語言這門看似與函數(shù)式編程最遠的上古編程語言,利用函數(shù)指針,居然也完全符合上述條件。觀察 employee_adjust_salary 和 employee_free 兩個函數(shù),都只有一個 Employee 類型的參數(shù)且沒有返回值,翻譯成C語言就是 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ù)式語言中,這類將函數(shù)作為參數(shù)或返回值的函數(shù)稱為高階函數(shù),C語言里稱為控制語句。用這個自定義的控制語句代替原生的 for 循環(huán),則代碼可以簡化成:

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

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

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

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

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

    同時,需要改造 employees_each 去適應新接口:加入 File 參數(shù),以及返回處理結果。在編程的語義中,單純利用副作用的迭代被稱為 foreach,而關注迭代每個元素的處理結果則稱為 map,即映射。因此,用 employees_map 取代之前的 employees_each:

    private Employees employees_map(Employees employees, File stream, EmployeeFn fn) {for (int index = 0; index < RECORD_COUNT; index++) {employees[index] = fn(employees[index], stream);}return employees; }

    重構后的完整代碼如下:

    #include < stdlib.h> #include < stdio.h>#define private static #define public#define RECORD_COUNT 4#define FILE_NAME "work.txt" #define INPUT_FILE_NAME FILE_NAME #define OUTPUT_FILE_NAME FILE_NAMEtypedef FILE *File; typedef char* String;/* 職員對象 */typedef struct _Employee {char name[8];int age;int salary; } *Employee;typedef Employee (*EmployeeFn)(Employee, File);private Employee employee_free(Employee employee, File stream) {free(employee);return NULL; }private Employee employee_read(Employee employee, File istream) {employee = (Employee) calloc(1, sizeof(struct _Employee));if (employee == NULL) {fprintf(stderr, "employee_read: out of memory\n");exit(EXIT_FAILURE);}if (fscanf(istream, "%s%d%d", employee->name, &employee->age, &employee->salary) != 3) {employee_free(employee, NULL);return NULL;}return employee; }private Employee employee_print(Employee employee, File ostream) {fprintf(ostream, "%s %d %d\n", employee->name, employee->age, employee->salary);return employee; }private Employee employee_adjust_salary(Employee employee, File stream) {if (employee->salary < 30000) {employee->salary += 3000;}return employee; }/* 職員列表對象 */typedef Employee* Employees;private Employees employees_map(Employees employees, File stream, EmployeeFn fn) {for (int index = 0; index < RECORD_COUNT; index++) {employees[index] = fn(employees[index], stream);}return employees; }private Employees employees_read(File istream) {Employees employees = (Employees) calloc(RECORD_COUNT, sizeof(Employee));if (employees == NULL) {fprintf(stderr, "employees_read: out of memory\n");exit(EXIT_FAILURE);}return employees_map(employees, istream, employee_read); }public void employees_print(Employees employees, File ostream) {employees_map(employees, ostream, employee_print); }public void employees_adjust_salary(Employees employees) {employees_map(employees, NULL, employee_adjust_salary); }public void employees_free(Employees employees) {employees_map(employees, NULL, employee_free);free(employees); }/* I/O層 */private File file_open(String filename, String mode) {File stream = fopen(filename, mode);if (stream == NULL) {fprintf(stderr, "Cannot open %s with %s mode.\n", filename, mode);exit(EXIT_FAILURE);}return stream; }public Employees employees_input(String filename) {File istream = file_open(filename, "r");Employees employees = employees_read(istream);fclose(istream);return employees; }public void employees_output(Employees employees, String filename) {File ostream = file_open(filename, "w");employees_print(employees, ostream);fclose(ostream); }/* 應用層 */int main(void) {Employees employees = employees_input(INPUT_FILE_NAME); /* 從文件讀入 */employees_print(employees, stdout); /* 1. 輸出到屏幕 */employees_adjust_salary(employees); /* 2. 調整薪資 */employees_print(employees, stdout);/* 3. 輸出調整后的結果 */employees_output(employees, OUTPUT_FILE_NAME);/* 4. 保存到文件 */employees_free(employees); /* 釋放資源 */return EXIT_SUCCESS; }

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

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

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

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

    這類函數(shù)參數(shù)過多且逐層傳遞的問題,最簡單的解決方法就是使用全局變量。例如定義一個全局的文件對象,指向當前輸入/輸出的目標,這樣就能去除所有的文件對象入?yún)ⅰH肿兞康谋锥耸呛茈y判斷它的影響范圍,不加限制地使用全局變量就和無約束地使用goto一樣,代碼會迅速變成意大利面條。所以,建議有節(jié)制地使用全局變量:用完之后及時將值恢復。例如以下代碼:

    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 臨時開啟了調試選項,并在退出前恢復成原始值。一旦忘記恢復,后續(xù)所有調試信息就都會輸出,惡夢就會開始。為避免這種尷尬問題,可以利用上一版本中提到的函數(shù)式編程的方法,將重復的開啟選項、恢復工作抽象成函數(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 這種負責資源分配再自動回收(或資源修改再自動恢復)工作的函數(shù)稱為上下文包裝器(wrapper),開啟調試選項是一個常見的應用場景,還可以用于自動關閉打開的文件對象(例如Java 7的try-with-resources)。不過,目前的解決方案在多線程環(huán)境下依然有問題,為避免不同的線程之間相互沖突,理想的方案是采用類似Java中的 ThreadLocal 包裝所有全局變量,C語言的多線程方案POSIX thread有Thread Specific組件實現(xiàn)類似的線程特有數(shù)據(jù)功能,此處就不展開討論。

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

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

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

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

    void file_with(String filename, String mode) {int handler = dup(mode[0] == 'r'? 0: 1); /* 克隆文件句柄 */ File stream = freopen(filename, mode, mode[0] == 'r'? stdin: stdout);if (stream == NULL) {fprintf(stderr, "Cannot open %s with %s mode.\n", filename, mode);exit(EXIT_FAILURE);}/* TODO */ fclose(stream);fdopen(handler, mode); /* 完成后恢復標準IO */}

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

    #include < stdlib.h> #include < stdio.h>#include < unistd.h>#define private static #define public#define RECORD_COUNT 4#define FILE_NAME "work.txt" #define INPUT_FILE_NAME FILE_NAME #define OUTPUT_FILE_NAME FILE_NAMEtypedef FILE *File; typedef char* String;/* 職員對象 */typedef struct _Employee {char name[8];int age;int salary; } *Employee;typedef Employee (*EmployeeFn)(Employee);private Employee employee_free(Employee employee) {free(employee);return NULL; }private Employee employee_read(Employee employee) {employee = (Employee) calloc(1, sizeof(struct _Employee));if (employee == NULL) {fprintf(stderr, "employee_read: out of memory\n");exit(EXIT_FAILURE);}if (scanf("%s%d%d", employee->name, &employee->age, &employee->salary) != 3) {employee_free(employee);return NULL;}return employee; }private Employee employee_print(Employee employee) {printf("%s %d %d\n", employee->name, employee->age, employee->salary);return employee; }private Employee employee_adjust_salary(Employee employee) {if (employee->salary < 30000) {employee->salary += 3000;}return employee; }/* 職員列表對象 */typedef Employee* Employees;typedef Employees (*EmployeesFn)(Employees);private Employees employees_map(Employees employees, EmployeeFn fn) {for (int index = 0; index < RECORD_COUNT; index++) {employees[index] = fn(employees[index]);}return employees; }private Employees employees_read(Employees employees) {employees = (Employees) calloc(RECORD_COUNT, sizeof(Employee));if (employees == NULL) {fprintf(stderr, "employees_read: out of memory\n");exit(EXIT_FAILURE);}return employees_map(employees, employee_read); }public Employees employees_print(Employees employees) {return employees_map(employees, employee_print); }public void employees_adjust_salary(Employees employees) {employees_map(employees, employee_adjust_salary); }public void employees_free(Employees employees) {employees_map(employees, employee_free);free(employees); }/* I/O層 */private File file_open(String filename, String mode) {File stream = freopen(filename, mode, mode[0] == 'r'? stdin: stdout);if (stream == NULL) {fprintf(stderr, "Cannot open %s with %s mode.\n", filename, mode);exit(EXIT_FAILURE);}return stream; }private Employees file_with(String filename, String mode, Employees employees, EmployeesFn fn) {int handler = dup(mode[0] == 'r'? 0: 1); /* 克隆文件句柄 */File stream = file_open(filename, mode);employees = fn(employees);fclose(stream);fdopen(handler, mode); /* 完成后恢復標準IO */return employees; }public Employees employees_input(String filename) {return file_with(filename, "r", NULL, employees_read); }public void employees_output(Employees employees, String filename) {file_with(filename, "w", employees, employees_print); }/* 應用層 */int main(void) {Employees employees = employees_input(INPUT_FILE_NAME); /* 從文件讀入 */employees_print(employees); /* 1. 輸出到屏幕 */employees_adjust_salary(employees); /* 2. 調整薪資 */employees_print(employees); /* 3. 輸出調整后的結果 */employees_output(employees, OUTPUT_FILE_NAME); /* 4. 保存到文件 */employees_free(employees); /* 釋放資源 */return EXIT_SUCCESS; }

    這一版本改動非常大,連應用層接口都有不向下兼容的改動,所以不要忘記回歸測試。

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

    簡言之,動態(tài)有風險,入坑需謹慎!

    第九版:數(shù)據(jù)結構替換

    前文都在討論如何讓代碼變得更抽象、更加可維護,但到底有沒有取得期望的效果,需要一個例子來證明。

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

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

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

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

    #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;/* 職員對象 */typedef struct _Employee {char name[8];int age;int salary; } *Employee;typedef Employee (*EmployeeFn)(Employee);private Employee employee_free(Employee employee) {free(employee);return NULL; }private Employee employee_read(Employee employee) {employee = (Employee) calloc(1, sizeof(struct _Employee));if (employee == NULL) {fprintf(stderr, "employee_read: out of memory\n");exit(EXIT_FAILURE);}if (scanf("%s%d%d", employee->name, &employee->age, &employee->salary) != 3) {employee_free(employee);return NULL;}return employee; }private Employee employee_print(Employee employee) {printf("%s %d %d\n", employee->name, employee->age, employee->salary);return employee; }private Employee employee_adjust_salary(Employee employee) {if (employee->salary < 30000) {employee->salary += 3000;}return employee; }/* 職員列表對象 */typedef struct _Employees {Employee employee;struct _Employees *next; } *Employees;typedef Employees (*EmployeesFn)(Employees);private Employees employees_map(Employees employees, EmployeeFn fn) {for (Employees p = employees; p; p = p->next) {p->employee = fn(p->employee);}return employees; }private Employees employees_read(Employees head) {Employees tail = NULL;for (;;) {Employee employee = employee_read(NULL);if (employee == NULL) {return head;}Employees employees = (Employees) calloc(1, sizeof(Employees));if (employees == NULL) {fprintf(stderr, "employees_read: out of memory\n");exit(EXIT_FAILURE);}if (tail == NULL) {head = tail = employees;} else {tail->next = employees;tail = tail->next;}tail->employee = employee;} }public Employees employees_print(Employees employees) {return employees_map(employees, employee_print); }public void employees_adjust_salary(Employees employees) {employees_map(employees, employee_adjust_salary); }public void employees_free(Employees employees) {employees_map(employees, employee_free);while (employees) {Employees e = employees;employees = employees->next;free(e);} }/* I/O層 */private File file_open(String filename, String mode) {File stream = freopen(filename, mode, mode[0] == 'r'? stdin: stdout);if (stream == NULL) {fprintf(stderr, "Cannot open %s with %s mode.\n", filename, mode);exit(EXIT_FAILURE);}return stream; }private Employees file_with(String filename, String mode, Employees employees, EmployeesFn fn) {int handler = dup(mode[0] == 'r'? 0: 1); /* 克隆文件句柄 */File stream = file_open(filename, mode);employees = fn(employees);fclose(stream);fdopen(handler, mode); /* 完成后恢復標準IO */return employees; }public Employees employees_input(String filename) {return file_with(filename, "r", NULL, employees_read); }public void employees_output(Employees employees, String filename) {file_with(filename, "w", employees, employees_print); }/* 應用層 */int main(void) {Employees employees = employees_input(INPUT_FILE_NAME); /* 從文件讀入 */employees_print(employees); /* 1. 輸出到屏幕 */employees_adjust_salary(employees); /* 2. 調整薪資 */employees_print(employees); /* 3. 輸出調整后的結果 */employees_output(employees, OUTPUT_FILE_NAME); /* 4. 保存到文件 */employees_free(employees); /* 釋放資源 */return EXIT_SUCCESS; }

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

    七 總結

    本文對代碼做了多次迭代,介紹如何使用面向對象、函數(shù)式編程、動態(tài)作用域等方法不斷抽象其中重復的代碼。通過這個過程,可以看到面向對象編程和函數(shù)式編程兩者并非對立,都是為了提高代碼的抽象,可以相輔相成:

  • 函數(shù)式編程重點是增強類型系統(tǒng):常見的數(shù)據(jù)類型有數(shù)值型、字符串型等,函數(shù)式編程要求函數(shù)也是一種數(shù)據(jù)類型,即代碼也是一種數(shù)據(jù)。
  • 面向對象風格側重于代碼的組織形式:把數(shù)據(jù)和操作數(shù)據(jù)的函數(shù)組織在類中,提高內聚;對象之間通過調用開放的接口通訊,降低耦合。
  • 本文只是拋磚引玉,并不是標準答案,所以并不是要求后續(xù)所有的代碼都要抽象多少次才能提交。因此,首次交付出去的代碼,到底要到達第幾版本,這個問題留給大家自己思考。

    在說再見之前,再分享兩個關于識別重復、抽象重用的tips。

    編碼規(guī)范

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

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

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

    重構順序

    開始重構時,切記重構的元素一定要從小到大!

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

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

    傳遞接力棒

    本文第九版的代碼遠不是完美的代碼,還存在不少重復:

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

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

    附錄I:Common Lisp的解決方案

    從函數(shù)式風格重構的過程中能體會到,如果C語言能支持動態(tài)類型,就不必在 employee_read 中做強制轉換;如果C語言支持匿名函數(shù),亦不用寫這么多小函數(shù);如果C語言除了能讀入整型、字符串等基礎類型,還能直接讀入數(shù)組、結構體等復合類型,就無需 employee_read 和 employee_print 等輸入輸出函數(shù)……

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

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

    其中work.lisp的內容是:

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

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

    原文鏈接

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

    總結

    以上是生活随笔為你收集整理的从重复到重用的全部內容,希望文章能夠幫你解決所遇到的問題。

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

    在线观看成年人 | 国产成人61精品免费看片 | 91成人精品国产刺激国语对白 | 天堂在线v | 操碰av | 久久99热这里只有精品国产 | 成人国产亚洲 | 中文字幕丰满人伦在线 | 亚洲传媒在线 | 亚州精品成人 | 久久精品国产一区二区 | 国产高清视频色在线www | 久久一视频 | av高清一区 | 欧美一二三视频 | 久久久久久久久久久免费视频 | 国产理论一区二区三区 | 天天操天天操天天操 | 久久综合久久综合久久 | 日韩av中文字幕在线免费观看 | 免费观看av | 97在线看片 | 免费在线观看中文字幕 | 色综合久久久网 | 欧美日韩亚洲在线 | 黄色三级免费 | 麻豆果冻剧传媒在线播放 | 日韩特黄一级欧美毛片特黄 | 六月丁香综合 | 不卡电影一区二区三区 | 中文字幕观看视频 | 亚洲黄色网络 | 日韩免费在线观看视频 | 九九九九精品九九九九 | 国产尤物一区二区三区 | 99国产情侣在线播放 | 人人舔人人干 | 特级aaa毛片 | 久久久久国产a免费观看rela | 日韩免费电影一区二区三区 | 国产精品一区二区三区久久 | 狠狠色狠狠色综合系列 | 婷婷六月丁 | 伊香蕉大综综综合久久啪 | 五月天国产 | 97电影在线观看 | 在线精品观看国产 | 青青草国产在线 | 99热这里只有精品免费 | 国产香蕉视频在线播放 | 黄色a级片在线观看 | 黄网av在线| 精品电影一区 | 在线免费av观看 | 国产精品国产亚洲精品看不卡 | 999久久久| 精品国产1区2区3区 国产欧美精品在线观看 | 天天天操操操 | 青草视频在线 | 成人在线免费观看网站 | 色综合久久88色综合天天免费 | 97久久精品午夜一区二区 | 亚州人成在线播放 | 美女黄频在线观看 | 玖玖在线免费视频 | 麻豆综合网| 日日爽夜夜爽 | 国产麻豆电影在线观看 | 国产一区二区三区久久久 | 欧美另类交人妖 | 精品视频资源站 | 男女视频国产 | 97视频总站 | 九九九九九九精品 | 日韩电影一区二区在线 | 美腿丝袜一区二区三区 | 五月的婷婷 | 久久经典国产视频 | 日韩精品一区二区三区三炮视频 | 高清不卡免费视频 | 日韩成人看片 | 夜夜操网 | aaa免费毛片 | 久久一区91 | 天天碰天天操 | 久久99精品久久久久久清纯直播 | 日韩免费在线一区 | 日日操日日干 | 色婷婷久久久综合中文字幕 | 欧美色图一区 | 亚洲女欲精品久久久久久久18 | 18久久久久久 | 激情丁香久久 | 99在线观看视频网站 | 午夜视频在线观看一区二区三区 | 国产精品第三页 | 久久综合9988久久爱 | 干干干操操操 | 欧美日韩一区二区在线观看 | 欧美国产一区二区 | 亚洲激情在线观看 | 9999国产精品 | 中文字幕在线观看日本 | 黄色av在| 99免费看片 | 久久精品麻豆 | 日韩欧美在线观看一区二区三区 | 久草在线资源观看 | 久久精品在线 | 一级黄色片在线播放 | 午夜久久影视 | 亚洲成av人片在线观看无 | 操操操操网 | 国产亚洲综合精品 | 欧美性生活久久 | 69亚洲乱| 懂色av一区二区在线播放 | 在线视频99 | 一区二区三区精品久久久 | 久久精品免费 | 国产精品福利午夜在线观看 | 国产一二区免费视频 | 色综合久久综合中文综合网 | 国产亚洲午夜高清国产拍精品 | 亚洲国产网站 | 911免费视频| 一区二区影院 | 激情综合色播五月 | 在线观看日韩精品 | a午夜电影 | 免费成人av网站 | 免费在线观看av网站 | 成人午夜在线电影 | 日韩免费在线 | 亚洲激情在线播放 | 久久久久亚洲国产精品 | 中日韩三级视频 | 色狠狠婷婷 | 日日爽视频 | 精品v亚洲v欧美v高清v | 精品国偷自产在线 | 又色又爽又激情的59视频 | av成人资源 | 在线成人免费电影 | 免费在线观看a v | 亚洲国产精品电影 | 亚洲成人午夜av | 国产成人精品一区二区在线 | 久久久18| 久久香蕉影视 | 亚洲视频电影在线 | 菠萝菠萝蜜在线播放 | 国产成人三级三级三级97 | 亚洲日本成人 | 最近中文字幕完整视频高清1 | 91探花在线视频 | 国产一区二区在线看 | 国产 日韩 在线 亚洲 字幕 中文 | 久久久久免费精品视频 | 国产精品18久久久久vr手机版特色 | 国产裸体视频网站 | 日韩视频在线观看免费 | 黄色av影院| 欧美激情视频一区 | 毛片久久久 | 91综合久久一区二区 | 黄色片网站av | 91探花在线视频 | 91欧美日韩国产 | 亚洲一区视频在线播放 | 国产在线综合视频 | 一级a性色生活片久久毛片波多野 | 欧美精品九九99久久 | 视频直播国产精品 | 日韩国产欧美视频 | 黄色成人影院 | 97超碰在线视 | 国产小视频免费在线观看 | 永久免费在线 | 日韩欧美视频在线免费观看 | www.夜夜| 在线观看亚洲国产精品 | 在线观看国产一区 | 欧美亚洲国产精品久久高清浪潮 | 最新精品视频在线 | 91丨精品丨蝌蚪丨白丝jk | 激情丁香5月 | www.久久视频 | 亚洲视频精品 | 久久国产精品色av免费看 | 久久免费99精品久久久久久 | 日韩电影中文字幕 | 精品在线观看免费 | 99免费观看视频 | 国产日韩在线视频 | 日韩成人免费观看 | 亚洲精品国产精品久久99 | 成人精品国产 | 日韩午夜精品福利 | 高清不卡一区二区在线 | 国产一区二区在线播放 | www·22com天天操 | 国产免费三级在线观看 | 国产精品久久在线 | 国产一区二区三区 在线 | 国产精品扒开做爽爽的视频 | 色播激情五月 | 国内精品小视频 | 天天干天天射天天插 | 亚洲激情中文 | 九九九九精品九九九九 | 天天操天天干天天玩 | 欧美无极色 | 中文字幕电影网 | 黄色在线免费观看网站 | 成人wwwxxx视频 | 96av在线| 中文字幕第 | 人人看看人人 | 婷婷综合导航 | 91久久在线观看 | 久久视频网址 | 中文字幕 婷婷 | 国产91精品一区二区绿帽 | 99久久99久久| 天天射天天操天天干 | 美州a亚洲一视本频v色道 | a级黄色片视频 | 97人人爽人人 | 激情黄色av | 久久久免费| 久久99精品久久久久婷婷 | 国产精品久久99综合免费观看尤物 | 草久在线观看视频 | 99国产精品免费网站 | 天天操天天干天天摸 | 国产免费久久av | 日韩av看片| 久草在线观看资源 | 国产精品一区二区三区视频免费 | 中文字幕在线观看第一区 | 天天操天天拍 | 伊人网综合在线观看 | 1000部18岁以下禁看视频 | av电影免费在线播放 | 亚洲一片黄 | 久久精品高清视频 | 国产高清在线一区 | av高清在线 | 激情综合网色播五月 | av在线免费不卡 | 亚洲精品麻豆 | 69av在线播放 | 国产精品美女久久久久久 | 日本黄色大片免费看 | 久久久av免费 | 视频一区二区在线观看 | 天天干天天操天天射 | 日韩免费电影一区二区 | 成年人电影免费在线观看 | 人人揉人人揉人人揉人人揉97 | 伊人黄色网 | 国产特级毛片aaaaaaa高清 | www.五月天婷婷| 亚洲综合欧美精品电影 | 99久热在线精品视频观看 | 国内小视频在线观看 | 久久久国产精品电影 | 一区二区三区在线不卡 | 欧美日韩视频在线一区 | .国产精品成人自产拍在线观看6 | 国产午夜精品福利视频 | av在线永久免费观看 | 国产一区久久久 | 99精品成人| 视频在线观看入口黄最新永久免费国产 | 特黄特色特刺激视频免费播放 | av日韩在线网站 | 日韩在线观看你懂得 | 精品在线播放 | 色偷偷中文字幕 | 三级av免费观看 | 在线观看国产麻豆 | 精品自拍网 | 91人人揉日日捏人人看 | 国产一区二区久久久 | 操操综合| 日韩免费播放 | 国产精品com | 最近中文字幕完整高清 | 久久国产成人午夜av影院潦草 | 六月色| 亚洲a网 | 99久久精品午夜一区二区小说 | 中文字幕在线观看三区 | 久久xx视频 | 日韩精品一区在线播放 | 日韩电影中文,亚洲精品乱码 | 日韩精品一区二区在线观看视频 | 在线天堂中文www视软件 | 青青草国产成人99久久 | 在线视频 国产 日韩 | 午夜av在线播放 | 国产精在线 | 日韩一区二区三区在线看 | 成人黄色免费在线观看 | 操操操com | 最新免费中文字幕 | 亚洲国内精品在线 | 国产xxxx做受性欧美88 | 欧美激情精品久久久久久变态 | 欧美精品久久久久久久久久丰满 | 四虎影视成人永久免费观看亚洲欧美 | 国产香蕉久久精品综合网 | 久久国产欧美日韩 | 国产中文字幕在线视频 | 国产成人a v电影 | 久久综合久久88 | 国产中文字幕91 | 久久久久久久国产精品影院 | 中文字幕一区二区三区乱码在线 | 69视频在线播放 | 狠狠色香婷婷久久亚洲精品 | 久久男女视频 | 欧美最新大片在线看 | 国产精品中文久久久久久久 | 国产91aaa | 久久精品婷婷 | 免费亚洲电影 | 欧美一级片 | 狠狠干 狠狠操 | 亚洲一区网 | 国产 在线 日韩 | 国产最新在线观看 | 久久久久欧美精品 | 国产一区私人高清影院 | 国产视频亚洲视频 | 在线观看免费版高清版 | 日韩资源在线播放 | 成人毛片一区二区三区 | 中文字幕传媒 | 国产精品一区二区美女视频免费看 | 麻豆视频一区 | 久久精品一区二区国产 | 免费在线观看国产精品 | 日韩欧美一区二区三区在线 | 狠狠精品 | 在线观看中文字幕2021 | 免费中文字幕 | 国产午夜精品在线 | 久久久久成人精品亚洲国产 | 中文欧美字幕免费 | 欧美激情在线网站 | 超碰97av在线| 日韩视频免费观看高清完整版在线 | www.天天色 | 91人人人 | 91看片淫黄大片在线播放 | 91久久在线观看 | 五月天精品视频 | 成年人app网址 | 日本黄色免费播放 | 日韩精品短视频 | 国产精品久久久av久久久 | 一级免费av | 四虎影视成人永久免费观看亚洲欧美 | 91精品在线免费视频 | 日韩精品一区二区三区高清免费 | 国产青春久久久国产毛片 | 国产色婷婷在线 | 欧美男同视频网站 | 91精品视频网站 | 日韩电影中文,亚洲精品乱码 | 超碰97中文 | 久久成视频 | a黄色 | 亚洲精品9 | 在线黄色毛片 | 在线看国产视频 | 亚洲欧洲中文日韩久久av乱码 | 亚洲午夜精品电影 | 久久视频在线看 | 人人干在线观看 | 在线精品视频在线观看高清 | 九九久久国产 | 日韩精品中文字幕在线播放 | av中文字幕亚洲 | 亚州精品天堂中文字幕 | 手机在线免费av | 亚洲午夜精品在线观看 | 91精品国自产拍天天拍 | 亚洲免费一级电影 | 亚在线播放中文视频 | 色资源二区在线视频 | 欧美孕妇与黑人孕交 | 黄色成人在线 | 亚洲欧洲精品久久 | 亚洲jizzjizz日本少妇 | 97超级碰 | 久久久久亚洲精品中文字幕 | 国产精品99免费看 | 天天添夜夜操 | 日韩中文字幕91 | 欧美 亚洲 另类 激情 另类 | 国产黄色免费电影 | 手机在线永久免费观看av片 | 狠狠搞,com | 天天想夜夜操 | 久久电影中文字幕视频 | 91国内在线| 丝袜护士aⅴ在线白丝护士 天天综合精品 | 中文不卡视频 | 日韩免费一区 | 色婷婷成人网 | 中文字幕成人 | 丝袜美腿在线视频 | 91精品少妇偷拍99 | 午夜aaaa | 国产无套精品久久久久久 | 国产九色视频在线观看 | 精品国产亚洲在线 | 久久最新| 久久久久女人精品毛片九一 | 水蜜桃亚洲一二三四在线 | 天堂av中文字幕 | 在线播放 日韩专区 | 亚洲视频播放 | 特级黄色视频毛片 | av大全在线免费观看 | 日日干网 | 国产成人av福利 | 日韩欧美视频免费在线观看 | 日韩精品中文字幕在线 | 在线看国产精品 | 99精品视频一区二区 | 天天插狠狠插 | 在线综合色 | www.久久爱.cn | 亚洲精欧美一区二区精品 | 欧美日本国产在线观看 | 国产精品99久久久久人中文网介绍 | 国产午夜三级一区二区三桃花影视 | 久久精品久久99精品久久 | 国产一二三四在线视频 | 另类五月激情 | 久久国产系列 | 天天色棕合合合合合合 | 91在线欧美| 国产精品免费久久久久 | 一区二区电影在线观看 | 久久久九九| 免费成人在线网站 | 久操视频在线播放 | 国产精品国产三级国产专区53 | 午夜国产福利视频 | 97在线视频网站 | 亚洲综合在线观看视频 | 成年人免费在线播放 | 久久久久亚洲精品国产 | 亚洲午夜精品一区二区三区电影院 | 国产.精品.日韩.另类.中文.在线.播放 | 91丨九色丨高潮丰满 | 婷婷精品进入 | av在线播放亚洲 | 一级免费看 | 人人草天天草 | 最近2019好看的中文字幕免费 | 国产一级视频 | 天天综合入口 | 日韩欧美亚州 | 亚洲视频在线看 | 亚洲精品综合在线观看 | 黄色中文字幕 | 欧美专区国产专区 | 激情久久伊人 | 久久久免费看片 | av资源网在线播放 | 久久国产热 | 亚洲人成网站精品片在线观看 | 久久国产精品99久久久久久丝袜 | 免费日p视频 | 久草免费福利在线观看 | 色五月成人 | 欧洲亚洲女同hd | 毛片1000部免费看 | 中文字幕在线国产 | 伊人电影在线观看 | 超碰97免费在线 | 久久久91精品国产一区二区精品 | 99热亚洲精品 | 激情五月婷婷激情 | 五月天婷亚洲天综合网精品偷 | 一区二区三区四区在线 | 久久国产精品久久久 | 狠狠插狠狠干 | 国产美女在线观看 | 亚洲狠狠丁香婷婷综合久久久 | 婷婷六月在线 | 久久艹在线 | 亚洲在线网址 | 国产精品第54页 | 成年人免费电影 | 国产精成人品免费观看 | 日韩免费三区 | 在线看日韩av| 亚洲美女精品区人人人人 | 一区二区三区在线免费播放 | 久久精品一区二区三区视频 | ,久久福利影视 | 日韩精品视 | 亚洲电影成人 | 欧美人体xx | 婷婷av网| 欧美一区二区三区不卡 | 91麻豆精品国产自产在线游戏 | 欧美久久久久久 | 国产人免费人成免费视频 | 婷婷综合伊人 | 欧美久久久 | 午夜av电影院 | 免费能看的黄色片 | 久久国产午夜精品理论片最新版本 | 欧美激情在线看 | 婷婷在线免费视频 | 美女视频久久久 | 亚洲精品tv久久久久久久久久 | 色播六月天 | 91av视频在线播放 | 久久艹在线 | 91免费视频黄 | 狠狠干网 | 人人爱人人舔 | av在线超碰 | 美女视频又黄又免费 | 97视频人人澡人人爽 | 99久热在线精品视频观看 | 国产视频一区二区在线 | 国产xxxx性hd极品 | 人人插人人射 | 免费视频一区 | 亚洲国产精品女人久久久 | 手机在线看片日韩 | 久久久久久中文字幕 | 国产丝袜 | 在线免费三级 | 欧美人牲 | 日日精品 | 国产伦精品一区二区三区高清 | 日本久久久久久久久久 | 久久亚洲福利视频 | 国产午夜精品一区 | 午夜电影一区 | 欧美日韩p片 | 国产自产高清不卡 | 欧洲亚洲精品 | 在线天堂亚洲 | 91日韩在线专区 | 91麻豆网站 | 国产真实精品久久二三区 | 亚洲精品乱码白浆高清久久久久久 | 天天射天天爽 | 国产精品麻豆免费版 | 久久夜色精品国产欧美乱 | 性色大片在线观看 | 99av在线视频 | 久久成人麻豆午夜电影 | 在线观看av片| 国产一区 在线播放 | 在线播放精品一区二区三区 | 成人黄色在线 | 伊人婷婷网| 最新av网址在线观看 | 免费黄色一区 | 日韩丝袜在线观看 | www.超碰| 国产手机视频在线播放 | 国产主播大尺度精品福利免费 | 免费观看的黄色 | 久久久精品国产一区二区电影四季 | 成人免费视频网站 | 日本中文在线观看 | 亚洲视频免费在线 | 国产亚洲精品久久 | 成人资源在线播放 | 久久久久久久毛片 | a级国产片| 国产在线一区观看 | 日本一区二区三区免费观看 | 一区二区中文字幕在线观看 | 精品久久久一区二区 | 国产高清视频在线播放一区 | 亚洲无线视频 | 一区二区视频电影在线观看 | 婷婷新五月| 久久色视频 | 国产一区二区三区免费视频 | 婷婷在线网站 | 亚洲精选在线 | 午夜精品视频福利 | 免费黄色a网站 | 国产成人一区二区三区影院在线 | 日韩在线视频观看 | 欧美精品一区二区在线播放 | www日日夜夜 | 色欧美成人精品a∨在线观看 | 亚洲综合精品视频 | 国产精品精品国产婷婷这里av | 久久精品国产一区二区三 | 亚洲黄色大片 | 欧美综合在线观看 | 精品国产色 | 亚洲国产精久久久久久久 | 精品亚洲欧美无人区乱码 | www.黄色在线 | 免费在线观看91 | 99久久婷婷国产 | 久久免费视频8 | 国产精品自产拍在线观看桃花 | a天堂免费| 亚洲天堂首页 | 黄色成人影视 | 天天操天天色综合 | 在线看黄色的网站 | 国产69精品久久久久99 | 天天操天天操天天操天天操天天操天天操 | 日韩免费一区二区在线观看 | 狠狠伊人 | 久久国产精品网站 | 岛国av在线不卡 | 伊人色综合久久天天 | 在线超碰av | 91精品国产99久久久久久久 | 日日夜夜天天综合 | 精品女同一区二区三区在线观看 | 成人sm另类专区 | 亚洲第一av在线 | 免费观看91视频 | 久草精品在线观看 | 国产亚洲精品久久久久久大师 | 午夜在线日韩 | 久草在线久草在线2 | 国产高清亚洲 | 国产91精品久久久久 | 黄色aa久久| 91视频亚洲 | .国产精品成人自产拍在线观看6 | 婷婷六月综合网 | 免费高清在线观看成人 | 色.com| 天天干夜夜操视频 | 亚洲va欧美va | 色五丁香| 4hu视频| 国产伦精品一区二区三区无广告 | 99精品在线视频播放 | 一级免费片 | 欧美亚洲精品在线观看 | 日韩欧美一级二级 | 成人性生交大片免费看中文网站 | 91网站免费观看 | 99热超碰在线 | 午夜婷婷网 | 久久精品91久久久久久再现 | 久久精品站 | www.色五月| 欧美久久久一区二区三区 | 91亚洲网| 欧美做受69 | 免费情缘 | 国产手机av| 99久久精品国产亚洲 | 欧美怡红院视频 | aaa毛片视频 | 91精品国产92久久久久 | 久久久久免费精品国产小说色大师 | 精品国产一区二区三区久久久蜜臀 | 99久久婷婷国产一区二区三区 | 国产婷婷一区二区 | 久久狠狠一本精品综合网 | 日韩在线理论 | 国内精品久久久久久久影视简单 | 亚洲一级电影在线观看 | 日韩精品视频在线免费观看 | 黄色免费av | 99爱视频在线观看 | av不卡中文字幕 | 精品久久久国产 | 伊人国产在线观看 | 中文字幕在线视频一区二区三区 | 国产精品美女久久久久久久 | 国产专区视频在线观看 | 久久精品亚洲一区二区三区观看模式 | 正在播放 国产精品 | 国产福利不卡视频 | www天天干com| 欧美狠狠色 | 欧美日本一区 | 日韩大片在线观看 | 成人97视频一区二区 | 99精品国产99久久久久久福利 | 91精品视频导航 | 超碰在线免费福利 | 日韩电影精品一区 | 欧美a级片免费看 | 深爱激情av | 亚洲精品视频久久 | 亚洲h视频在线 | 久久激情婷婷 | 久久久免费毛片 | 国产在线欧美 | 中文字幕日本在线 | 日韩成人xxxx| 狠狠综合 | 久久香蕉国产精品麻豆粉嫩av | 日日干夜夜草 | 亚洲国产操 | 天天操福利视频 | 在线观看中文字幕一区 | 在线观看视频三级 | 999色视频 | 五月天婷亚洲天综合网精品偷 | 中文字幕人成一区 | 亚洲最新av网站 | 日韩精品一二三 | 99热这里只有精品免费 | 日韩成人精品一区二区三区 | 久久精久久精 | 久久综合九色 | 成人黄色av网站 | 久久99在线观看 | 在线中文字幕一区二区 | 国产99久久久国产精品免费看 | 777xxx欧美 | 激情五月婷婷丁香 | 天天拍天天爽 | 亚洲国产精品人久久电影 | 国产亚洲精品电影 | 国产一区久久 | 久久久久国产一区二区三区 | 日本丶国产丶欧美色综合 | 亚洲精品88欧美一区二区 | 久久尤物电影视频在线观看 | 国产高清专区 | 亚洲美女视频在线观看 | 国产在线观看,日本 | 免费在线色电影 | 亚洲精品国偷自产在线91正片 | 免费精品在线 | 欧产日产国产69 | 欧美一二三区播放 | 久久开心激情 | 女人18片毛片90分钟 | 在线小视频 | 狠狠的日日 | 久久99电影 | 久久人人爽av | 成人午夜电影在线观看 | 国产91丝袜在线播放动漫 | 中文字幕成人在线 | 亚洲欧美日韩精品久久久 | 国产小视频在线观看 | 在线亚洲小视频 | 日韩欧美专区 | 国产日韩欧美在线观看 | 丁香激情五月婷婷 | 一区二区三区免费在线观看视频 | 麻豆国产网站入口 | 色婷婷久久久综合中文字幕 | 久草在线中文视频 | 亚洲精品免费观看视频 | 操操操操网 | 国产a精品 | 免费高清男女打扑克视频 | 激情五月播播久久久精品 | 婷婷成人亚洲综合国产xv88 | 六月丁香在线视频 | 亚州精品天堂中文字幕 | 在线观看国产v片 | 久久午夜国产精品 | 色中文字幕在线观看 | 国产伦精品一区二区三区四区视频 | 亚洲国产精品久久 | 一区二区三区韩国免费中文网站 | 天天爽天天碰狠狠添 | 国产又粗又硬又长又爽的视频 | 999久久| 九九免费观看视频 | 欧美资源| 激情网在线视频 | 国产流白浆高潮在线观看 | 日本爽妇网 | 在线成人高清电影 | 欧美日韩一区久久 | 精品女同一区二区三区在线观看 | 黄av免费在线观看 | 精品av网站 | 色午夜 | 夜色成人av | 中文字幕在线观看视频免费 | 亚洲激情校园春色 | 日韩高清在线一区二区 | 欧美性一级观看 | 亚洲国产三级在线观看 | 日韩高清成人 | 国产原创在线观看 | 在线观看视频一区二区三区 | 国产一级二级三级视频 | 国产精品 中文在线 | 91黄视频在线观看 | 很黄很污的视频网站 | 日本久久久久久久久久久 | 丁香婷婷综合激情 | 在线视频婷婷 | 天天天色综合a | 狠狠躁夜夜躁人人爽视频 | 久草久视频 | 久草在线免费看视频 | 4438全国亚洲精品观看视频 | 成人久久18免费网站 | 中文字幕免费一区二区 | 日韩午夜av | 在线观看完整版 | 中文字幕视频 | 伊人久操 | 在线观看亚洲电影 | 93久久精品日日躁夜夜躁欧美 | 91av电影网| 天堂av在线 | av午夜电影 | 亚洲三级国产 | 蜜臀av.com| 天天激情 | 日韩一区二区三区免费视频 | 亚洲精品乱码久久久久久蜜桃不爽 | 欧美精品一区二区免费 | 久香蕉 | 天天操欧美 | www.日日日.com| 五月婷婷激情六月 | 国产亚洲成人网 | 福利av影院 | 国产91亚洲精品 | 国产午夜精品av一区二区 | 开心色激情网 | 亚洲精品美女在线观看 | 五月综合久久 | 美腿丝袜一区二区三区 | 日韩免费视频网站 | 91九色视频导航 | 成人av电影免费在线观看 | av免费电影在线 | japanesexxxhd奶水 国产一区二区在线免费观看 | 久久综合精品国产一区二区三区 | 99在线视频免费观看 | 东方av在| 亚洲国产欧美在线看片xxoo | 天天爱天天射 | 国产精品精品国产婷婷这里av | 综合色站| 欧美亚洲国产精品久久高清浪潮 | 人人爽人人爽 | 欧美va在线观看 | 成人一级片免费看 | 国产成人精品av在线 | 337p西西人体大胆瓣开下部 | 精品欧美一区二区在线观看 | 精品免费视频. | 69视频永久免费观看 | 亚洲黄网站 | 五月婷综合 | 人人插人人干 | 亚洲国产av精品毛片鲁大师 | 成人av高清 | 日韩免费在线 | 成人动态视频 | 亚洲精品日韩av | 免费99精品国产自在在线 | 超碰在线9| 成人在线免费观看网站 | 天天搞夜夜骑 | 欧美婷婷色 | 免费视频一二三区 | 亚洲国产中文在线 | 亚洲一级电影在线观看 | 国产v视频 | 久久久久国产精品免费免费搜索 | 综合激情伊人 | 男女视频91 | 日韩电影中文字幕在线观看 | 午夜av大片 | 一区二区三区免费在线观看视频 | 国产精品久久久久久久久毛片 | 97手机电影网 | 久久伊人热 | 欧美精品久久久久久久久老牛影院 | 亚洲传媒在线 | 婷婷av网站 | 国产视频高清 | 黄色三级视频片 | 操天天操 | www最近高清中文国语在线观看 | 日韩一区二区三区在线观看 | 女人高潮特级毛片 | 韩日av一区二区 | 久久精品波多野结衣 | 亚洲高清视频一区二区三区 | 欧美特一级片 | 亚洲最快最全在线视频 | av在线播放一区二区三区 | 国产成人免费av电影 | 久草视频在线看 | 国产精品不卡视频 | 精品久久一区 | 亚洲精品视频在线免费 | 中文字幕av网站 | 国产一级视频在线观看 | 欧美成人黄色 | 夜夜操综合网 | 欧美日韩精品综合 | 黄色软件视频大全免费下载 | 久久情爱| 一本一道久久a久久精品蜜桃 | 人人爽人人爽人人片 | 国产一区在线免费观看 | 国产一级性生活视频 | 国产精品久久久久久久久久久久久久 | 日韩激情免费视频 | 久久成人国产精品免费软件 | 色天堂在线视频 | 精品在线99 | 国产伦精品一区二区三区在线 | 国产成人精品在线 | 日本激情动作片免费看 | 国产一区二区精品在线 | 人人揉人人揉人人揉人人揉97 | 国产亚洲成av人片在线观看桃 | 免费av网站观看 | 免费看片网址 | 91视频在线免费下载 | 日韩免| 九色视频自拍 | 一区二区激情视频 | 激情一区二区三区欧美 | www.97视频 | 91看片网址 | 国产精品久久久久久欧美 | 日韩黄色一区 | 99热这里有精品 | 国产成人无码AⅤ片在线观 日韩av不卡在线 | 国产三级av在线 | 国产视频亚洲精品 | 天堂av在线免费观看 | 午夜黄色大片 | 欧产日产国产69 | 亚洲毛片视频 | 久久激情影院 | 中文字幕在线不卡国产视频 | 精品亚洲成a人在线观看 | 国产视频手机在线 | 国产免费又粗又猛又爽 | 国产精品久久久久久模特 | 欧美日韩中文字幕综合视频 | 正在播放日韩 | 国产精品久久精品 | 欧美一区免费观看 | 国产精品大片免费观看 | 在线观看不卡视频 | 成年人视频在线免费观看 | 99精品在线免费视频 | 黄色三级网站 | 国产视频精选在线 | 黄污在线看 | 在线免费观看黄 | 丁香网五月天 | 欧美做受xxx | 欧洲在线免费视频 | 啪啪精品 | 精品国产免费一区二区三区五区 | 最近最新最好看中文视频 | 波多野结衣精品视频 | 精品久久久久久久久中文字幕 | 狠狠色伊人亚洲综合网站野外 | 伊人狠狠色丁香婷婷综合 | 狠狠综合久久av | 国产一区二区三区在线免费观看 | 91亚洲精品久久久中文字幕 | 一区二区视频在线免费观看 | 久久a级片 | 久久激情小说 | 国产精品久久久久久69 | 嫩草av影院 | 国产一区二区三区在线免费观看 | 99re国产| 97手机电影网 | 色婷婷骚婷婷 | 亚洲免费在线观看视频 | 麻豆精品视频在线观看免费 | 在线观看视频国产一区 |