昨天我們用分層架構把系統切成表現層、邏輯層、資料層三塊。但「層」本身還是可能很肥大——邏輯層裡如果塞了十件不相干的事,一樣會變回 Day 13 講的高耦合,只是搬到了同一層裡發生而已。從今天開始,我們要進入 SOLID 五大原則,把「職責要單一」這件事講得更精確、更有系統。
在軟體開發的過程中,我們常常會遇到程式碼隨著需求增加而變得臃腫、難以維護的情況。SOLID 原則是物件導向設計的五大核心準則,旨在幫助開發者打造低耦合、高內聚且易於擴充的架構。這篇要深入探討其中的第一項原則——單一職責原則(Single Responsibility Principle, SRP)。
單一職責原則的核心概念是:「一個類別或模組應該只有一個修改的原因」。換句話說,每個函式、類別或模組都應該只負責一項特定的任務或功能。當一個模組承載了過多不相干的責任時(例如同時處理商業邏輯、資料庫存取與通知發送),只要其中一項需求變更,就容易牽一髮而動全身,增加產生臭蟲的風險。透過將複雜的功能拆分為獨立的小型模組,我們能大幅提升程式碼的可讀性、可測試性與可維護性。
在初期開發時,可能會把所有功能寫在同一個 OrderManager 類別中,同時處理訂單建立、匯率換算、海外物流追蹤,以及發送 LINE 買家通知:
// 違反 SRP:一個類別做三件不相干的事
class OrderManager {
createOrder(order) {
const rate = this.getExchangeRate(order.currency); // 匯率換算
const total = order.price * rate + order.shippingFee;
db.save(order); // 資料庫存取
this.notifyBuyer(order.buyerId, total); // 通知
}
getExchangeRate(currency) { /* 匯率邏輯 */ }
notifyBuyer(buyerId, total) { /* 呼叫 LINE API */ }
}
違反 SRP 的設計:OrderManager 需負責計算匯率、寫入資料庫及呼叫通訊 API。當 LINE 通知格式改變、或匯率計算公式調整時,都必須修改同一個檔案,導致這個類別因為多種不同原因而需要被修改。
遵循 SRP 的重構方式:把職責拆分成各自獨立的模組:
// 遵循 SRP:拆成各司其職的模組
class ExchangeRateCalculator {
getRate(currency) { /* 匯率邏輯 */ }
}
class OrderRepository {
save(order) { /* 資料庫存取 */ }
}
class LineNotificationService {
notify(buyerId, message) { /* 呼叫 LINE API */ }
}
function createOrder(order) {
const rate = exchangeRateCalculator.getRate(order.currency);
const total = order.price * rate + order.shippingFee;
orderRepository.save({ ...order, total });
lineNotificationService.notify(order.buyerId, `訂單總額 ${total} 元`);
}
OrderRepository:專門負責訂單資料的儲存與讀取。ExchangeRateCalculator:專門負責處理外幣匯率換算邏輯。LineNotificationService:專門負責發送買家狀態通知。透過這樣的拆分,每個模組各司其職,未來若要新增其他通知管道(如 Email),只需調整 LineNotificationService,完全不會影響核心的訂單處理邏輯。
單一職責原則是打造穩固軟體架構的基石。透過確保每個函式和模組只做一件事並做好,我們能有效降低元件間的耦合度,讓程式碼在面臨需求變更時展現更高的彈性與延展性——這也是這五天以來,從壞味道、分層架構到 SRP,一直在重複強調的同一件事:把不相干的職責分開,系統才不會牽一髮動全身。把 Day 13 到 Day 15 這三天串起來看,反而覺得是一條很順的路——先看到義大利麵程式碼有多難改,再看到分層架構怎麼把職責攤開,最後才懂 SRP 講的「一個類別只能有一個改變的理由」,其實就是分層原則往下再收斂一層的具體規則。