[IT] 軟體架構編年史 The Software Architecture Chronicles

前言:這系列文章為翻譯自作者 hgraca。 這篇文章是關於軟體架構系列文章的第一篇。在這些文章中,我將分享我所學習到的軟體架構知識,我如何看待它,以及我如何運用這些知識。 我這一系列的文章稱為「軟體架構編年史」,並非因為我自認為是一位偉大的作家,而是因為我覺得這個名字有點俗氣,又帶點趣味。 在這篇首篇文章中,我將會談論我為何要撰寫這一系列的文章,以及接下來將會有什麼內容。 認識歷史的重要性 Those who fail to learn History are doomed to repeat it. - George Santayana, The Life of Reason, 1905 未能學習歷史的人注定要重蹈覆轍。 - 喬治‧桑塔亞納《理性的生活》,1905 我認為從歷史中學習是非常重要的,它能教導我們事情。在個人層面上,我們終究需要(也希望)能從錯誤中學習。從國家的角度來看,歷史幫助塑造我們的文化、創造群體的概念,因此有了「台灣人」的觀念,一種國家認同。同時,歷史也幫助我們從祖先的錯誤中學習,比如信仰那些有著怪異思想的人,如二戰… 對程式開發者而言,歷史有助於我們仰靠前人的經驗上,少走許多錯的道路,並讓我們「站在巨人的肩膀上」達到更高的境界! 在我成為更好的開發者路上,我閱讀了很多文章,觀看了許多演講,我盡我所能的站在巨人的肩膀上。 有一件事情使我感到困惑,那就是許多意見是基於意見之上的意見…這就像是以訛傳訛,我們最終得到是對於一篇論文、文章或書籍真正內容的扭曲理解。 因此,我開始在網路上搜尋原始的論文、文章和書籍,這些都是我認為對我的工作最重要的概念,並自己對它們進行思考。 這些文章是這種推理的結果,因為我試圖理解這些概念是如何產生的,以某種程度上的時間順序來看。 撰寫這些文章迫使我大量閱讀和思考所有議題,並幫助我理解當代在軟體開發中使用的技術。我希望這些文章能對更多開發者有所幫助。 然而,如果你讀到一些你不理解或是不認同的內容,請告訴我,我非常願意討論這些議題,並從討論中學習,也願意修正我錯誤的觀點。 文章列表 1. 軟體架構前提 (Software Architecture Premises) 2. 程式語言的演進 (Programming Languages Evolution) 3. 架構風格 / 架構模式 / 設計模式 (Architectural Styles vs. Architectural Patterns vs. Design Patterns) 4. 單體架構 (Monolithic Architecture) 5. 分層架構 (Layered Architecture) 6. MVC 及其變形 ...

<span title='2023-10-13 23:41:57 +0800 +0800'>October 13, 2023</span>&nbsp;·&nbsp;2 min&nbsp;·&nbsp;hgraca

[IT] Clean Architecture - 第4章 - 結構化程式設計

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 也是正確的,他也證明了迭代的起始條件與結束條件的正確性。 這樣的證明是費時且複雜的,但它們具備了證明的意義。隨著它們的發展,建立一個歐基里得式階層式的定理看似變得可行。 ...

<span title='2023-10-03 22:28:44 +0800 +0800'>October 3, 2023</span>&nbsp;·&nbsp;1 min&nbsp;·&nbsp;Rain Hu

[IT] Clean Architecture

乾淨架構 設計(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) ...

<span title='2023-09-29 02:03:47 +0800 +0800'>September 29, 2023</span>&nbsp;·&nbsp;2 min&nbsp;·&nbsp;Rain Hu

[IT] 事件總線 EventBus

EventBus EventBus 用於維護一個事件源與事件處理的映射字典 通過 Singleton,確保 EventBus 的唯一入口 利用反射完成事件源與件事處理的初始化綁定 提供統一的事件注冊(register)、取消注冊(unsubscribe)和觸發(trigger)。 Interfaces IEventData public interface IEventData { DateTime EventTime { get; set; } object EventSource {get; set; } } IEventHandler public interface IEventHandler { } IEventHandler`1 public interface IEventHandler<TEventData> : IEventHandler where TEventData : IEventData { void HandlerEvent(TEventData eventData); } base class EventData public class EventData : IEventData { public DateTime EventTIme { get; set; } object EventSource { get; set; } public EventData() { EventTime = DateTime.Now; } } Domain FishType public enum FishType { None, 鯽魚, 鯉魚, 黑魚, 青魚, 草魚, 鱸魚 } FishingEventData : EventData public class FishingEventData : EventData { public FishType FishType { get; set; } public FishingMan FishingMan { get; set; } } FishingEventHandler : IEventHandler public class FishingEventHandler : IEventHandler<FishingEventData> { public void HandleEvent(FishingEventData eventData) { var type = eventData.FishType; var fishMan = eventData.FishingMan; var Name = fishMan.Name; if (type == FishType.None) { fishMan.Message = string.Format("{0}: 沒有釣到魚, 累計釣了{1}條魚", Name, fishMan.FishCount); } else { fishMan.FishCount++; fishMan.Message = string.Format("{0}: 釣到一條[{2}], 累計釣了{1}條魚", Name, fishMan.FishCount, type); } } } FishingMan public class FishingMan { public string Name { get; set; } public int FishCount { get; set; } public FishingRod FishingRod { get; set; } public string Message { get; set; } public FishingMan(string name) { Name = name; FishCount = 0; } public void Fishing() { FishingRod.ThrowHook(this); } } FishingRod 用反射註冊事件 public class FishingRod { public string Message { get; private set; } public FishingRod() { } public void ThrowHook(FishingMan man) { if (new Random().Next() % 2 == 0) { var type = (FishType)(new Random().Next(0, 5) + 1); Message = ("魚兒上鉤了!"); if (FishingEvent != null) { var eventData = new FishingEventData { FishingMan = man, FishType = type }; EventBus.Default.Trigger<FishingEventData>(eventData); } } else { var type = FishType.None; Message = ("可惜了!"); if (FishingEvent != null) { var eventData = new FishingEventData { FishingMan = man, FishType = type }; EventBus.Default.Trigger<FishingEventData>(eventData); } } } } EventBus 實作 用一個靜態單例來統一管理事件 public class EventBus { public static EventBus Default => new EventBus(); private readonly ConcurrentDictionary<Type, List<Type>> _eventAndHandlerMapping; private EventBus() { _eventAndHandlerMapping = new ConcurrentDictionary<Type, List<Type>>(); MapEventToHandler(); } private void MapEventToHandler() { Assembly assembly = Assembly.GetEntryAssembly(); foreach (var type in assembly.GetTypes()) { if (typeof(IEventHandler).IsAssignableFrom(type)) { Type handlerInterface = type.GetInterface("IEventHandler`1"); if (handlerInterface == null) continue; Type eventDataType = handlerInterface.GetGenericArguments()[0]; if (_eventAndHandlerMapping.ContainsKey(eventDataType)) { List<Type> handlerTypes = _eventAndHandlerMapping[eventDataType]; handlerTypes.Add(type); _eventAndHandlerMapping[eventDataType] = handlerTypes; } else { var handlerTypes = new List<Type> { type }; _eventAndHandlerMapping[eventDataType] = handlerTypes; } } } } public void Register<TEventData>(Type eventHandler) { List<Type> handlerTypes = _eventAndHandlerMapping[typeof(TEventData)]; if (!handlerTypes.Contains(eventHandler)) { handlerTypes.Add(eventHandler); _eventAndHandlerMapping[typeof(TEventData)] = handlerTypes; } } public void Unsubscribe<TEventData>(Type eventHandler) { List<Type> handlerTypes = _eventAndHandlerMapping[typeof(TEventData)]; if (!handlerTypes.Contains(eventHandler)) { handlerTypes.Remove(eventHandler); _eventAndHandlerMapping[typeof(TEventData)] = handlerTypes; } } public void Trigger<TEventData>(TEventData eventData) where TEventData : IEventData { List<Type> handlers = _eventAndHandlerMapping[eventData.GetType()]; if (handlers != null && handlers.Count > 0) { foreach (var handler in handlers) { MethodInfo methodInfo = handler.GetMethod("HandleEvent"); if (methodInfo != null) { object obj = Activator.CreateInstance(handler); methodInfo.Invoke(obj, new object[] { eventData }); } } } } } demo MacOs Cocoa Project public partial class ViewController : NSViewController { public ViewController (IntPtr handle) : base (handle) { } public override void ViewDidLoad () { base.ViewDidLoad (); jeff = new FishingMan("Jeff"); rod = new FishingRod(); jeff.FishingRod = rod; EventBus eventBus = EventBus.Default; eventBus.Register<FishingEventData>(typeof(FishingEventHandler)); } FishingMan jeff; FishingRod rod; public override NSObject RepresentedObject { get { return base.RepresentedObject; } set { base.RepresentedObject = value; } } partial void Button_Click(NSButton sender) { jeff.Fishing(); TextLabel.StringValue = rod.Message; TextLabel2.StringValue = jeff.Message ?? ""; } }

<span title='2023-09-25 23:50:02 +0800 +0800'>September 25, 2023</span>&nbsp;·&nbsp;3 min&nbsp;·&nbsp;Rain Hu

[IT] Clean Architecture - 重點整理

乾淨架構(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

<span title='2023-09-23 20:03:55 +0800 +0800'>September 23, 2023</span>&nbsp;·&nbsp;1 min&nbsp;·&nbsp;Rain Hu

[IT] LINQ: IQueryable Provider

可重複使用的 IQueryable 基類 IQueryable 簡介 在 C# 最新版本中的 IQueryable 已經不再是一個介面,而是分為兩個部分: IQueryable 與 IQueryProvider。在開始實作之前,我們必須先了解一下這兩個介面。 public interface IQuerable : IEnumerable { Type ElementType { get; } Expression Expression { get; } IQueryProvider Provider { get; } } public interface IQueryable<T> : IEnumerable<T>, IQueryable, IEnumerable { } IQueryable 有三個唯讀屬性: ElementType 代表了元素的類型 (或等於 IQueryable<T> 中的 T) Expression 代表了查詢對應的表達式。這是 IQueryable 存在的核心要素。在 IQueryable 的內部,實際上是一個表示查詢的表達式,它將查詢表示為 LINQ 查詢運算子/方法調用的樹狀結構。如果進一步看,你會發現,IQueryable 或是 Queryable 都只是在提供一個自動構建表達式樹節點 (expression tree nodes) 的機制。當我們對 IQeuryable 使用 Where 方法時,它只是回傳一個新的 IQueryable,並且在進行調用的樹頂添加一個方法表達式樹節點。 Provider 作為真正的「提供者」,它負責原先所有 IQueryable 的執行方法。 IQueryProvider 簡介 public interface IQueryProvider { IQueryable CreateQuery(Expression expression); IQueryable<TElement> CreateQuery<TElement>(Expression expression); object Execute(Expression expression); TResult Execute<TResult>(Expression expression); } 當我們進一步觀察 IQueryProvider,會發現它事實上只有兩個操作:CreateQuery、Execute,只是各有一個泛型與非泛型的方法。一般我們會使用泛型的方法,因為它可以避免使用反射來建構實例,從而提高性能。 ...

<span title='2023-09-21 11:34:15 +0800 +0800'>September 21, 2023</span>&nbsp;·&nbsp;19 min&nbsp;·&nbsp;Rain Hu

[IT] Clean Architecture - 第3章 - 程式設計範式總覽

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. 我們利用結構化程式設計作為模塊的演算法基礎。 注意這三個與建築的三個重要關注點不謀而合:功能、組件分離、數據管理。

<span title='2023-09-07 22:07:54 +0800 +0800'>September 7, 2023</span>&nbsp;·&nbsp;1 min&nbsp;·&nbsp;Rain Hu

[IT] Clean Architecture - 第二部分 從基礎構件開始: 程式設計範式(Paradigms)

 軟體架構始於程式碼,因此我們將從程式碼的角度開始討論架構,看看自從程式碼被寫下以來我們所學到的內容。 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等等,無窮無盡。 另一個可能更重要的革命是在程式設計範式方面。範式是編程的方式,與語言相對無關。範式指導了開發人員應該使用哪些程式結構,以及何時使用它們。 迄今為止,已經有三種這樣的範式,也不太可能再有其它的範式,原因後述。

<span title='2023-09-05 21:43:11 +0800 +0800'>September 5, 2023</span>&nbsp;·&nbsp;1 min&nbsp;·&nbsp;Rain Hu

[IT] Clean Architecture - 第2章 - 兩個價值維度

Ch2. 兩個價值維度 每個軟體系統都為利益相關者(stakeholders)提供兩種不同的價值維度:行為(behavior)與結構(structure)。軟體開發人員負責確保這兩種價值保持高水準。不幸的是,他們往往只關注其中一個而忽略另一個,更不幸的是,他們往往只關注較低價值的那一個,最終使軟體失去價值。 行為 歉體的第一個價值來自於行為。程式設計師被聘請來使機器以一種能為利益相關者帶來獲利或節省成本的方式運作。我們透過協助利益相關者制定功能規格或者需求文件,然後編寫程式碼,使利益相關者的機器滿足這些需求。當機器違反這些要求時,程式設計師便開始除錯以修復這些問題。 結構 軟體的第二個價值來自於結構,軟體之所以為「軟」體,是因為它被創造出來是為了方便改變機器的行為。為了實現其目的,軟體必須要有足夠的彈性,易於修改。當利益相關者對某項功能改變主意時,這種改變應該要是可以簡單且容易進行的。進行這種改變的困難程度應該只與改變的範圍成比例,而不是與改變的形式成比例。 然正是範圍和形狀之間的差異常常推動軟體開發成本的增長。這就是為什麼成本與所需求的變更規模不成比例。這也是為什麼開發前期比開發後期便宜得多的原因。 從利益相關者的角度來看,他們只是提供了一系列大致相似範圍的變更。從開發者的角度來看,利益相關者給予他們一系列拼圖碎片,他們必須將其放入日益複雜的拼圖中。每個新的要求都比上一個更難擬合,因為系統的形狀與要求的形狀不匹配。 我在這裡以一種非傳統的方式使用「形狀」這個詞,但我認為隱喻很貼切。軟體開發人員常常感覺自己被迫將方形木塊塞進圓洞中。 問題當然在於系統的架構。如果這個架構偏好某種形式,那麼新功能愈來愈難以適應這種結構。因此,架構應該盡可能地不受形式的限制。 更大的價值 行為與架構,何者提供更大的價值?對軟體系統來說,它的功能更重要,還是易於修改更重要? 如果你問企業經理,他們通常會說軟體系統的工作更重要。開發人員通常也會贊同。但這是錯誤的態度,我們可以用簡單的邏輯工具來證明它是錯誤的,那就是檢查極端情況。 如果你有一個完美運作但無法更改的程式,那麼當需求改變時它將無法運作,意謂著這個程式將變得沒有用處;然而一個不起作用但容易修改的程式,那麼我們可以透過簡單的修改使其運作起來,並在需求變化時保持運作,因此這個程式將持續保持有用。 也許這個論點對有些人來說不那麼具有說服力,畢竟沒有什麼程式是真的不能改變的。但是,有些系統在實際上是幾乎不可能改變的,因為改變的成本大過於了改變的好處。許多系統在某些功能或配置上達到了這一點。 如果你問企業經理們是否希望能夠進行變更,他們當然會說是,但可能會在回答中指出目前的功能比任何後續的靈活性更重要。相反地,如果企業經理們向你提出變更要求,而你估計的成本過高而無法負擔,他們很可能會對你允許系統達到變更變得不切實際的程度感到憤怒。 艾森豪威爾矩陣 下圖為艾森豪威爾(Dwight D. Eisenhower)總統的重要性(importance)與緊迫性(urgency)的矩陣,艾森豪是這麼說的: 我有兩種問題,一種是緊急的,一種是重要的。緊急的問題並不重要,而重要的問題從不緊急。 軟體的第一個價值 - 行為,是緊迫但並非總是特別重要。 軟體的第二個價值 - 架構,重要但從不特別緊迫。 當然,有些事情既緊急又重要,或有些事情既不緊急又不重要。故最終,我們可以將這四種問題安排成優先順序: 緊急且重要 不緊急但重要 緊急但不重要 不緊急也不重要 需要注意的是,程式碼的架構位於前兩個位置,而程式碼的行為則佔據第一與第三的位置。商業經理和開發人員常犯的錯誤是將第三位的事項提升至第一位。換句話說,他們未能將那些緊急但不重要的功能與真正緊急且重要的功能區分開來。這種失誤導致忽視系統的重要架構,而偏好系統中不重要的功能。 軟體開發人員面臨的困境是,商業經理無法評估架構的重要性。這就是軟體開發人員被聘用的原因。因此,軟體開發團隊有責任強調架構的重要性,而不是功能的緊迫性。 為架構而戰 履行這個責任意味著投入一場戰鬥,或者也許更好的詞是「奮鬥」。坦白說,這些事情總是這樣做的方式。開發團隊必須為他們認為對公司最好的事情而奮鬥,管理團隊、市場團隊、銷售團隊和運營團隊也是如此。這總是一場奮鬥。 有效的軟體開發團隊會毫不畏懼地與其他利益相關者平起平坐地爭論。請記住,作為一名軟體開發者,您也是一個利益相關者。您需要保護您所需的軟體。這是您的角色和責任的一部分,也是您被雇用的重要原因之一。 如果你是一位軟體架構師,這個挑戰對你來說尤其重要。軟體架構師根據他們的工作描述,更關注系統的結構而非其特性和功能。架構師創建一個架構,使得這些特性和功能能夠容易地開發、修改和擴展。 只要記住:如果架構放在最後,那麼系統的開發成本將會愈來愈高,最終對系統的某個部分或整個系統的變更幾乎變得不可能。如果這種情況發生,那意味著軟體開發團隊沒有為他們知道的必要事項進行足夠的努力。

<span title='2023-09-04 15:18:12 +0800 +0800'>September 4, 2023</span>&nbsp;·&nbsp;1 min&nbsp;·&nbsp;Rain Hu

[IT] Clean Architecture - 第1章 - 設計與架構到底是什麼

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的日子還快。 結論 在每一種情況下,最好的選擇是開發組織要認識到並避免自己的過度自信,並且開始認真對待軟體架構的質量。 要認真對待軟體架構,你需要知道什麼是良好的軟體架構。為了構建一個設計和架構能夠最大程度減少工作量並提高生產力的系統,你需要知道哪些系統架構的特性能夠達到這個目標。 這本書就是在談這個,它描述了好的乾淨架構和設計的樣貌,讓軟體開發者能夠建立具有長期盈利生命力的系統。

<span title='2023-09-03 23:14:07 +0800 +0800'>September 3, 2023</span>&nbsp;·&nbsp;1 min&nbsp;·&nbsp;Rain Hu