← Back to list

Day 02|行會規範:畫室開工前,刻在門楣上的五條規矩(SOLID)

本文同樣發表於 2026 iThome 鐵人賽 Day 02|行會規範:畫室開工前,刻在門楣上的五條規矩(SOLID)

Henry Chiang · 2026-08-24 18:27 · 0 claps · 13.3 min read
#code-smells #clean-code #solid-principles #object-oriented
Open on Medium ↗

Day 02|行會規範:畫室開工前,刻在門楣上的五條規矩(SOLID)

本文同樣發表於 2026 iThome 鐵人賽 Day 02|行會規範:畫室開工前,刻在門楣上的五條規矩(SOLID)

在任何一位學徒被允許碰畫筆之前 畫室的門楣上,早就刻好了五條規矩(SOLID)

不是技法,不是配色,是更底層的東西 「一幅畫該怎麼分工、一個角色該扛多少責任、換人代班會不會壞事」

接下來 28 天,會帶你認出 23 種具體的壞味道 但每一種壞味道,回頭問到底,都是在違反這五條規矩的其中一條

今天,就是把這五條規矩,一次刻清楚

為什麼先講規矩,不是先講味道

昨天問了一句話: 「這段程式碼,值不值得留下來?」 但「值不值得」不能只靠直覺,你需要一套標準

就像畫室師傅不會只說「這裡怪怪的」,他能指出「這裡的透視錯了」 因為他心裡有一套關於「什麼是對的透視」的規矩

先把規矩刻清楚,後面認味道,才不會只是死背 23 個名詞。

Code Smell 是症狀,SOLID 是病理學

聞到味道,是第一步 知道這個味道對應哪一條規矩被打破,才能真正說清楚「為什麼要改」,而不是「我覺得要改」

先把規矩刻清楚,後面認味道,才不會只是死背 23 個名詞

S 單一職責原則(Single Responsibility Principle)

一幅畫,只有一位總負責的構圖師 畫室接一張大型委託,會指定一位總負責的構圖師 決定整張畫「該長什麼樣子」

顏料庫存誰在管、跟催收款誰在跑,是另外兩個人的事

如果構圖師同時要管顏料庫存、又要跟催收款,任何一件事出狀況,都會打斷他手上真正該做的事「決定這幅畫該長什麼樣子」

一個類別,管了三件不相干的事

public class OrderReportGenerator
{
    public OrderSummary BuildSummary(List<Order> orders)
    {
        return new OrderSummary(orders.Sum(o => o.Total), orders.Count);
    }

    public string ToHtml(OrderSummary summary)
    {
        return $"<div>總金額:{summary.Total},共 {summary.Count} 筆</div>";
    }

    public void SaveToFile(string html, string path)
    {
        File.WriteAllText(path, html);
    }
}

這個類別,有三個完全不相干的理由會讓它變動:

  • 彙總邏輯變了 — — 例如要多算一欄稅金
  • 排版樣式變了 — — 例如行銷要求換一套 HTML 模板
  • 儲存方式變了 — — 例如要改存雲端,不再存本機檔案

三個互不相關的理由,卻共用同一個類別 改其中一件事,都有可能不小心影響到另外兩件

讓每個角色,只扛一份責任

public class OrderSummaryBuilder
{
    public OrderSummary Build(List<Order> orders) =>
        new OrderSummary(orders.Sum(o => o.Total), orders.Count);
}

public class OrderReportFormatter
{
    public string ToHtml(OrderSummary summary) =>
        $"<div>總金額:{summary.Total},共 {summary.Count} 筆</div>";
}

public class ReportStorage
{
    public void SaveToFile(string content, string path) =>
        File.WriteAllText(path, content);
}

現在

稅金規則變了,只動 OrderSummaryBuilder

模板變了,只動 OrderReportFormatter

三個角色,各自只對一個理由負責

單一職責原則(Single Responsibility Principle):一個類別,應該只有一個引起它變動的理由

這正是模組一「臃腫者」的核心,後續會提到的 Long Method、Large Class,都是這條規矩在方法與類別層級,各自失守的樣子

O 開放封閉原則(Open/Closed Principle):加新配方,不必刮掉舊畫的底層

新技法用「疊加一層」的方式加進去,原本已經畫好、也已經風乾的部分,完全不受影響 畫室接受新配方顏料,不需要把舊畫的底層刮掉重畫

每加一個地區,就要回頭改一次舊方法

public decimal CalculateShippingFee(Order order, string region)
{
    if (region == "TW") return 60m;
    if (region == "HK") return 120m;
    if (region == "JP") return 150m;
    // 明天要支援新加坡,又要多一行 if
    return 200m;
}

這個方法明明已經在 Production 跑得好好的、也已經被測試過 ,卻因為「多一個地區」這種完全不影響既有邏輯的需求,被迫重新修改、重新測試一次

讓新地區,只靠「新增」就能加進來

public interface IShippingFeeStrategy
{
    decimal Calculate(Order order);
}

public class TaiwanShippingFeeStrategy : IShippingFeeStrategy
{
    public decimal Calculate(Order order) => 60m;
}

public class HongKongShippingFeeStrategy : IShippingFeeStrategy
{
    public decimal Calculate(Order order) => 120m;
}

呼叫端透過一個查表用的 Dictionary<string, IShippingFeeStrategy> 取得對應的策略 要支援新加坡,只要新增一個 SingaporeShippingFeeStrategy 原本的呼叫邏輯,一行都不用動

開放封閉原則(Open/Closed Principle):對擴充開放,對修改封閉

新增行為,靠「加一個新類別」,而不是回頭修改一段已經在運作的舊程式碼 Switch Statement 會正式命名這個 Code Smell: 以多型取代條件式

L 里氏替換原則(Liskov Substitution Principle):代班學徒,撐不撐得住原本談好的委託

客戶跟老師傅談好一張委託,如果當天由代班學徒接手 客戶不該感覺到任何差異 — 委託當初講好的每一件事,代班的人都得撐得住

如果代班學徒某個環節做不到,甚至還偷偷加了一條客戶事前不知道的但書 這場代班,就不算真正頂替得住原本的角色

子類別,偷偷加嚴了父類別沒承諾過的條件

public abstract class DiscountCalculator
{
    public abstract decimal Apply(Order order);
}

public class SeasonalDiscountCalculator : DiscountCalculator
{
    public override decimal Apply(Order order) => order.Total * 0.9m;
}

public class MemberOnlyDiscountCalculator : DiscountCalculator
{
    public override decimal Apply(Order order)
    {
        if (!order.Customer.IsMember)
            throw new InvalidOperationException("非會員不適用此折扣");
        return order.Total * 0.8m;
    }
}

呼叫端只認得父類別的約定:

public decimal ApplyBestDiscount(DiscountCalculator calculator, Order order) =>
    calculator.Apply(order);

這段程式碼,換一顆 SeasonalDiscountCalculator 進去,跑得好好的 換一個 MemberOnlyDiscountCalculator 進去,同一張非會員訂單,直接就爆了

呼叫端完全沒有辦法從 DiscountCalculator 這個型別本身,看出「換了子類別,會多一條它沒承諾過的規則」

里氏替換原則(Liskov Substitution Principle):子類別要能安全地取代父類別,出現在任何用到父類別的地方

Refused Bequest,會用另一種更常見的違反樣貌(子類別覆寫方法卻直接拋例外)把這條規矩的代價講得更完整

I 介面隔離原則(Interface Segregation Principle):工具箱按專長分,不強迫每人都背整套

畫室依專長分工具箱, 畫框師傅的工具箱,不會被塞進調色師傅才用得到的乳缽跟研磨棒

每個角色,只需要扛自己專長會用到的那一份工具,不必為了別人的專長,背整套用不到的重物

一個介面,塞了五種不相干的能力

public interface IOrderService
{
    void Process(OrderRequest request);
    decimal CalculateShippingFee(Order order);
    string GenerateInvoice(Order order);
    void SendNotification(Order order);
    void ExportToAccountingSystem(Order order);
}

報表模組只需要「產生發票」這一件事,卻因為要實作 IOrderService ,被迫連帶交代四個用不到的方法:

public class ReportingOrderService : IOrderService
{
    public string GenerateInvoice(Order order) => $"發票:{order.Id}";

    public void Process(OrderRequest request) =>
        throw new NotSupportedException("報表模組不處理訂單");
    public decimal CalculateShippingFee(Order order) =>
        throw new NotSupportedException("報表模組不算運費");
    public void SendNotification(Order order) =>
        throw new NotSupportedException("報表模組不發通知");
    public void ExportToAccountingSystem(Order order) =>
        throw new NotSupportedException("報表模組不匯出會計系統");
}

這其實也會讓里氏替換原則跟著爆炸,但根源不在繼承 在介面本身塞了太多不相干的能力,逼著每一個實作者都得為用不到的方法負責

拆成幾個小介面,各司其職

public interface IOrderProcessor { 
    void Process(OrderRequest request); 
}

public interface IShippingFeeCalculator { 
    decimal CalculateShippingFee(Order order); 
}

public interface IInvoiceGenerator { 
    string GenerateInvoice(Order order); 
}
public interface IOrderProcessor { 
    void Process(OrderRequest request); 
}

public interface IShippingFeeCalculator { 
    decimal CalculateShippingFee(Order order); 
}

public interface IInvoiceGenerator { 
    string GenerateInvoice(Order order); 
}

**ReportingOrderService 現在只需要對它真正做得到的那件事負責,不用假裝自己會處理訂單、算運費、發通知**

介面隔離原則(Interface Segregation Principle):不要強迫任何角色依賴自己用不到的方法

這條規矩不會單獨對應到某一天的壞味道,但接下來每次看到「乾淨的小介面」,背後多半都是這條規矩在撐著

D 依賴反轉原則(Dependency Inversion Principle):畫室認規格,不認人

畫室跟供貨商簽約,認的是行會公告的顏料規格,不是某個特定商人的個人手法 換一個供貨商,只要新規格的顏料符合行會規格,畫室的作業方式完全不用跟著調整

因為畫室從一開始,就只依賴規格,不依賴某個具體的人

直接依賴一個具體實作,而不是一份規格

// 直接依賴一個具體的實作,連連線字串都寫死在裡面
public class OrderProcessor
{
    private readonly SqlOrderRepository _repository =
        new SqlOrderRepository("Server=prod-db;...");

    public void Process(OrderRequest request)
    {
        _repository.Save(request.ToOrder());
    }
}

OrderProcessor 不只依賴「怎麼存訂單」這件事,還依賴了「用 SQL Server 存、連線字串長這樣」的具體細節

想幫它寫測試,或改用別的儲存方式,都得先拆開這一行 new

兩邊都只認同一份規格

public interface IOrderRepository
{
    void Save(Order order);
}

public class OrderProcessor
{
    private readonly IOrderRepository _repository;
    public OrderProcessor(IOrderRepository repository) => _repository = repository;

    public void Process(OrderRequest request) => _repository.Save(request.ToOrder());
}

**OrderProcessor 現在只認一份規格 IOrderRepository 說了什麼,它就依賴什麼,**規格背後是 SQL Server、PostgreSQL,還是測試用的記憶體假物件, OrderProcessor 完全不需要知道

依賴反轉原則(Dependency Inversion Principle):高層模組(業務邏輯)不該依賴低層模組(技術細節)的具體實作,兩者都該依賴同一份抽象規格

事實上,Day 01 那段 OrderProcessor 建構子注入 IEmailServiceISmsService 的寫法,就已經是這條規矩的正確示範。接下來每一段範例的建構子注入,都是延續這條規矩

五條規矩,對照表

23 種壞味道,之後每認出一種,都可以回頭對照這張表 問一句:「它踩到的,是哪一條規矩?」

自我檢查清單

  1. 這個類別/方法,能不能用一句話講完它的職責?如果要用「和」才講得完,可能不只一個職責
  2. 上次新增一種類型或規則,是不是要回頭修改一段舊邏輯,而不是新增一個新類別?
  3. 換一顆子類別進去,呼叫端的行為會不會意外改變、甚至丟出例外?
  4. 有沒有介面塞了太多方法,逼著某些實作者對著用不到的方法拋 NotSupportedException
  5. 程式碼裡有沒有直接 new 出一個具體的技術實作,而不是依賴一份介面?

明日預告

門楣上的五條規矩,刻好了

明天,我們去一個真實存在、到今天都還立在佛羅倫斯天空下的工地看看 「一座蓋了快一百年沒人敢動工的圓頂」

第一站: 過長方法(Long Method) ,正式開工


메타데이터
post_id
c33987c842df
slug
day-02-行會規範-畫室開工前-刻在門楣上的五條規矩-solid-c33987c842df
url
https://medium.com/@henry.chiang/day-02-%E8%A1%8C%E6%9C%83%E8%A6%8F%E7%AF%84-%E7%95%AB%E5%AE%A4%E9%96%8B%E5%B7%A5%E5%89%8D-%E5%88%BB%E5%9C%A8%E9%96%80%E6%A5%A3%E4%B8%8A%E7%9A%84%E4%BA%94%E6%A2%9D%E8%A6%8F%E7%9F%A9-solid-c33987c842df
canonical_url
https://medium.com/@henry.chiang/day-02-%E8%A1%8C%E6%9C%83%E8%A6%8F%E7%AF%84-%E7%95%AB%E5%AE%A4%E9%96%8B%E5%B7%A5%E5%89%8D-%E5%88%BB%E5%9C%A8%E9%96%80%E6%A5%A3%E4%B8%8A%E7%9A%84%E4%BA%94%E6%A2%9D%E8%A6%8F%E7%9F%A9-solid-c33987c842df
author_url
https://medium.com/@henry.chiang
status
ok
fetched_at
2026-08-25 04:38:01