# Abstract Data Types: Information Hiding ## Abstract Data Types 作者認為的良好的程式撰寫風格要盡量把物件的表示方式 (e.g., 資料型態組成方式) 隱藏 (封裝),並且僅提供必要的操作介面 (抽象化)。 要做到這件事必然會提到 **abstract data type** (抽象化資料),也就是將不是必須提供給使用者知道的資訊隱藏的物件 (資料型態)。 抽象化資料可以讓我們開發的程式具有彈性,因為僅提供必要資訊 (e.g., 各項操作的介面) 實際上代表著無論我們如何改變實作方式,只要程式執行結果與原先是相同的都不會影響到使用者。 * 不論未來要新增功能,或是改變實作方式以提升效能都不會影響到使用者,因為使用者使用的介面仍維持不變。 除了提供我們開發上的彈性以外,在只提供必要資訊的同時我們也把各項功能的實作獨立出來,也就是說我們把資料型態要提供的各個功能用類似 divide and conquer 的方式分開實作, ## An Example — Set 以下為一個使用資料抽象化概念實作的 set 例子: ```C= /* Set.h */ #ifndef SET_H #define SET_H extern const void * Set; void * add (void * set, const void * element); void * find (const void * set, const void * element); void * drop (void * set, const void * element); int contains (const void * set, const void * element); #endif ``` 上方程式碼提供了我們正在使用的資料型態與相關操作介面 * 外部連結的 `Set` 變數: 透過變數名稱告訴使用者目前正在使用 set,實際上是帶有創建 set 物件所需的資訊。 * `void * add (void * set, const void * element)`: 將 element 加到 set 中,並回傳已經存在或是被加到 set 的 element。 * `void * find (const void * set, const void * element)`: 尋找 set 中是否有 element,有的話回傳,反之回傳 NULL。 * `void * drop (void * set, const void * element)`: 找到 set 中的 element 後將其移除。 * `int contains (const void * set, const void * element)`: 將 find() 的結果以 true/false 表示。 程式碼中大量使用 `void *`,這是因為它一方面可以去隱藏 set 實際實作的方式,另一方面它也讓我們幾乎可以傳遞任何東西到各個介面中,使我們的介面具有能應用在各種型態上的彈性。 * `void *` 會帶來一些安全上的問題,這部份的應對方式會在第八章做介紹。 ### Memory Management 除了定義 set 的介面以外,我們還需要以下取得 set 物件的介面: * `void * new (const void * type, ...)`: 接收描述型態資訊的描述符 (e.g., `Set`) 與用來初始化的不定數量參數,最後回傳指向資料表示方式與描述符相符的物件的指標。 * `void delete (void * item)`: 接收由 `new()` 產生的物件,並回收這個物件佔據的記憶體空間。 ### Object 由於 set 裡面可能會包含各種我們有興趣的物件,因此提供以下資料型態與介面: * `extern const void * Object`: 為了擴充性,因此用 `Object` 這個更大的集合 (不只是要放入 set 的 element,set 同樣也是一種物件)。 * `int differ (const void * a, const void * b)`: 用來比較物件是否相同。 * 這個函式可能會包含類似 `strcmp()` 的實作,也可能會透過回傳正負值來表示順序關係。 ```C= /* Object.h */ extern const void * Object; /* new(Object); */ int differ (const void * a, const void * b); ``` ## An Application 下方為根據上面提供的介面撰寫的應用: ```C= #include <stdio.h> #include "new.h" #include "Object.h" #include "Set.h" int main () { void * s = new(Set); void * a = add(s, new(Object)); void * b = add(s, new(Object)); void * c = new(Object); if (contains(s, a) && contains(s, b)) puts("ok"); if (contains(s, c)) puts("contains?"); if (differ(a, add(s, a))) puts("differ?"); if (contains(s, drop(s, a))) puts("drop?"); delete(drop(s, b)); delete(drop(s, c)); return 0; } ``` 以下為應用程式執行流程: 1. 創建了一個 set,並且新增兩個物件給它。 2. 測試 contains() 的功能。 * 輸出 ok 代表正常。 * 物件 c 沒有被新增到 set 中,因此輸出 contains? 代表不正常。 3. 測試 differ() 功能,同時確保 add() 在新增重複物件時不會重複新增,並會回傳已存在的同物件。 4. 測試 drop() 功能,確保它有成功將回傳的物件從 set 移除。 5. 測試 delete() 功能,包括能正常釋放物件佔用空間以及能正確處理 null pointer。 ## Implementation 實作的部分由於小弟我整理筆記的時候後面還有很多還沒看的章節,因此懶得重看這些比較瑣碎的部分。大家有興趣的話就麻煩自行去閱讀原作了🙏。 ### An Implementation — Set ### Another Implementation — Bag ## Summary 為了程式在實作上具備更大的彈性,可以透過抽象化資料讓應用程式無法得知實作細節 (e.g., 資料型態的組成方式)。應用程式只可以透過存取包含用來提供資料型態資訊的 descriptor pointer 與使用並回傳 generic pointer (`void *`) 的相關操作介面。 * descriptor pointer 可以搭配 new() 來產生指向物件的指標,並且產生的物件可以透過 delete() 釋放。 * 通常 descriptor pointer 包含的資訊至少會有表示資料所需空間大小的資訊。