🏛️ SOLID 五大原則
📌 什麼是 SOLID?
SOLID 是五個物件導向設計原則的縮寫,就像蓋房子的五大基本功法,遵守它們可以讓你的程式碼更容易維護、擴充和測試。
想像你在經營一家餐廳,SOLID 就是經營的五大黃金法則!
🔤 S — Single Responsibility Principle(單一職責原則)
💡 核心概念
一個類別只做一件事,就像專業廚師:炒菜的廚師不應該同時負責洗碗和收銀。
❌ 違反的程式碼
// 這個類別做太多事了!又管員工資料、又算薪水、又存資料庫
public class Employee
{
public string Name { get; set; } = ""; // 員工姓名
public decimal Salary { get; set; } // 員工薪資
// 計算薪水 — 這是商業邏輯
public decimal CalculateBonus()
{
return Salary * 0.1m; // 獎金為薪水的 10%
}
// 儲存到資料庫 — 這是資料存取邏輯
public void SaveToDatabase()
{
// 直接寫 SQL 存到資料庫(不應該在這裡做!)
Console.WriteLine("儲存員工資料到資料庫"); // 模擬存檔
}
// 產生報表 — 這是呈現邏輯
public string GenerateReport()
{
// 產生 HTML 報表(也不應該在這裡!)
return $"<h1>{Name}</h1><p>薪資:{Salary}</p>"; // 回傳報表
}
}
✅ 遵守的程式碼
// 員工類別:只負責管理員工資料
public class Employee
{
public string Name { get; set; } = ""; // 員工姓名
public decimal Salary { get; set; } // 員工薪資
}
// 薪資計算服務:只負責計算薪資相關邏輯
public class SalaryCalculator
{
// 計算獎金:傳入員工,回傳獎金金額
public decimal CalculateBonus(Employee employee)
{
return employee.Salary * 0.1m; // 獎金為薪水的 10%
}
}
// 員工倉儲:只負責資料存取
public class EmployeeRepository
{
// 儲存員工資料到資料庫
public void Save(Employee employee)
{
Console.WriteLine($"儲存 {employee.Name} 到資料庫"); // 模擬存檔
}
}
// 報表產生器:只負責產生報表
public class EmployeeReportGenerator
{
// 產生 HTML 報表
public string Generate(Employee employee)
{
return $"<h1>{employee.Name}</h1>"; // 回傳簡單報表
}
}
📖 解釋
把一個「什麼都做」的大類別拆成多個「各司其職」的小類別。每個類別只有一個改變的理由:薪資算法改了只改 SalaryCalculator,資料庫換了只改 EmployeeRepository。
🔤 O — Open-Closed Principle(開放封閉原則)
💡 核心概念
對擴充開放,對修改封閉。就像樂高積木:你可以一直往上加新的積木(擴充),但不需要拆掉原本的結構(修改)。
❌ 違反的程式碼
// 每次新增折扣類型,都要修改這個方法 — 很危險!
public class DiscountCalculator
{
// 計算折扣:每次加新類型都要改這裡
public decimal Calculate(string customerType, decimal price)
{
if (customerType == "Regular") // 一般客戶
return price * 0.9m; // 打 9 折
else if (customerType == "VIP") // VIP 客戶
return price * 0.8m; // 打 8 折
else if (customerType == "SVIP") // 超級 VIP
return price * 0.7m; // 打 7 折
// 每次新增客戶類型都要加 else if... 容易改壞!
return price; // 預設不打折
}
}
✅ 遵守的程式碼
// 定義折扣策略介面:所有折扣都要實作這個合約
public interface IDiscountStrategy
{
decimal ApplyDiscount(decimal price); // 計算折扣後的價格
}
// 一般客戶的折扣策略
public class RegularDiscount : IDiscountStrategy
{
public decimal ApplyDiscount(decimal price)
{
return price * 0.9m; // 一般客戶打 9 折
}
}
// VIP 客戶的折扣策略
public class VipDiscount : IDiscountStrategy
{
public decimal ApplyDiscount(decimal price)
{
return price * 0.8m; // VIP 客戶打 8 折
}
}
// 新增 SVIP 折扣時,只需要加一個新類別,不用改原本的程式碼!
public class SvipDiscount : IDiscountStrategy
{
public decimal ApplyDiscount(decimal price)
{
return price * 0.7m; // SVIP 客戶打 7 折
}
}
// 折扣計算器:接受任何折扣策略
public class DiscountCalculator
{
// 傳入不同的折扣策略,就能算不同的折扣
public decimal Calculate(IDiscountStrategy strategy, decimal price)
{
return strategy.ApplyDiscount(price); // 委派給策略物件處理
}
}
📖 解釋
透過介面(interface)和多型(polymorphism),新增功能時只需要增加新類別,不用修改原本已經測試過的程式碼。就像手機裝 App — 手機本身不用改,裝上新 App 就有新功能!
🔤 L — Liskov Substitution Principle(里氏替換原則)
💡 核心概念
子類別必須能完全替代父類別,不能讓使用者嚇一跳。就像你點了一杯「飲料」,不管送來的是果汁還是咖啡,都應該能喝,不會送來一塊石頭。
❌ 違反的程式碼
// 鳥類的基底類別
public class Bird
{
public virtual void Fly() // 所有鳥都能飛...真的嗎?
{
Console.WriteLine("我在飛!"); // 印出飛行訊息
}
}
// 企鵝是鳥,但企鵝不會飛!
public class Penguin : Bird
{
public override void Fly()
{
// 企鵝不會飛,所以丟出例外 — 這就違反了 LSP!
throw new NotSupportedException("企鵝不會飛!");
}
}
// 使用時會出問題
public class BirdWatcher
{
public void WatchBirdFly(Bird bird) // 傳入任何鳥
{
bird.Fly(); // 如果傳入企鵝就會爆炸!💥
}
}
✅ 遵守的程式碼
// 基底類別:所有鳥都有的行為
public abstract class Bird
{
public abstract void Move(); // 所有鳥都會移動(但方式不同)
}
// 會飛的鳥
public class Sparrow : Bird
{
public override void Move()
{
Console.WriteLine("麻雀在天上飛!"); // 麻雀用飛的移動
}
}
// 企鵝也是鳥,但用走的移動
public class Penguin : Bird
{
public override void Move()
{
Console.WriteLine("企鵝在地上走!"); // 企鵝用走的移動
}
}
// 如果需要「飛」的行為,用介面來區分
public interface IFlyable
{
void Fly(); // 只有會飛的才實作這個介面
}
// 麻雀會飛,所以實作 IFlyable
public class FlyingSparrow : Bird, IFlyable
{
public override void Move()
{
Console.WriteLine("麻雀在移動"); // 基本移動行為
}
public void Fly()
{
Console.WriteLine("麻雀在天上飛翔!"); // 飛行行為
}
}
📖 解釋
重新設計繼承階層,讓 Move() 成為所有鳥共有的行為,而 Fly() 透過介面只給會飛的鳥。這樣任何 Bird 的子類別都能安全替換父類別,不會出現意外。
🔤 I — Interface Segregation Principle(介面隔離原則)
💡 核心概念
介面不要太胖!不要強迫類別實作它不需要的方法。就像餐廳菜單:素食者不應該被迫點牛排。
❌ 違反的程式碼
// 太胖的介面!不是每台機器都需要所有功能
public interface IMachine
{
void Print(); // 列印
void Scan(); // 掃描
void Fax(); // 傳真
void Staple(); // 裝訂
}
// 舊式印表機只能列印,但被迫實作所有方法
public class OldPrinter : IMachine
{
public void Print()
{
Console.WriteLine("列印文件"); // 這個沒問題
}
public void Scan()
{
throw new NotSupportedException("我不會掃描!"); // 被迫實作但做不到
}
public void Fax()
{
throw new NotSupportedException("我不會傳真!"); // 被迫實作但做不到
}
public void Staple()
{
throw new NotSupportedException("我不會裝訂!"); // 被迫實作但做不到
}
}
✅ 遵守的程式碼
// 把大介面拆成小介面,各司其職
public interface IPrinter
{
void Print(); // 列印功能
}
public interface IScanner
{
void Scan(); // 掃描功能
}
public interface IFaxMachine
{
void Fax(); // 傳真功能
}
// 舊式印表機只實作需要的介面
public class OldPrinter : IPrinter
{
public void Print()
{
Console.WriteLine("列印文件"); // 只做自己會的事
}
}
// 多功能事務機實作多個介面
public class MultiFunctionPrinter : IPrinter, IScanner, IFaxMachine
{
public void Print()
{
Console.WriteLine("列印文件"); // 列印功能
}
public void Scan()
{
Console.WriteLine("掃描文件"); // 掃描功能
}
public void Fax()
{
Console.WriteLine("傳真文件"); // 傳真功能
}
}
📖 解釋
把一個「大而全」的介面拆成多個「小而專」的介面。每個類別只需要實作自己真正需要的功能,不用被迫寫一堆 throw NotSupportedException()。
🔤 D — Dependency Inversion Principle(依賴反轉原則)
💡 核心概念
高層模組不應該依賴低層模組,兩者都應該依賴抽象。就像電器和插座:吹風機不需要知道電力來自火力發電還是太陽能,只要有標準插座(介面)就能用。
❌ 違反的程式碼
// 低層模組:寄送 Email
public class EmailService
{
public void SendEmail(string message)
{
Console.WriteLine($"寄送 Email:{message}"); // 寄出 Email
}
}
// 高層模組:直接依賴具體的 EmailService — 耦合太緊!
public class OrderProcessor
{
private readonly EmailService _emailService; // 直接依賴具體類別
public OrderProcessor()
{
_emailService = new EmailService(); // 自己 new — 無法替換!
}
public void ProcessOrder(string orderId)
{
Console.WriteLine($"處理訂單:{orderId}"); // 處理訂單邏輯
_emailService.SendEmail($"訂單 {orderId} 已處理"); // 寄送通知
}
}
✅ 遵守的程式碼
// 定義抽象:通知服務的介面
public interface INotificationService
{
void Send(string message); // 發送通知(不管用什麼方式)
}
// 實作一:Email 通知
public class EmailNotification : INotificationService
{
public void Send(string message)
{
Console.WriteLine($"📧 Email 通知:{message}"); // 用 Email 寄送
}
}
// 實作二:簡訊通知
public class SmsNotification : INotificationService
{
public void Send(string message)
{
Console.WriteLine($"📱 簡訊通知:{message}"); // 用簡訊寄送
}
}
// 高層模組:依賴抽象(介面),不依賴具體實作
public class OrderProcessor
{
private readonly INotificationService _notification; // 依賴介面
// 透過建構子注入依賴(DI)
public OrderProcessor(INotificationService notification)
{
_notification = notification; // 從外部注入,可以隨時替換
}
public void ProcessOrder(string orderId)
{
Console.WriteLine($"處理訂單:{orderId}"); // 處理訂單邏輯
_notification.Send($"訂單 {orderId} 已處理"); // 發送通知
}
}
📖 解釋
透過依賴注入(Dependency Injection),高層模組只認識介面,不認識具體實作。想從 Email 改成簡訊通知?換一行注入的程式碼就好,OrderProcessor 完全不用改!
🤔 我這樣寫為什麼會錯?
錯誤一:一個類別超過 500 行
// ❌ 一個 UserService 做了所有事情
public class UserService
{
public void Register() { } // 註冊
public void Login() { } // 登入
public void SendEmail() { } // 寄信
public void GeneratePdf() { } // 產生 PDF
public void UploadFile() { } // 上傳檔案
// ... 還有 50 個方法 😱
}
// ✅ 拆成 UserAuthService、EmailService、PdfService 等
錯誤二:用 if-else 處理所有情況
// ❌ 每次加新的付款方式都要改這裡
public decimal ProcessPayment(string method, decimal amount)
{
if (method == "CreditCard") return amount * 0.97m; // 信用卡手續費
else if (method == "LinePay") return amount * 0.98m; // Line Pay 手續費
else if (method == "ApplePay") return amount * 0.985m; // Apple Pay 手續費
// 每次都要加 else if...
return amount; // 預設金額
}
// ✅ 用 IPaymentStrategy 介面 + 各自的實作類別
錯誤三:在建構子裡 new 所有相依物件
// ❌ 寫死相依性,無法單元測試
public class ReportService
{
private readonly DatabaseContext _db = new DatabaseContext(); // 寫死
private readonly EmailService _email = new EmailService(); // 寫死
private readonly PdfGenerator _pdf = new PdfGenerator(); // 寫死
}
// ✅ 改用建構子注入(Constructor Injection)
// public ReportService(IDatabase db, IEmailService email, IPdfGenerator pdf)
📝 SOLID 速查表
| 原則 | 一句話記憶 | 比喻 |
|---|---|---|
| S — 單一職責 | 一個類別只做一件事 | 專業廚師各司其職 |
| O — 開放封閉 | 加新功能不改舊程式碼 | 樂高積木往上疊 |
| L — 里氏替換 | 子類別能安全替換父類別 | 飲料都能喝 |
| I — 介面隔離 | 介面小而專 | 素食者不用點牛排 |
| D — 依賴反轉 | 依賴抽象不依賴具體 | 電器只認插座 |