If you can’t describe what you are doing as a process, you don’t know what you’re doing.

— W. Edwards Deming (attr)

核心概念#

所有程式都在轉換資料——將輸入轉換為輸出。然而當我們思考設計時,我們很少想到創建轉換。我們擔心的是類別和模組、資料結構和演算法、語言和框架。

作者認為這種對程式碼的聚焦經常偏離了重點:我們需要回到將程式視為將輸入轉換為輸出的思維。當我們這樣做時,許多之前擔心的細節都會消失。結構變得更清晰、錯誤處理更一致、耦合大幅降低。

Tip 49 - Programming Is About Code, But Programs Are About Data(程式設計關乎程式碼,但程式關乎資料)

Unix 管線:最早的轉換#

1970 年代的 Unix 程式設計師會這樣思考:列出目錄樹中最長的五個檔案。

$ find . -type f | xargs wc -l | sort -n | tail -6 | head -5

這是一系列的轉換:

  • 目錄名稱 -> 檔案列表 -> 帶行數的列表 -> 排序後的列表 -> 最高五個 + 總計 -> 最高五個

就像工業生產線:一端送入原始資料,另一端出來成品(資訊)。

尋找轉換#

有時找到轉換最簡單的方式是從需求開始,確定其輸入和輸出。現在你已經定義了代表整個程式的函式。然後你可以找出從輸入到輸出的步驟。這是一種由上而下(top-down) 的方法。

範例:字謎搜尋器#

輸入是一組字母,輸出是按長度分組的單字列表。整個程式分解為四個獨立的轉換:

  1. 所有三個字母以上的組合
  2. 組合的簽章(sorted letters)
  3. 所有匹配簽章的字典單字
  4. 按長度分組

每個轉換可以再分解為更小的轉換(Transformations All the Way Down)。

管線運算子 |>#

Elixir 等函數式語言有 pipeline operator(管線運算子)。它只是把左邊的值插入作為右邊函式的第一個參數:

"vinyl" |> String.codepoints |> Comb.subsets()

等同於:

Comb.subsets(String.codepoints("vinyl"))

使用管線意味著你自動以轉換資料的方式思考——每次看到 |>,你實際上看到的是資料從一個轉換流向下一個的地方。

許多語言有類似的功能:Elm、F#、Swift 有 |>,Clojure 有 ->->>,R 有 %>%,Haskell 有管線運算子。即使你的語言沒有管線,你仍然可以用一系列賦值語句來表達轉換思維。

為什麼這很棒#

將主函式串在一起:

word
|> all_subsets_longer_than_three_characters()
|> as_unique_signatures()
|> find_in_dictionary()
|> group_by_length()

這就是滿足需求所需的轉換鏈,每個從前一個轉換接收輸入並傳遞輸出到下一個。這幾乎是你能得到的最接近文學式程式碼(literate code)的東西。

Tip 50 - Don’t Hoard State; Pass It Around(不要囤積狀態;傳遞它)

在轉換模型中,不是把一小堆一小堆的資料散佈在整個系統中,而是把資料想像成一條大河、一個流(flow)。資料成為功能的夥伴:管線是一系列 code -> data -> code -> data...。資料不再綁定於特定的函式群組(如類別定義中那樣),而是自由地代表應用程式將輸入轉換為輸出的展開過程。

這意味著我們可以大幅降低耦合:一個函式可以在任何其參數匹配另一個函式輸出的地方使用(和重用)。

錯誤處理#

如果我們只能建構線性管線,要怎麼加入錯誤處理的條件邏輯呢?

基本慣例是:我們永遠不在轉換之間傳遞原始值。而是將它們包裝在一個資料結構(或類型)中,該結構也告訴我們包含的值是否有效。在 Haskell 中這叫 Maybe,在 F# 和 Scala 中叫 Option

兩種處理方式#

  1. 在每個轉換內部處理:每個函式接受 {:ok, value}{:error, reason} tuple,如果收到 error 就直接傳遞
  2. 在管線中處理:使用 and_then 這樣的 bind 函式——它接受包裝的值,如果是 :ok 就解開並套用函式,如果是 :error 就直接回傳
def and_then({:ok, value}, func), do: func.(value)
def and_then(anything_else, _func), do: anything_else

轉換改變程式設計#

以一系列(巢狀的)轉換來思考程式碼,是一種解放式的程式設計方法。習慣需要一段時間,但一旦養成這個習慣,你會發現程式碼變得更乾淨、函式更短、設計更扁平。

相關章節#

  • Topic 8,好設計的本質
  • Topic 17,Shell 遊戲
  • Topic 26,如何平衡資源
  • Topic 28,去耦合
  • Topic 35,Actor 與 Process