Day 02|行會規範:畫室開工前,刻在門楣上的五條規矩(SOLID)
本文同樣發表於 2026 iThome 鐵人賽 Day 02|行會規範:畫室開工前,刻在門楣上的五條規矩(SOLID)
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建構子注入IEmailService、ISmsService的寫法,就已經是這條規矩的正確示範。接下來每一段範例的建構子注入,都是延續這條規矩
五條規矩,對照表

23 種壞味道,之後每認出一種,都可以回頭對照這張表 問一句:「它踩到的,是哪一條規矩?」
自我檢查清單
- 這個類別/方法,能不能用一句話講完它的職責?如果要用「和」才講得完,可能不只一個職責
- 上次新增一種類型或規則,是不是要回頭修改一段舊邏輯,而不是新增一個新類別?
- 換一顆子類別進去,呼叫端的行為會不會意外改變、甚至丟出例外?
- 有沒有介面塞了太多方法,逼著某些實作者對著用不到的方法拋
NotSupportedException? - 程式碼裡有沒有直接
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