API 設計— 命名之術

相信很多人都知道,寫程式的時候,多數的時間,我們都是在想變數名稱、函數名稱、類別名稱。

由於軟體正式開始使用後,不管是內部使用或是開放專案,都有外部依賴,一旦用了不好的名稱,往後修改,是相當不便,而且會破壞外部軟件的穩定度。

2010 年的 jQuery,主張了 Less is more 的概念,從 jQuery 的 API 設計,多少可以學到一些如何設計簡單易用 API 的概念。然而,不同的程式語言、編譯器都有各自適用的設計模式,不見得會通用,需要對其效能、語法限制作斟酌。 所以,Sometimes, less is more, but sometimes, less leads to confusion.

筆者雖不算相當資深,但覺得多年的設計經驗應該找個時間,統合組織一下,寫出來 (所以這篇其實藏在草稿裡很久了 ….. XD)

##資料與狀態

最常見的糟糕變數命名,很顯然的就是 a1, a2, i1, i2, j1, j2, r1, r2, 這類毫無任何含意的變數名稱(拜託,我們不是在寫 LLVM bitcode 啊,你又不是編譯器)

好吧,再進步一點,變成 product1, product2, book1, book2,好像好多了?

但事實上,有誰能從字面上分得出 product1 跟 product2 的差別?這類的變數名稱只有在一個情境下有意義,譬如:從一個 product list 中,其中符合條件第一項與第二項的 product。

其實,程式設計就是在處理資料與狀態,用來儲存資料的變數,很明顯的,因此有狀態、範圍之區分,只要掌握這個概念,基本上就不會取出令人匪夷所思的變數名稱了。

譬如,一份產品清單內,有已經上架的產品以及尚未上架的產品,其變數名稱很明顯就可命名為: $publishedProducts 或為 $unpublishedProducts,由其情境下的狀態、數量去構成。

語意是變數名稱中最重要的一部份,”published”, “unpublished” 意味著產品的狀態,所以最簡單最簡單的 pattern,就是 Adj. + N. 就可以是一個清楚的變數名稱。

單數複數表示也很重要,當我們採用複數命名,其意味著該變數很可能是一個 array 類型、collection 類型、traversable 類型的資料。

不過,如果你在同一個函數下,不需要同時處理兩種狀態的產品清單,那麼其實狀態描述多半可以省略(除非其情境有可能造成混淆才需要區分),否則多半可以寫成如下代碼:

function processProductStats() {
    $products = getPublishedProducts();
}

如上,函數名稱已經很明顯的告訴讀者,這邊取得的產品,是已經 published 的產品。

而同時處理兩種狀態的產品清單,寫成如下代碼,就很容易閱讀:

function processProductStats() {
    $publishedProducts = getPublishedProducts();
    $unpublishedProducts = getUnpublishedProducts();
}

會比下方代碼,好得多:

function processProductStats() {
    $p1 = getProducts(true, 1, 10);
    $p2 = getProducts(false, 2 , 10);
}

不蓋你,真的有人這樣寫,真心不騙。

再複雜一點,當你的資料有不同關係時,也可以加上 prep. of 作為區分,譬如:

$publishedProductsOfUser, $publishedProductsOfAdmin

如上,意味著 published product 區分為 user product 或 admin product。

如果同一個 Context 內,要標示不同的資料來源,可以用 “from”

$userFromFacebook, $userFromTwitter

當然,也可以直接簡化為 $twitterUser 或 $facebookUser,”from” 只是最為一個當你不知道如何命名時,最安全的一個手法。

而匈牙利命名法也不是全然的有病,在某些場合下,其實相當有用,譬如現代框架常用的 ORM 會提供 Collection 這類的物件,而 Collection 可以轉成Model 的陣列資料,如下:

$products = new ProductCollection;
$items = $products->items(); // 實際上是 Product[] 而非 array of array

如上場合使用 $items 其實沒有什麼大問題,但多數時候,我們會處理多個 Collection:

$products = new ProductCollection;
$categories = new CategoryCollection;

全部轉成 array of model record 呢?

$products = new ProductCollection;
$categories = new CategoryCollection;
$productItems = $products->items();
$categoryItems = $categories->items();

好,目前都沒有什麼大問題,不過我們現在遇到一個狀況,就是需要將資料組合後,以 JSON 格式送出,而 encode_json 只吃 array 型別的資料,所以我們需要將 Array of model record 轉成 array of array,怎麼辦?程式碼很可能變成這樣:

$products = new ProductCollection;
$categories = new CategoryCollection;

$productItems = [];
foreach ($products as $product) {
    $productItem = $product->toArray();
    $productItem['types'] = $product->types->toArray();
    $productItems[] = $productItem;
}
$output = [
    'products' => $productItems,
    ......
];

上面這段程式碼,看起來沒有什麼大問題,不過他違反了一個規則 — Consistency 一致性

上上段程式碼,我們使用 suffix -items 作為 array of model records 表示,但這邊,我們卻使用 suffix -item 以及 suffix -items 作為 array of array 的表示,如果先看第一段代碼,再看這段代碼,就會知道容易造成混淆的地方為何了。

整理一下,加上型別名稱,是否比較清楚?

$products = new ProductCollection;
$categories = new CategoryCollection;

$arrayProducts = [];
foreach ($products as $product) {
    $arrayProduct = $product->toArray();
    $arrayProduct['types'] = $product->types->toArray();
    $arrayProducts[] = $arrayProduct;
}
$output = [
    'products' => $arrayProducts,
    ......
];

這邊,取名為 $arrayProduct 以及 $arrayProducts 的主要原因,是為了強調其型別為 array,因此作為 prefix 放在 N 的前面,同樣的 $productArray 或是 $productsArray 也是可行,或甚至更清楚的名稱也是可行,如 $arrayOfProductArray 以及 $productArray… 等,同樣都達到了型別標示的目的。 ##常見縮寫

而常見變數名稱如 $i 其實是 $index 的縮寫,所以也不用大驚小怪,而 $j 則是常見用來作為第二層迴圈的索引值,這幾個用法,都是相當通用,所以也不用避諱。

$a, $b ??

隨著 functional 寫法越來越普遍,多數陣列處理也會用上 comparator,譬如 sort(), 其中 $a , $b 多半用來表示兩邊不同的變數,這個也蠻常用的,算是很簡潔的寫法,畢竟 comparator 函數內容通常很精簡,不太需要過度清楚的區分不同變數。

##操作資料

目前為止以上概念都很簡單,我們現在懂了變數命名,更進一步的,就是撰寫操作這些資料的代碼,而操作這些資料,我們需要函數,因此函數名稱是比變數名稱更重要的,因為它不僅僅是操作資料,也是對外公開表示:「Hey I can do this」

如果你取錯名稱,在下一個版本把它改了名字,套件升級的使用者只會覺得憤怒,因為你讓他們的軟體增加了不穩定性… XD

你能想像當你走進一家政府機構辦事,該路上的指示牌到告訴你 XXX 事務請往右走到底,第一次很順利,隔年你又去辦事,跟著指示牌走到底,結果居然換位置了?當然很怒啊。 所以這個指示牌有沒有更新,就很重要,在軟體裡面我們可以把它叫做 Changelog 或 Migration,通常會以 CHANGELOG 或用 MIGRATION, UPGRADE 命名…之所以全部用大寫,是因為小寫的話沒人看,最後作者怒了,全部都用大寫,其意義就是「拜託你給我看這個文件」

當然,Migration, Upgrade 越少越好,雖然我們沒辦法一次就看到好的設計,但我們至少可以在一開始的時候,把初步的設計做好。 函數命名

在講函數命名之前,我想可以先來簡單介紹一下,幾個通用性的動詞,基本上理解這幾種動詞背後所隱含的意味,函數命名就不會錯太多。

以下幾個 prefix- 不是絕對的限制,只是一個通用、常用的準則,用以參考:

get-

通常類別方法以 get- 開頭,意味著我們要取得某項屬性或數值,很顯然的,我們沒辦法透過屬性直接存取,所以才需要 get- ,而 get- 最重要的一點,就是他「只做簡單回傳,不作修改」,也因此,一個 get method 不應該修改物件的狀態、修改物件內容、也不該調用複雜資料庫查詢

除非整個類別是專為遠端 API 設計,其 get- 才比較 make sense,不過也須注意,類別本身屬性的取得和遠端資料取得都使用 get- ,很可能會造成混淆。譬如:

$client = new MyAppAPI;
$client->getHost();    // 好像沒有 API query?
$client->getUserId();  // 到底有沒有 API query 呢?
$client->getStories(); // 到底有沒有 API query 呀!!???!

如上範例,使用 fetch- prefix 改為如下代碼,可能清楚很多:

$client = new MyAppAPI;
$client->getHost();
$client->fetchUserId();
$client->fetchStories();

get- prefix 意味著,輕巧的、快速的拿到某個資料… 以此類推,set- 也是一樣。

不過,如果是單純的函數,而非類別方法,則 get- 就無此限制,命名方式較為彈性。

set-

通常被用在設定屬性,與 get- 一樣,不該用在資料庫操作。我們在另外一篇文章「建構子設計之道」有討論 set- 正確的使用時機。

query-

上面說的 get- 不該調用遠端 API 或資料庫操作,讀者肯定心中充滿了疑問,「什麼?不用 get? 我看你才不懂吧」

事實上,還有一個 prefix 很好用,就是 query-,query 意味著查詢,而且是帶有條件的,所以使用者可能會期望 query- method 有條件參數可以傳遞。查詢有很多種含義,不管是 database query, api query 都算是 query,當開發者查看 API 規格,甚至只看得到 caller 怎麼呼叫,”query-” 很明顯的就可以讓開發者瞭解 — 喔,這個可能得花一點時間,而且可能會有一些 ”溝通” 要做

do-

do- 列在這邊,實際是常見的錯用 prefix- ,很多時候會看到有人寫 doUpdate, doQuery, doKill 這類包含 do- prefix 的函數名稱,但其實 do- 很沒意義。 如果你只是為了區分 doUpdate 以及 update 這兩個函數名稱,不如就用底線開頭來隱藏另外一個實作吧 _update, update 表面上看起來,很明顯的 update 才是真正的開放 API,而 _update 的 scope 建議應該為 private 或 protected。

fetch-/store-

fetch- 與 store- 通常是對應的,比 query 更模糊一點的是,fetch- 通常不帶有複雜條件,譬如 fetchProduct, fetchBooks, fetchUserStories 等等,對象通常是資料庫,當然場景需求不同,沒有一定要如此限制。

而當 “fetch”, “store” 同時出現在同一個類別,就可能需要考慮到一致性,因為使用者可能預期 “fetch”, “store” 是對同一個 storage 操作,不該 fetch from A, 但 store to B.

build-

通常用在 — 給予某些參數,建置某個結構或物件。 筆者常用的是 — 給予某些簡單的參數,建置某一用途的 Config 物件或 Map,譬如 buildReactProducctAppConfig。 build- 也可能意味著,建置資料庫內某種用途的資料,譬如 buildBaseData、buildUserData,但其意義與包含此函數的類別名稱有關。

create-

通常被用在 ORM Record 建置,如 ActiveRecord Pattern 常使用這個 create() method 來建置新的 Record。 create- 與 build- 很像,但就筆者看來,這兩者不同的地方在於,create- 有明顯目標跟範圍,譬如 Product::create().. Book::create(), createOrder() … 等等,而 build- 如 buildBaseData 等,不是這麼明確。(但也有可能有筆者沒有想到的案例,若有的話,之後補上 XD)

new-

new- 其實與 create- 不同,new- 很明顯的只意味著建構一個類別的實體化,取名以 new- 開頭,那麼隱含的操作應該不包含資料庫操作。

remove-, delete-

remove- 與 delete- 其實差異也不是太明顯,但就筆者來看,remove- 意味著從某個資料集,移除掉這個項目,這個 “移除” 很可能只是移除關聯性,並不會刪除實體資料,而 delete- ,很明確的意味著是實體資料的刪除。

traverse-

在處理樹狀資料時,traverse- 很好用,traverse- 意味著會遍歷一整個樹,常用在處理 AST 的部分。

下集待續

(也不知道有沒有下集….先寫到這邊,不然等到寫完也不知道什麼時候了。)


书籍推荐