☕ NEW! 完成新手任務即可參加抽獎!LINE 星巴克禮券等你拿,名額有限!        🎉 推廣活動:邀請好友註冊 DevLearn,累積推薦抽 LINE 星巴克禮券! 活動詳情 →        🔥 活動期間 2026/4/1 - 5/31 |已有 0 人參加       
設計模式 中級

🏛️ 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 — 依賴反轉 依賴抽象不依賴具體 電器只認插座

💡 大家的想法 · 0

載入中...
💬 即時聊天室 🟢 0 人在線
😀 😎 🤓 💻 🎮 🎸 🔥
➕ 新問題
📋 我的工單
💬 LINE 社群
🔒
需要註冊才能使用此功能
註冊帳號即可解鎖測驗、遊戲、簽到、筆記下載等所有功能,完全免費!
免費註冊