深夜裡的守衛進階篇
夜又深了一點。城市沉睡,資料仍在流動。守衛們站在崗位上,他們已經不再只是「接住錯誤」,而是要學會 如何命名、如何回報、如何協同。這是守衛的進階修煉。
🪪 守衛的身份 —— Custom Exception在黑夜裡,錯誤四處遊走。如果每位守衛都只高喊「發生了錯誤!」,那麼城裡的人永遠無法分辨:到底是火災、盜賊,還是野獸入侵?這就是 Custom Exception 的意義。
12345public class PayException : Exception{ public PayException(string message, Exception innerException = null) : base(message, innerException) { }}
與其到處貼上 HappyPayException、CoolPayException,有時不如直接說:「這是付款業務的錯誤」。
這增強了系統金流錯誤處理的通用性,不論金流服務換誰,守衛喊的口號都一樣。在夜晚,有時守衛最重要的不是鉅細靡遺交代細節,而是讓人快速判 ...
誰來接住夜裡的錯誤?
深夜裡,城市安靜下來,人們沉睡,街燈是唯一的光。資料依舊流動著,系統像一條河川般運行不息。就在這寧靜之中,錯誤悄然來訪...
🌃 Return Code在黑夜裡,錯誤敲然造房,我們只看到了一張紙條寫著 -1
程式是這樣寫的
123456789101112131415public int CallApi(string url, out string response){ response = string.Empty; if (string.IsNullOrEmpty(url)) return -1; // 錯誤:網址不合法 using var client = new HttpClient(); var result = client.GetAsync(url).Result; if (!result.IsSuccessStatusCode) return -2; // 錯誤:HTTP 狀態碼非 200 response = result.Content.ReadAsStringAsync().Result; ...
Dependency-Inversion Principle
🪵 我受夠了依賴「依賴」就是一種受到某個東西牽制、影響 的狀態。
一個大叔不抽菸就全身不舒服,這就是對香菸的依賴。手機一沒電就開始焦慮(好像是我),這也是一種依賴。
在系統裡,如果 A 模組必須靠 B 模組才能運作,那我們就說 A 依賴了 B。最典型的情況是:A 模組需要直接建立或呼叫 B 模組的實例,才能完成某個功能。
問題來了 —— 假設「DB 的連線方法」被修改了,那所有用到這個方法的「會員查詢」功能就得跟著改,甚至其他關聯模組也會被迫一起修改。 改動會一路延燒,越燒越大。
事實上,我們設計功能的順序應該是反過來的。不是因為「手邊剛好有鍋子和刀子」才說:「那就來煮菜吧!」而是因為「我們想做一道番茄炒蛋」,所以才需要鍋子來炒,刀子來切。
這正是 DIP(依賴反轉原則) 所強調的:
👉 高階模組(業務邏輯)不應該依賴低階模組(細節實作),兩者都應該依賴抽象。
🪵 IoC:控制反轉就算我們把依賴反轉了,還是得有人「建立實例」吧? 光是改成依賴抽象還不夠,因為在使用的那一刻,我們依然需要一個具體的物件。這時候,「控制反轉(Inversion of Control, IoC) ...
Liskov Substitution Principle
在軟體設計中,繼承 (Inheritance) 的發生是再正常不過的事了,作為結構化的系統,我們可以視其為多個組件裝載而成的,當結構益龐大起來,就會想將行為類似的程式碼重用,而有了繼承的概念,但這也造成父類與子類使用上耦合造成的問題,繼承不應該只是為了「重複使用程式碼」而存在,而是應該基於行為上的一致性。
因此里氏替換原則 (LSP, Liskov Substitution Principle) 就是在提醒我們
👉 子類別必須能夠替代父類別而不影響系統的正確性。
換句話說,當一個地方期待父類別時,若換成子類別,程式應該還是能正常運作、不會出現違反預期的行為。
🪵 先驗條件(Preconditions)不可加強子類別不能比父類別更挑剔輸入。想像你去餐廳,主廚說:「你隨便帶食材來,我都能幫你做菜。」結果徒弟廚師說:「不行!你只能帶牛肉來,不然我不煮。」這就是把條件變得更嚴格,使用者本來以為能帶什麼都行,結果換成子類卻被限制了。
🪵 後驗條件(Postconditions)不可削弱子類別的輸出不能比父類別更弱,至少要保證一樣的結果。父類(物流公司)承諾:「只要你下單,我一定送達 ...
Interface Segregation Principle
客戶(Client)不應被迫依賴對其而言無用的方法或功能。換句話說:介面設計不應包山包海,而應拆分成精簡、專注的責任。
🪵 違反介面隔離原則的例子1234567891011121314151617181920212223242526272829303132333435363738394041interface IPrintFunction{ public void print(); public void scan(); public void fax(); public void copy(); public void copyThenSendMail();}public class EPSON: IPrintFunction{ public void print() { Console.WriteLine("我能列印!"); } public void scan() { Console.Wri ...
Single Resposibility Principle
🪵 SRP 想解決甚麼問題在系統設計裡,最常見也最棘手的問題之一,就是「改 A 怕改到 B」。過度耦合會讓維護變得艱難:一個看似單純的修改,卻可能意外影響其他功能。這也是單一職責原則 (Single Responsibility Principle, SRP) 想要解決的核心:一個組件(class、模組、服務)應該只有一個「變動的原因」。
🪵 職責的本質:來自業務,而非技術那麼什麼是「職責」?不是單純的「資料庫」、「控制器」或「畫面元件」,而是 業務驅動的需求。一個組件的職責,應該來自同一群「對它提出業務需求的人」。
舉例來說:
帳戶狀態管理:由風控或會員管理部門提出需求。
用戶等級規則:由行銷或會員成長團隊提出需求。
這些需求雖然都跟「用戶」有關,但來自不同的業務關聯方。如果硬是把它們混在同一個組件裡,將來只要一邊業務有變化,另一邊就會被波及,導致「明明沒動它,卻壞了」。
🪵 規模越大,越容易失控在小型專案裡,把功能寫在一起好像還能撐住。但當系統規模逐漸擴大、業務日益複雜,就會開始顯現問題
修改週期變慢:每次改動都要檢查一堆不相關的功能。
心智負擔增加:維護 ...
Polymorphism
🪵 進擊的巨人尤米爾·弗里茲是最初的巨人之力擁有者。在她死後,國王為了維持這股力量,命令她的三個女兒 ── 瑪莉亞、羅塞、希娜 ── 吃下分屍後的母體。於是,巨人之力被分散成 九大巨人:始祖、進擊、超大型、鎧甲、女巨人、獸、戰鎚、顎、車力。
為甚麼呢,因為諫山創實作了多型(Polymorphism):
同樣都是「巨人」,卻能展現出完全不同的外觀、能力與戰鬥風格。
基底類別 Titan 定義了巨人的共通特性,而每個具體巨人(ColossalTitan、ArmoredTitan…)則是子類別,實作或覆寫不同的行為
後期的 Eren 同時擁有「進擊的巨人」、「始祖巨人」、「戰鎚巨人」的能力,就像一個物件同時實作多個介面,或覆寫了不同的行為
共通限制:因為尤米爾只活了 13 年,她的繼承者也都受「13 年壽命」的約束,這就像基底類別定義的共同規則,所有子類別都必須遵守
🪵 Static (Compile-time) Polymorphism不同角色都會「攻擊」,但攻擊方式可能帶有不同條件(武器、對象、力量)。
123456789101112131415161718192 ...
Encapsulation
🪵 降低耦合所謂「降低耦合」,就是讓物件的使用者能很直覺地透過公開的方法去操作,而不用關心內部的細節。這麼做有兩個好處:
使用者能專注在更高層次的抽象,而不是陷在物件內部的實作。
後續如果內部邏輯要修改,開發者比較不用擔心會影響到外部程式。
購物車
12345678public class ShoppingCart{ public List<string> Items = new List<string>();}var cart = new ShoppingCart();cart.Items.Add("iPhone");cart.Items.Clear(); // 嘿嘿,使用者直接把購物車清空
如果直接把集合公開,外部程式就能為所欲為:
隨便新增重複的商品、甚至一次把所有商品刪光,這樣一來,我們完全無法控制購物車的正確狀態。
透過封裝,我們只提供必要的方法:
123456789101112public class ShoppingCart{ private readonly List< ...
internal - 不要偷看!
internal 1internal 2
🪵 組件(Assembly)在 .NET 中,「組件(Assembly)」是一個可以被執行或被載入的最小單位,通常就是一個 .dll(類別庫) 或 .exe(應用程式執行檔)。
Dynamic Link Library(動態連結程式庫)。它是一個 二進位檔(binary file),副檔名是 .dll。裡面存放的是「程式碼 + 資源」,但不能自己執行。要靠其他程式(例如 exe)來「載入」它,才能使用裡面的功能。簡單說,DLL 就像一本工具書,裡面有很多函式與類別,你可以引用它,但它不會自己動。
在 Windows 下,DLL/EXE 都遵循 PE(Portable Executable)格式。.NET 世界裡,DLL 裡面會多一層 IL(Intermediate Language,中間語言) 和 Metadata(中繼資料)。也就是說,.NET 的 DLL 並不是直接的 CPU 機器碼,而是「IL + Metadata」組合。
而 Visual Studio 的「Project」編譯出來之後,通常就是一個組件(Assembly ...
sealed - 封印
seal 1seal 2
法律是一個經過設計的系統,不能隨意被改寫、繼承或覆蓋。你不能說:「我繼承《交通法》,然後 override 掉超速罰則。」就算你覺得這條法律不合理,也不能自己改動,只能在法律框架下使用它。
法律是 sealed 的。因為它必須保證一致性、可預測性、公平性。若人人都能繼承、改寫,那麼整個社會將陷入混亂。
同樣地,一位資深醫生制定的手術流程極為嚴謹有效。新人不能說:「我繼承這個流程,但不消毒就直接開刀。」這就是為什麼醫療 SOP 是 sealed ——當一個系統至關重要、不能容許錯誤時,它就應該被封印。
sealed 的意義在於:在自由與彈性之中,劃下一條「不可更動」的底線,以確保核心價值的穩定與安全。
對系統來說:是一種 封裝
對團隊來說:是一種 約束
對使用者來說:是一種 保證
🪵 對開發者的具體好處如果 sealed 只是「道德感的約束」,對工程師來說可能太抽象。我們更需要理解它對寫程式的人帶來的實際好處。
明確表達設計意圖,不怕被「誤用」或「誤繼承」當你設計一個類別並加上 sealed,你在宣告:「這個類別不是設計來被繼承的 ...