Try   HackMD

Clean Architecture 無瑕的程式碼 (Ch1-Ch5) 筆記

整潔的軟體設計與架構篇


Ch1. 什麼是設計與結構

高層次的結構 & 低層次的細節都是同一個整體的一部分
以家為例:
高層次的結構:外觀、挑高和空間及房間的佈局
低層次的細節:每個插座、燈的開發和燈的位置,哪些開關控制的哪些燈,爐子、熱水器的位置

軟體架構的目標是最小化建置和維護「需求系統」所需要的人力資源

設計品質的度量標準,不過就是度量滿足客戶需求所花的精力(effort)。如果這種精力是少的,並且在系統整個生命週期,都保持是少的,那這就是好的設計。如果effort隨著新版本的增長而增加的話,那這個設計就是不好的。

龜兔賽跑的故事 (爛攤子產生的故事)

  • 過度自信的愚蠢。
  • 開發人員常陷入一個熟悉的謊言:「我們可以晚點整理它,但我們只能先上市!」
  • 事情永遠不會被清乾淨,因為市場壓力永遠不會減少

總結

  • 避免過度自信,認真對待軟體架構的品質
  • 你需要知道什麼是好的軟體架構,而這就是本書的內容

Ch2 兩種價值觀的故事

每一個軟體系統都會為利益相關者提供兩種不同價值

  1. 行為(behavior)
  2. 結構(structure)

軟體開發人員有責任確保這些價值都保持在高價值。但往往大家只會把重點放在其中一個,而且還是兩個之中價值比較小的那個,使得軟體系統最終變得沒價值

行為

透過幫助利益相關者制定功能規格(functionality specification)或需求文件(requirements document)來做到這一點。當機器違反這些需求時,程式設計師debug來解決問題

自我解讀:做出功能,可以運作,符合期待。

架構

software, soft + ware(=product)。軟體被叫做軟的(soft),代表他是可以輕易改變機器行為的一種方式,如果期望機器的行為難以改變,我們會將之稱為硬體(hardware)

軟體必須很容易改變。當利益相關者改變他們的想法時,這個改變應該簡單就能輕易做到。

誰的價值更高?

立場不同,觀點不同
業務經理 vs. 開發人員

  • 業務經理: 系統能夠工作比較重要
  • 開發人員:經常跟著業務經理的態度去做,但這不對

Eisenhower Matrix

Urgent Not Urgent
Important 1. 迫切且重要 2. 不迫切但重要
Unimportant 3. 迫切但不重要 4. 不迫切也不重要

程式碼的架構: 1., 2.
程式碼的行為: 1., 3.

  • 業務經理和開發人員經常犯的錯誤是把3.的項目提升到1.
  • 軟體開發人員的困境是業務經理沒有能力去評估架構的重要性。而這就是軟體開發人員該去做的

為架構而戰

作為一名軟體開發人員,你就是一個利益相關者。你是你需要維護的軟體當中的一份子。這是你的角色的一部分,也是你的職責之一

如果架構最後才出現,那麼開發系統將變得更昂貴

Ch3 範式概述

  • 結構化程式設計 (structured programming)
  • 物件導向程式設計 (object-orient programming)
  • 函數式程式設計 (functional programming)

結構化程式設計 (structured programming)

Dijkstra 證實使用無限制地跳躍(goto語句)對於程式的結構是有害的。他使用更為人所熟悉的 if/else 和 do/while/until 來建構程式取代goto

結構化程式設計在直接的控制轉移上加上規範

物件導向程式設計 (object-orient programming)

所設計的堆疊框架(stack frame)可以被移動到一個累堆(heap)中,這樣就能允許函式宣告的局部變數在函式返回後仍然存在很長的一段時間。該函式成為了類別的建構函式(constructor),局部變數變成了實例變數(instance variable),內部的巢狀函式則變成了方法(method)。而透過有紀律地使用函式指標,就必然會發現多型性(polymorphism)

物件導向程式設計在間接的控制轉移上加上規範

函數式程式設計 (functional programming)

l-calculus的基本概念是不變性(immutability)-也就是說,符號的值不會改變。實際意味著一個函數式語言沒有賦值語句(assignment statement)。事實上大多數函數式語言都有一些方法可以改變變數的值,但只能在非常嚴格的規範下進行

函數式程式設計在賦值上加上規範

Thinking

上面每個範式都移除了一些功能、強加一些額外的規範,意圖是消極的。範式告訴我們不該做什麼,而不是告訴我們該做些什麼

Summary

範式的歷史就是架構所有的一切。我們使用多型作為跨越架構邊界的機制;使用函數式程式設計對資料的位置和存取進行了規範;使用結構化程式設計當作模組演算法的基礎

Ch4 結構化程式設計 (structured programming)

功能分解

  • 結構化程式設計允許將模組遞迴地分解成可證明的單元,這意味著模組可以從功能上進行分解。你可以將一個大規模的問題陳述,分解成高級別功能。這些功能的每一個都可以再被分解為低一級的功能,而且可以無限次地做下去

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

依靠科學

科學與數學在根本上是不同的,因為科學的定理和定律無法用證明來表達其正確性。
科學定理和定律的本質:他們是可證偽的(falsifiable),但不是可證明的
科學並非是要證明陳述是正確的,而是要證明陳述是錯誤的。那些我們經過很多努力仍無法證明它是虛假的陳述,我們就認為那就是足夠真實的陳述。

> 科學家的思考模式

並非所有陳述都是可證明的。e.g.,「This is a lie」既不真實也不虛假
數學是要證明「可證明陳述是真實的」。相反地,科學是要證明「可證明陳述是虛假的」

測試

Dijkstra曾說:「測試顯示了錯誤的存在,而不是沒有錯誤」。換句話說,一個程式可以用測試來證明它是不正確的,但不能因此證明它是正確的。

我寫的測試去測我寫的功能,只能確保錯誤的時候經過測試被檢查發現。但不能確認100%正確。

Ch5 物件導向程式設計

Q: 物件導向(OO)是什麼?
A: 資料和函式(功能)的結合

有人回頭找了三個神奇的詞來解釋物件導向的本質:封裝(encapsulation)、繼承(inheritance)和多型(polymorphism)

封裝 (Encapsulation)

OO語言提供簡單又有效的資料和函式(功能)封裝。一組具有緊密關聯的資料和功能匯出一個框。對於框外,資料是隱藏的,只有一些函式(功能)是已知的。

e.g., 咖啡機的概念,提供按鈕選擇:熱水、黑咖啡、Latte、Espresso、Cappuccino,是水、咖啡豆、牛奶的排列組合,你不知道機器怎麼調製咖啡,只能透過機器提供的品項選擇咖啡。

繼承 (Inheritance)

繼承就是在一個封閉範圍內重新宣告一組變數和函式。

e.g., 風味咖啡機,在既有的咖啡機上增加風味選項,焦糖、蜂蜜和可可,繼承了原本咖啡機的基本款咖啡,又多了新功能風味咖啡選項。

多型 (Polymorphism)

不同的對象可以對同一個方法做出不同的反應

e.g., 飲料製作機,可以依據主要原料搭配不同比例的水與牛奶。搭配不同原物料就可以產生不同飲料。
咖啡機和奶茶機都繼承了飲料製作機的功能,能夠搭配水和奶的比例,煮出不同飲品。
咖啡豆+水=美式咖啡,紅茶葉+水=紅茶;
咖啡豆+水+牛奶=拿鐵,紅茶葉+水+牛奶=奶茶

多型的威力

我們在1950年代後期了解到,我們的程式應該要獨立於設備。因為我們邊寫了很多依賴於設備的程式之後,卻發現我們其實是希望那些程式做相同的工作,只是使用不同的設備

依賴反向

不希望我們的飲料製作機只能使用特定的咖啡粉,所以我們制定了一個介面(interface)定義了主原料的特性,如粉狀物之類的,然後當這台咖啡機想轉成泡茶機使用時,不用重新建一台泡茶機,直接改變投入茶粉就好

Ch6 函數式程式設計

不可變與架構

為什麼架構師會關心變數的可變性?答案是:所有競爭條件(race condition)、deadlock條件(死結條件)和平行更新問題(concurrent update problem)都來自於可變的變數。

如果沒有可變的變數,我們在平行化應用程式面臨的所有問題(我們在需要多執行緒和多處理器的應用程式中面臨的所有問題)都不會發生了

e.g.,
假設你要準備早餐,你需要做以下幾件事情:煮咖啡、煎蛋和烤麵包。在傳統的命令式編程中,你可能會這樣做:

1. 準備咖啡機。
2. 打開咖啡機,煮咖啡。
3. 準備鍋子和電爐。
4. 打開電爐,加熱鍋子。
5. 打開冰箱,取出蛋、麵包等食材。
6. 打開電爐,加熱鍋子,倒入油。
7. 打開冰箱,取出麵包,放入烤箱。
8. 等待食材加熱。
9. 將煮好的咖啡倒入杯子中。
10. 將煎好的蛋和烤好的麵包放入盤子中。
11. 完成早餐準備。

這是一種命令式的步驟式編程,你需要明確指定每個動作的順序和細節。

而在函數式編程中,你可以將這些動作表示為函數,並通過組合這些函數來完成早餐的準備。例如:

函數:煮咖啡()
函數:煎蛋()
函數:烤麵包()

組合函數:
準備早餐 = 煮咖啡 + 煎蛋 + 烤麵包

函數式程式設計強調對函數的純粹性(Pure Function)、不可變性(Immutability)和避免副作用(Side Effects)。這意味著函數的輸出只取決於輸入,不會修改外部狀態,也不會產生隨機的副作用,這有助於減少程式中的錯誤並提高可讀性。

函數式程式設計還包括一些特性,如高階函數(Higher-Order Functions)、遞迴(Recursion)、匿名函數(Anonymous Functions)、函數組合(Function Composition)等,這些特性可以使程式更簡潔、模組化並提高代碼的重用性。

因此,函數式程式設計是一種以函數為核心的程式設計風格,它強調純函數和不可變性,並通過這些特性來構建更容易理解、測試和維護的程式。

總結:軟體就是由循序、迭代循環(iteration)和間接(indirection)所組成的電腦程式內容