ch4. 結構化程式設計 Edsger Wybe Dijkstra 於 1930 年出生在鹿特丹。他在第二次世界大戰期間倖存於鹿特丹的轟炸,以及德國對荷蘭的占領,並於1948年以數學、物理、化學、生物最高分從高中畢業。1952年3月,21歲的 Dijkstra 在阿姆斯特丹的數學中心找到工作,成為荷蘭第一位程式設計師。
1955年,已經當了三年的程式設計師,且同時身為學生的 Dijkstra 得出一個結論:程式設計的智力挑戰比起理論物理學還大。因此,他選擇了程式設計作為他的長期職業。
1957年,Dijkstra 與 Maria Debets 結婚。當時,在荷蘭結婚必須登記職業,荷蘭的公家機關並不接受 programmer 這個職業,他們從未聽過這樣的職業,為了滿足他們,Dijkstra 在職業欄中將自己定位為「理論物理學家」。
Dijkstra 與他的老闆 Adriaan van Wijngaarden 討論著以程式設計當作他的生涯志向,他認為沒有人會將程式設計視為一門學科或是科學,因此他認為自己可能不會被認真以待,然而他的老闆回答說,Dijkstra 將有可能成為那位發現新學科,以致於將軟體變成一門科學的人。
Dijkstra 在真空管時代開始了他的職業生涯,當時的計算機很巨大、脆弱、緩慢、不可靠、且極其有限。在早期,程式是二進制或是非常粗糙的組合語言編寫的,並以紙帶或打孔卡片作為輸入的這種物理形式存在,編輯/編譯/測試的循環就需要數小時甚至數天的時間。
正是在這個原始的環境造就了 Dijkstra 做出了他偉的的發現。
證明 Dijkstra 很早就發現,程式設計是一件難事,且程式設計師也不容易將它做好。任何複雜的程式都包含了太多人類大腦在沒有幫助下可以管理的細節。忽視一個極小的細節程式看似可以運作正常,但卻可以以出人意料的方式失敗。
Dijkstra 的解決方案是應用數學的證明法。他的願景是建立一個如同歐基里得的公理、定理、推論和引理的層次結構。Dijkstra 認為程式設計師可以像數學家一樣使用這樣的證明方法,換句話說,程式設計師應該要運用這些經過驗證的結構,並將它們與自己證明正確的程式碼相結合。
當然,為了使這一切開始進行,Dijkstra 意識到他必須要撰寫一些範例以展示如何用基本的證明方法來證明簡單的演算法,而他發現這是一件極具挑戰性的事。
在調查的過程中,Dijkstra 發現某些使用 goto 語句的情況會阻止模塊被遞迴地分解成更小的單元,從而阻礙了使用分而治之的方法進行證明的可能性。
然而,goto 的其他用途卻沒有這個問題。Dijkstra 意識到,「好的」goto 使用方法對應到簡單的選擇和迭代控制結構,例如 if/then/else 和 do/while。只使用這些控制結構的模塊才可以被遞迴的切割成可以被證明的單元。
Dijkstra 認識到,當這些控制結構與順序執行相結合是特殊的。在當時的兩年前,已經由 Böhm 和 Jacopini 證明了,所有程式都可以由三個結構建構而成:順序(sequence)、選擇(selection)、迭代(iteration)。
這個發現非常了不起:使一個模塊可證明的控制結構,正是構成所有程式的最小控制結構集合。因此,結構化程式設計應運而生。
Dijkstra 證明了順序語句可以通過簡單的列舉來證明其正確性。這種技術利用數學方法系統性地追蹤語句的輸入與輸出,與一般的數學證明無異。
Dijkstra 通過重新應用列舉法來解決選擇問題,對於選擇中的每條路徑都進行了列舉,如果所有的路徑都產生了適當的數學結果,那麼就表示證明是可靠的。
迭代則有些不同,為了證明迭代的正確性,Dijkstra 必須使用歸納法。他通過列舉證明了 1 得情況,然後他再次通過列舉證明了如果假設 N 是正確的,則 N+1 也是正確的,他也證明了迭代的起始條件與結束條件的正確性。
這樣的證明是費時且複雜的,但它們具備了證明的意義。隨著它們的發展,建立一個歐基里得式階層式的定理看似變得可行。
...
乾淨架構 設計(design)與架構(architecture)為何重要? 軟體架構的目標是將開發與維護軟體系統所需的人力最小化。 不好的設計會使維護的成本愈來愈高。 每次版本的發布時的生產力。 良好的開發模式(TDD)大幅減少開發時間。 行為(behavior)與架構(architecture) 行為:緊迫但並非特別重要 架構:重要不緊迫
∵ 緊急且重要 > 不緊急但重要 > 緊急但不重要 > 不緊急且不重要
∴ 大多情況下,架構(設計)比行為(開發)更重要。 程式設計範式(paradigms) 結構化程式設計(structed programming) 不要使用 goto,使用結構化的設計模式。(順序、選擇、迭代) 總結:對直接控制權施加限制。 關注點:功能 物件導向程式設計(object-oriented programming) 使用多型來避免函數指針的濫用。 總結:對間接控制權施加限制。 關注點:組件分離 函式程式設計(functional programming) λ演算的概念是不可變性,符號的值不會改變,意味著沒有賦值。 總結:對賦值施加限制。 關注點:數據管理 物件導向設計: 依賴反轉: 商業邏輯不依賴於 UI 與 DB,UI 與 DB 可以做為商業邏輯的插件。 小結: 三種範式都在約束你寫 code 的某些行為。這些約束就是在制定規則。 SOLID 設計原則 SRP: 單一職責原則(The Single Responsibility Principle) 一個模組只有一個原因(用戶/利益相關者)需要改變。 OCP: 開放封閉原則(The Open-Closed Principle) 軟體工程應對擴展開放,但對修改封閉。 LSP: 里氏替體原則(The Liskov Substitution Principle) 避免簡單的可替代性違規導致大量的額外機制。 ISP: 介面隔離原則(The Interface Segregation Principle) 關注點分離。將一個多功能的物件拆成繼承三個不同功能介面的物件。 DIP: 依賴反轉原則(The Dependency Inversion Principle) ...
乾淨架構(Clean Architecture) 筆記 分層 乾淨架構中從外而內依序為 Framework Layer Interface Adapter Layer Application Layer Domain Layer Models 一般來說會有四個 Models View Model(給前端) App Model(App Layer 隔離 Domain Layer 所用,aka DTO) Domain Model Data Model(for DBMS) Usecase App Layer 中的 Usecase 做四件事: 查 改 存 推 單向依賴原則 依賴的方向必為單向且為
\(\boxed{\text{Interface Adapter}} \rightarrow \boxed{\text{Application Layer}} \rightarrow \boxed{\text{Domain Layer}}\) Repository Application Layer 為了遵守單向依賴,與 ORM 解耦會做一次依賴反轉,翠取 Repository 介面。 套用乾淨架構的效益衡量 Model Mapping 的成本 vs. 獨立出「領域模型」的價值 省下更換技術的成本(migration cost) 「領域層」的部分通常會結合 DDD
ch3. 程式設計範式總覽 這個概述章節中包含的三種範式(paradigm)是結構化編程、物件導向編程和函數式編程。
結構化程式設計(Structured Programming) 第一個被採用的範式(但不是第一個發明的)是結構化程式設計,由艾德斯格·韋伯·迪科斯特拉(Edsger Wybe Dijkstra)在1968年發現。迪科斯特拉指出,無節制的跳躍(goto語句)對程式結構是有害的。正如我們在接下來的章節中將看到的那樣,他用更為熟悉的if/then/else和do/while/until結構取代了這些跳躍。
一句話總結結構化程式設計:
結構化程式設計對直接控制權的轉移施加限制
Structured programming imposes discipline on direct transfer of control.
物件導向程式設計(Object-Oriented Programming) 第二個採用的範式實際上是在1966年早兩年被奧利·約翰·達爾(Ole Johan Dahl)和克利斯登·奈加特(Kristen Nygaard)發現的。這兩位程式設計師注意到,在ALGOL語言中,函數呼叫的 stack frame 可以移動到 heap,從而使函數聲明的區域變數在函數返回後仍然存在。該函數成為一個類的構造函數,區域變數成為實例變數,嵌套函數則成為方法。這不可避免地導致了多態的發明,用以限制函數指針的使用。
一句話總結物件導向程式設計:
物件導向程式設計對間接控制權的轉移施加限制 Object-oriented programming imposes discipline on indirect transfer of control.
函數式程式設計 第三種範式,最近才開始被採用,卻是最早被發明的。事實上,它的發明早於程式設計本身。函數式程式設計是阿隆佐·邱奇(Alonzo Church)的工作的直接產物,他在1936年時發明了λ演算法(l-calculus),當時圖靈也在研究同樣的問題。他的λ演算法是基於9158年由約翰·麥卡錫(John McCarthy)發明的LISP語言,λ演算法有一個最基本的概念是不可變性(immutability),也就是說,變數的值不會改變。這意味著函數式程式設計並不會有賦值的敘述。事實上,大多數的函數式程式語言,有自己的方法去改變變數的值,但只有在非常嚴格的限制下可以使用。
一句話總結函數式程式設計:
函數式程式設計對賦值施加限制 Functional programming imposes discipline upon assignment
討論 注意到本章所介紹到的三個範式,都是在限制程式設計師的能力,而非增加新的能力。每個範式都在告訴我們什麼不應該做,而不是應該做什麼。
從另一角度來看,從結構化程式設計消除了go to語句,從物件導向程式設計消除了function pointers,從函數式程式設計消除了assignment。我們還有什麼可以消除的呢?
答案很可能是沒有。因此這三種範式很有可能是唯一的三種,至少是唯三限制型的範式,另一個證據是,在爾後的數十年間,也沒有再出現任何的範式。
結論 從範式的歷史,我們可以怎麼與架構做聯想呢?
1. 我們利用多型的機制來跨越架構的邊界。
2. 我們利用函數式程式設計來約束對數據的位置與訪問權限。
3. 我們利用結構化程式設計作為模塊的演算法基礎。
注意這三個與建築的三個重要關注點不謀而合:功能、組件分離、數據管理。
軟體架構始於程式碼,因此我們將從程式碼的角度開始討論架構,看看自從程式碼被寫下以來我們所學到的內容。
1938年,艾倫·圖靈(Alan Turing)奠定了計算機編程的基礎。他並不是第一個構想可編程機器的人,但他是第一個理解程式即數據(programs are simply data)的人。到了1945年,圖靈已經在真正的電腦上用我們現在能夠認出的程式碼編寫真正的程式了。這些程式使用了循環(loops)、分支(branches)、賦值(assignment)、子程序(subroutines)、堆棧(stacks)和其他熟悉的結構。但,圖靈的語言是二進制的。
自從那些日子以來,程式設計界發生了許多革命。其中一個我們都非常熟悉的革命就是語言的革命。首先,在1940年代末期,出現了組合語言(assemblers)。這些「語言」解放了程式設計師將他們的程式轉換成二進制的苦差。1951年,格雷斯·霍珀(Grace Hopper)發明了第一個編譯器 A0。事實上,她創造了「編譯器(compiler)」這個詞彙。Fortran 在1953年被發明出來。接著,一股源源不斷的新程式語言湧入 - COBOL、PL/1、SNOBOL、C、Pascal、C++、Java等等,無窮無盡。
另一個可能更重要的革命是在程式設計範式方面。範式是編程的方式,與語言相對無關。範式指導了開發人員應該使用哪些程式結構,以及何時使用它們。
迄今為止,已經有三種這樣的範式,也不太可能再有其它的範式,原因後述。
Ch2. 兩個價值維度 每個軟體系統都為利益相關者(stakeholders)提供兩種不同的價值維度:行為(behavior)與結構(structure)。軟體開發人員負責確保這兩種價值保持高水準。不幸的是,他們往往只關注其中一個而忽略另一個,更不幸的是,他們往往只關注較低價值的那一個,最終使軟體失去價值。
行為 歉體的第一個價值來自於行為。程式設計師被聘請來使機器以一種能為利益相關者帶來獲利或節省成本的方式運作。我們透過協助利益相關者制定功能規格或者需求文件,然後編寫程式碼,使利益相關者的機器滿足這些需求。當機器違反這些要求時,程式設計師便開始除錯以修復這些問題。
結構 軟體的第二個價值來自於結構,軟體之所以為「軟」體,是因為它被創造出來是為了方便改變機器的行為。為了實現其目的,軟體必須要有足夠的彈性,易於修改。當利益相關者對某項功能改變主意時,這種改變應該要是可以簡單且容易進行的。進行這種改變的困難程度應該只與改變的範圍成比例,而不是與改變的形式成比例。
然正是範圍和形狀之間的差異常常推動軟體開發成本的增長。這就是為什麼成本與所需求的變更規模不成比例。這也是為什麼開發前期比開發後期便宜得多的原因。
從利益相關者的角度來看,他們只是提供了一系列大致相似範圍的變更。從開發者的角度來看,利益相關者給予他們一系列拼圖碎片,他們必須將其放入日益複雜的拼圖中。每個新的要求都比上一個更難擬合,因為系統的形狀與要求的形狀不匹配。
我在這裡以一種非傳統的方式使用「形狀」這個詞,但我認為隱喻很貼切。軟體開發人員常常感覺自己被迫將方形木塊塞進圓洞中。
問題當然在於系統的架構。如果這個架構偏好某種形式,那麼新功能愈來愈難以適應這種結構。因此,架構應該盡可能地不受形式的限制。
更大的價值 行為與架構,何者提供更大的價值?對軟體系統來說,它的功能更重要,還是易於修改更重要?
如果你問企業經理,他們通常會說軟體系統的工作更重要。開發人員通常也會贊同。但這是錯誤的態度,我們可以用簡單的邏輯工具來證明它是錯誤的,那就是檢查極端情況。
如果你有一個完美運作但無法更改的程式,那麼當需求改變時它將無法運作,意謂著這個程式將變得沒有用處;然而一個不起作用但容易修改的程式,那麼我們可以透過簡單的修改使其運作起來,並在需求變化時保持運作,因此這個程式將持續保持有用。
也許這個論點對有些人來說不那麼具有說服力,畢竟沒有什麼程式是真的不能改變的。但是,有些系統在實際上是幾乎不可能改變的,因為改變的成本大過於了改變的好處。許多系統在某些功能或配置上達到了這一點。
如果你問企業經理們是否希望能夠進行變更,他們當然會說是,但可能會在回答中指出目前的功能比任何後續的靈活性更重要。相反地,如果企業經理們向你提出變更要求,而你估計的成本過高而無法負擔,他們很可能會對你允許系統達到變更變得不切實際的程度感到憤怒。
艾森豪威爾矩陣 下圖為艾森豪威爾(Dwight D. Eisenhower)總統的重要性(importance)與緊迫性(urgency)的矩陣,艾森豪是這麼說的:
我有兩種問題,一種是緊急的,一種是重要的。緊急的問題並不重要,而重要的問題從不緊急。
軟體的第一個價值 - 行為,是緊迫但並非總是特別重要。
軟體的第二個價值 - 架構,重要但從不特別緊迫。
當然,有些事情既緊急又重要,或有些事情既不緊急又不重要。故最終,我們可以將這四種問題安排成優先順序:
緊急且重要 不緊急但重要 緊急但不重要 不緊急也不重要
需要注意的是,程式碼的架構位於前兩個位置,而程式碼的行為則佔據第一與第三的位置。商業經理和開發人員常犯的錯誤是將第三位的事項提升至第一位。換句話說,他們未能將那些緊急但不重要的功能與真正緊急且重要的功能區分開來。這種失誤導致忽視系統的重要架構,而偏好系統中不重要的功能。
軟體開發人員面臨的困境是,商業經理無法評估架構的重要性。這就是軟體開發人員被聘用的原因。因此,軟體開發團隊有責任強調架構的重要性,而不是功能的緊迫性。 為架構而戰 履行這個責任意味著投入一場戰鬥,或者也許更好的詞是「奮鬥」。坦白說,這些事情總是這樣做的方式。開發團隊必須為他們認為對公司最好的事情而奮鬥,管理團隊、市場團隊、銷售團隊和運營團隊也是如此。這總是一場奮鬥。
有效的軟體開發團隊會毫不畏懼地與其他利益相關者平起平坐地爭論。請記住,作為一名軟體開發者,您也是一個利益相關者。您需要保護您所需的軟體。這是您的角色和責任的一部分,也是您被雇用的重要原因之一。
如果你是一位軟體架構師,這個挑戰對你來說尤其重要。軟體架構師根據他們的工作描述,更關注系統的結構而非其特性和功能。架構師創建一個架構,使得這些特性和功能能夠容易地開發、修改和擴展。
只要記住:如果架構放在最後,那麼系統的開發成本將會愈來愈高,最終對系統的某個部分或整個系統的變更幾乎變得不可能。如果這種情況發生,那意味著軟體開發團隊沒有為他們知道的必要事項進行足夠的努力。
Ch1. 設計與架構到底是什麼? 對初學者而言,設計(Design) 與架構(Architecture) 基本上是沒有差別的。
「架構」這個詞常常用在高層次的情境,而與低層次的細節脫節;而「設計」則更常用於暗示著低層次的結構和決策。但事實上底層的細節與高層次的架構往往是伴隨而生的。(作者以建築設計作為範例,在建築設計圖中,會包含房屋形狀、外觀設計、高度、房間佈局等等,但同時也具備大量的設計細節,如每個插座、開關以及每個電燈具體的安裝位置,甚備某個開關與所控制的電燈的具體連接訊息等等。)
總而言之,底層設計細節和高層架構資訊是不可分割的。它們共同定義了整個軟體系統,缺一不可。所謂的底層和高層本身就是一系列決策組成的連續體,並沒有清晰的分界線。
目標是什麼? 軟體架構的終極目標是,用最小的人力成本來滿足構建和維護該系統的需求。 一個軟體架構的優劣,可以用它滿足用戶需求所需要的成本來衡量。如果成本很低,並且在系統整個生命週期內一直都能維持這樣的成本,那麼這個系統的設計就是優良的。反之如果該系統的每次發布都會提升下一次變更的成本,那麼這個設計就是不好的。
案例分析 下面為書中的一個真實案例,該案例中的數據均來源於一個匿名的真實公司。
該公司的工程人員數量的增長 由圖可見,人員的增長肯定是顯示了產品重大的成功。 2. 該公司同一時段的生滻力 從第二張圖可以發現一些端倪,雖然開發者愈來愈多,但程式碼的增長似乎接近了一個漸近線。 3. 隨著時間推移每行程式碼的成本 從第三張圖可見,成本快速的增加,大量地消耗利潤,將公司推向停滯,甚至是完全崩潰的境地。
凌亂系統的特點 當系統匆忙地拼湊在一起,當程式設計師的數量成為唯一的驅動力,而沒有考量程式碼的整潔度與設計結構時,必定會走向醜陋的結局。
每次版本發布的生產力 這張圖顯示了開發人員對這條曲線的看法。一開始的生產力接近100%,但隨著每次發布,生產力逐漸下降,最後趨近於零。
從開發者的角度來看,真是令人沮喪,因為每個人都在努力工作,但實際上已經無法完成更多。所有努力都被轉移到處理混亂上了,而不是開發新功能。
管理層視角 每次版本發布時的薪資支出 由圖明顯可見,後期投入的資金幾乎沒有帶來任何東西。但其中發生了什麼問題呢?
問題到底在哪裡? 現代的程式開發者,大腦中有一部分是在沉睡的,儘管它們知道「好的、乾淨的、設計良好的程式碼是重要的」。
這些程式開發者通常相信一個熟悉的謊言:「我們可以之後再整理,我們現在更需要趕快上線。」實際上是,在未來,程式從來不會被整理,因為市場上的壓力永遠不會減輕。所以開發人員從來不會切換模式,他們無法回頭整理事情,因為他們被逼著完成下一個任務,一而再再而三。於是混亂愈來愈多,生產力逐漸趨近於零。
程式開發者還相信一個更大的謊言:「沒有秩序的程式碼可以讓他們在短期內快速前進,且只會在長期才會反應出速度的變慢」,他們認為可以在未來的某個時刻從製造混亂轉換成整理混亂,但事實是,無論使用哪種時間尺度,製造混亂總是比保持整潔更慢。
完成任務所需的時間(With TDD/No TDD) 圖6是傑森戈爾曼(Json Gorman)進行的一個實驗。傑森在六天的時間內反覆進行這項測試,每一天他會完成一個整數轉羅馬數字的簡單小程式,當他通過了他預定義好的ATDD(Acceptance Tests),他可以清楚知道他完成了程式。在過去六天內,每天的任務都花不到30分鐘。在第一、第三、第五天使用了 TDD(Test Driven Development),而另外三天則沒有遵守。結果顯示,後期工作完成速度比前期快,而在實施 TDD 的日子裡,工作進展大約比沒有實施的日子快了 10%,即使是最慢的TDD日子也比非TDD的日子還快。
結論 在每一種情況下,最好的選擇是開發組織要認識到並避免自己的過度自信,並且開始認真對待軟體架構的質量。
要認真對待軟體架構,你需要知道什麼是良好的軟體架構。為了構建一個設計和架構能夠最大程度減少工作量並提高生產力的系統,你需要知道哪些系統架構的特性能夠達到這個目標。
這本書就是在談這個,它描述了好的乾淨架構和設計的樣貌,讓軟體開發者能夠建立具有長期盈利生命力的系統。
採用好的軟體架構可以大大節省軟體項目構建與維護的人力成本。讓每次變更都短小簡單,易於實施,並且避免缺陷,用最小的成本,最大程度地滿足功能性和靈活性的要求。
Clean Architecture
A Craftsman’s Guide to Software Structure and Design
中文翻譯: 無瑕的程式碼 - 整潔的軟體設計與架構
原著: Robert C. Martin(Uncle Bob)
目錄 第一部分 概述 第1章 - 設計與架構到底是什麼 第2章 - 兩個價值維度 第二部分 從基礎構件開始: 程式設計範式 第3章 - 程式設計範式總覽 第4章 - 結構化程式設計 第5章 - 物件導向程式設計 第6章 - 函數式程式設計 第三部分 設計原則 第7章 - SRP 單一職責原則 第8章 - OCP 開放封則原則 第9章 - LSP 里氏替換原則 第10章 - ISP 介面隔離原則 第11章 - DIP 依賴反轉原則 [第四部分 組件構建原則)(/clean_arch/sec4) 第12章 - 元件 第13章 - 元件聚合 第14章 - 元件耦合 第五部分 軟體架構 第15章 - 什麼是軟體架構 第16章 - 獨立性 第17章 - 劃分邊界 第18章 - 邊界剖析 第19章 - 策略與層次 第20章 - 業務邏輯 第21章 - 尖叫的軟體架構 第22章 - 整潔架構 第23章 - 展示器和謙卑物件 第24章 - 不完全邊界 第25章 - 層次與邊界 第26章 - Main 元件 第27章 - 服務: 宏觀與微觀 第28章 - 測試邊界 第29章 - 整潔的嵌入式架構 第六部分 實現細節] 第30章 - 數據庫只是實現細節 第31章 - Web 是實現細節 第32章 - 應用程式框架是實現細節 第33章 - 案例分析: 影片銷售網站 第34章 - 拾遺