“架構”這個詞彙讓人聯想到權力和神秘,並且讓人感覺是在進行重要決策和技術分析。軟體架構被視為技術成就的巔峰,軟體架構師則是被尊敬和羨慕的對象,許多年輕的工程師都夢想成為軟體架構師。然而,什麼是軟體架構?軟體架構師的具體職責和工作時間又是什麼呢?
首先,軟體架構師應該是最優秀的程式設計師,他們不僅要寫程式碼,還需引導團隊達到最大化生產力的設計目標。軟體架構的本質是將系統劃分為組件,安排其排列和通信方式,以便於開發、部署、運行和維護。好的架構設計策略是盡可能長時間保留多種選項。雖然架構的首要目的是確保系統正常運行,但更重要的是支撐系統的整個生命周期,使其易於理解和維護。
軟體系統的架構與其能否正常運行關係不大,許多架構糟糕的系統依然能運行良好。真正的問題多出現在系統的開發、部署和維護中。雖然好的架構對系統行為有重要影響,但主要是被動和修飾性的。架構的主要目的是支援系統的全生命周期,使其易於理解、開發、維護和部署,最大化程式設計師的生產力並最小化系統的總運營成本。
1. 架構的角色:軟體架構的本質是決定系統的形狀,包括組件的劃分、排列和通信方式。其主要目的是支援系統的開發、部署、運行和維護,最大化選項的開放性。
2. 架構師的角色:軟體架構師仍需是程式設計師,並持續參與編程工作。他們不僅要處理程式碼,還需引導團隊實現最大化生產力的設計。
3. 架構的目標:好的架構應使系統易於理解、開發、維護和部署,目的是最大化程式設計師的生產力,最小化系統的生命周期成本。雖然架構對系統的行為有影響,但主要是被動和修飾性的。
DEVELOPMENT 開發
一個難以開發的軟體系統不可能有長久健康的生命周期,因此系統架構應該讓開發變得容易。對於小團隊來說,他們可以高效地開發單體系統,早期階段的架構反而可能成為障礙。這也是許多系統缺乏良好架構的原因之一。
相反,若是由多個團隊開發,則需要將系統分成明確的組件並設置穩定的接口。這樣的架構雖然對部署、運行和維護不一定最佳,但在開發進度的驅動下,團隊通常會選擇這種方式。
DEPLOYMENT 部署
為了有效運行,軟體系統必須易於部署,部署成本越高,系統的可用性就越低。因此,設計軟體架構的一個目標應是實現一鍵式的輕鬆部署。然而,開發初期很少考慮部署策略,這導致一些系統雖易於開發但難於部署。
例如,早期開發可能選擇“微服務架構”,雖然這使開發更簡單,但在實際部署時,微服務數量過多及其連接配置和啟動時間會成為主要問題。如果早期考慮到部署問題,架構師可能會選擇更少的服務,並採用更集成的管理方式。
OPERATION 運行
軟體架構對系統運行的影響遠不及它對開發、部署和維護的影響來得深遠。幾乎所有運行問題都可以通過增加硬體來解決,這避免了重新設計軟體架構的必要。
長期以來,我們經常目睹這種情況發生。對於那些因架構設計不佳而效率低下的系統,只需增加更多的存儲器和服務器,就能讓其有效運行。 此外,硬體相對於人力成本更低,因此阻礙運行的架構問題不如阻礙開發、部署和維護的架構問題那麼昂貴。
雖然如此,我們仍應該優化軟體架構以更好地支持系統運行,但基於成本效益的考量,優化重點應偏向開發、部署和維護。 此外,軟體架構還在系統運行中扮演另一重要角色,即清晰傳達系統的運行需求。
理想的架構設計應使系統的運行過程對開發者一目了然,將用例(Use Cases)、功能(Features)和必備行為(Required Behaviors) 提升為明顯的實體化,這不僅簡化了開發者對系統的理解,還大大有助於系統的開發和維護。
MAINTENANCE 維護
在軟體系統的各個方面中,維護成本最高。新功能的需求和系統缺陷的修改會消耗大量人力資源,主要成本來自於“探秘(spelunking)”和“風險(risk)”。“探秘”是指挖掘現有系統以確定新增功能或修復問題的最佳位置和方式,而“風險”則是指在修改過程中可能會引入新的問題。通過精心設計的架構,將系統切分為組件並用穩定的接口隔離,可以顯著降低未來功能添加的成本並減少意外故障的風險。
KEEPING OPTIONS OPEN 保持可選項
軟體有行為價值和架構價值兩種,其中架構價值更重要,因為它使軟體具有靈活性。軟體的靈活性取決於系統的形狀、元件的排列和它們之間的連接方式。保持軟體靈活的關鍵是盡可能長時間地保留多種選項,這些選項通常是不重要的細節設計。
所有軟體系統都可以分解為策略和細節兩部分。策略包含所有業務規則和流程,是系統的真正價值所在。細節則是為了與策略交互所需的技術細節,但不影響策略的行為。架構師的目標是創建一個以策略為核心、細節無關的系統形態,這樣可以延後對細節的決策。
舉例部分
1. 資料庫選擇:在開發初期不需要選擇資料庫系統,因為高層策略不應該關心使用哪種資料庫。實際上,如果架構師足夠小心,高層策略不需要在意資料庫是關聯型、分佈式、多層次還是單純的平面檔案。
2. Web 服務選擇:在開發初期不應選定是否使用 Web 服務架構,因為高層策略不應該知道自己會以何種形式發佈。如果高層策略與 HTML、AJAX、JSP、JSF 各項技術等脫鉤,那麼可以將對 Web 系統的選擇推遲到專案後期,甚至不必考慮是否以網頁形式發佈。
3. REST 模式選擇:開發初期不應採用 REST 模式,因為高層策略應該與外部介面無關。類似地,不應過早考慮微服務框架或 SOA 框架,高層策略不應該關心這些。
4. 依賴注入框架:在開發初期不應採用依賴注入框架,因為高層策略不應該操心如何解析系統的依賴關係。
這樣做可以延後決策,讓系統有更多時間進行實驗並獲得更多資訊,從而做出更好的決策。如果已有人做出決策,優秀的架構師會假裝決策未定,儘可能延後或改變這些決策。
DEVICE INDEPENDENCE 設備無關性
在 20 世紀 60 年代,由於計算機行業還處於萌芽階段,大部分程式設計師來自數學或其他工程專業,並且有超過三分之一是女性。當時,我們犯了很多錯誤,其中之一就是將程式碼與 I/O 設備直接緊密地綁定在一起。例如,當我們要寫一段在電傳打印機上輸出的 PDP-8 程序時,需要用到一組特定的機器指令。這導致我們的程式碼高度依賴於特定設備。
PRTCHR, 0
TSF
JMP .-1
TLS
JMP I PRTCHR
這段程式碼中,PRTCHR 是電傳打印機上一段用來打印字符的子程序。首語句中的 0 是存儲其返回地址用的。TSF 指令告訴電傳打印機如果準備就緒,就跳過下一指令;如果繁忙,則繼續執行 JMP .-1 指令。一旦電傳打印機就緒,TSF 跳轉到 TLS 指令,將 A 寄存器中保存的字符發送給電傳打印機,然後 JMP I PRTCHR 指令返回給調用方。
改進措施
隨著時間的推移,我們學到了教訓,並提出了設備無關性這一概念。操作系統將 I/O 設備抽象成處理一條條記錄的標準軟件函數。程序通過調用操作系統提供的服務與這些抽象設備進行交互,從而實現程式碼不經修改即可讀寫卡片或磁帶。這樣,開閉原則 (OCP) 便誕生了。
JUNK MAIL 垃圾郵件
在20世紀60年代末期,我曾在一家為客戶打印群發垃圾郵件的公司工作。客戶會將消費者的名字和地址存儲在磁帶上並寄給我們,我們則負責編寫程序,從磁帶中提取這些信息並將其打印在個性化的廣告信紙上。這些信紙有幾千封,重量近500磅,我們必須一封一封地打印。起初,我們使用的是IBM 360自帶的單行打印機,每個工作日可以打印幾千封信,但這台昂貴的機器租金很高,每月達數萬美金。
為了降低成本,我們改用磁帶來替代單行打印機。這樣一來,IBM 360每10分鐘就可以寫滿一卷磁帶,足夠打印幾卷信紙。這些磁帶被取下後裝載到離線打印機上進行批量打印。我們有五台離線打印機,7x24小時不間斷工作,每週可打印數十萬封信。設備無關性的價值在這裡體現得淋漓盡致:我們的程序不需要關心具體的I/O設備,可以在本地打印機上調試,然後“打印”到磁帶上,最後交由離線打印機進行批量打印。
這段程序的架構設計實現了高層策略與底層實現細節的分離。策略部分負責格式化姓名和地址,細節部分負責操作具體的I/O設備。我們可以在最後階段決定具體使用哪種設備,這種設計大大提高了系統的靈活性和效率。
1. 設備無關性(Device Independence)
在軟體架構中,設備無關性是指系統不依賴於特定的硬件設備。這在例子中體現為,程序可以不依賴於具體的I/O設備(如單行打印機或磁帶),而是通過操作系統提供的抽象接口進行操作。這種架構設計使得系統具有更大的靈活性,因為它可以根據需要更換不同的設備,而無需修改程序。
2. 策略與細節分離(Separation of Policy and Detail)
這是軟體設計中的一個重要原則,強調將高層策略(如業務邏輯)與底層實現細節(如具體技術)分離。在例子中,程序的高層策略是格式化姓名和地址,而具體的打印設備是底層的實現細節。這種分離使得系統可以更容易地維護和擴展,因為更改設備或技術不會影響到核心的業務邏輯。
3. 延後決策(Deferring Decisions)
這個原則強調在設計中儘可能延後對具體技術或設備的決策,以保持系統的靈活性。在例子中,我們可以在程序完成後,根據具體需求選擇是使用本地打印機還是離線打印機,這樣可以根據實際情況做出最優的決策,避免過早鎖定某種技術。
PHYSICAL ADDRESSING 物理尋址
在20世紀70年代早期,我參與開發了一個本地卡車工會的大型會計系統。系統存儲的Agent、Employer和Member等不同類型的記錄因尺寸各異,所以我們特別格式化了一塊25MB的硬碟,使其不同的柱面對應於不同記錄的尺寸。由於我們的軟件需硬編碼硬碟的具體結構,包括柱面、磁頭和扇區等詳細信息,這使得每當需要升級硬碟時,我們不得不重寫大量代碼以適應新的硬件結構。
一位經驗豐富的開發者加入給我們的設計思路帶來了轉機。他建議我們將硬碟視為一個巨大的線性扇區陣列,每個扇區由一個連續的整數地址來訪問,這樣我們就可以通過一個小的轉換程序實時地將相對地址轉換成柱面、磁頭和扇區的物理地址。我們採納了這個建議,從而使系統的高層策略不再依賴於硬碟的物理結構,這一改變極大地提高了系統的靈活性和可擴展性,並使我們能夠更容易地適應未來的硬碟升級。
1. 抽象與分離關注點:將系統的高層策略(如業務邏輯)與低層細節(如硬件結構)分離,通過抽象接口來實現。
例子:將硬碟視為線性扇區陣列,通過相對地址與物理地址的轉換來實現對硬碟的操作。
2. 設備無關性:設計系統時不依賴於特定的硬件設備,通過抽象層來屏蔽具體的硬件實現。
例子:使用相對地址而非具體的柱面、磁頭、扇區號碼,使系統可以適應不同的硬碟設備。
3. 延後技術決策:在系統開發的初期避免過早鎖定具體的技術實現,保持設計的靈活性,便於未來的技術升級和變更。
例子:在初期設計中使用相對地址,推遲具體硬碟結構的決策,避免在硬件升級時需要大規模修改代碼。
本章小結
架構是指將軟體系統劃分為組件,並安排其排列和通信方式,以便於開發、部署、運行和維護。好的架構設計策略是盡可能長時間保留多種選項,確保系統在整個生命周期中易於理解和維護。
雖然架構的首要目的是確保系統正常運行,但更重要的是支撐系統的整個生命周期,最大化程式設計師的生產力並最小化系統的總運營成本。