這句話在軟體開發的討論中很常見,它觸及了一個核心問題:我們應該為「現在的需求」寫程式,還是為「未來的可能性」設計?
從表面上看,這句話是完全合理的,也符合 YAGNI (You Ain’t Gonna Need It) 原則。如果你的程式碼非常簡單,只是一個小工具或腳本,而且你「百分之百確定」未來不會有任何變化,那麼為單一實作建立介面確實會增加不必要的複雜性和檔案數量。
然而,在絕大多數的商業應用和長期維護的專案中,即使當下只有一個實作,使用介面(Interface)仍然是強烈推薦的最佳實踐。
原因如下,這不僅僅是為了「可能的多個實作」:
核心目的:解耦合 (Decoupling) 與依賴反轉 (Dependency Inversion)
這是使用介面最重要的原因。程式碼應該依賴於抽象(介面),而不是依賴於具體的實作(類別)。
想像一個情境:你的 OrderService (訂單服務) 需要發送通知。
不使用介面的寫法:
// 直接依賴具體的 EmailService
public class OrderService {
private EmailService emailService;
public OrderService() {
this.emailService = new EmailService(); // 強耦合!OrderService 自己建立了 EmailService
}
public void placeOrder() {
// ... 處理訂單邏輯 ...
emailService.send("user@example.com", "訂單已成立");
}
}
問題:
OrderService和EmailService緊緊地綁在一起。如果明天老闆說:「我們也要支援簡訊通知!」你該怎麼辦?你必須修改
OrderService的內部程式碼。如果
EmailService的建構子變了(例如需要 API Key),所有使用它的地方(包括OrderService)都可能要改。
使用介面的寫法:
// 1. 定義一個抽象的合約
public interface INotificationService {
void send(String recipient, String message);
}
// 2. 具體的實作
public class EmailService implements INotificationService {
@Override
public void send(String recipient, String message) {
// ... 實作寄送 Email 的邏輯 ...
}
}
// 3. 依賴於抽象,而不是具體
public class OrderService {
private final INotificationService notificationService;
// 透過建構子傳入,這就是「依賴注入」(Dependency Injection)
public OrderService(INotificationService notificationService) {
this.notificationService = notificationService;
}
public void placeOrder() {
// ... 處理訂單邏輯 ...
// OrderService 只知道要「send」,不在乎是怎麼 send 的
notificationService.send("user@example.com", "訂單已成立");
}
}
// 在程式的進入點或設定中,決定要用哪個實作
INotificationService emailSender = new EmailService();
OrderService orderService = new OrderService(emailSender);
orderService.placeOrder();
優點:
OrderService完全不知道EmailService的存在,它只認識INotificationService這份合約。未來擴充:當需要簡訊通知時,只需新增一個
SmsService實作INotificationService,然後在建立OrderService的地方把EmailService換成SmsService即可,OrderService一行程式碼都不用改。
大幅提升可測試性 (Testability)
這是現代軟體開發中極其重要的一環。如果你不寫介面,單元測試會變得非常困難或不可能。
回到剛剛的例子,如果你想測試 OrderService 的 placeOrder 方法,但不希望它真的寄出一封 Email (因為測試應該快速、獨立且不依賴外部系統),該怎麼辦?
沒有介面:你很難阻止
new EmailService()的發生,測試會變得很慢,而且會依賴網路。有介面:你可以輕易地建立一個「假的」通知服務來供測試使用,這就是模擬 (Mocking)。
// 在測試程式碼中
@Test
public void testPlaceOrder() {
// 建立一個假的 Mock 物件
INotificationService mockNotification = Mockito.mock(INotificationService.class);
OrderService orderService = new OrderService(mockNotification);
orderService.placeOrder();
// 驗證:placeOrder 是否真的有呼叫 send 方法
Mockito.verify(mockNotification).send("user@example.com", "訂單已成立");
}
這個測試可以在毫秒內完成,並且完全不依賴外部服務。
定義清晰的邊界與 API (Clear Boundaries & API Definition)
介面是一個公開的「合約」。它清楚地告訴其他開發者:「這個模組提供了這些功能,你們可以這樣使用它」。
關注點分離:介面定義了「What」(做什麼),而實作類別則關心「How」(怎麼做)。這讓程式碼的架構更清晰。
團隊合作:前端和後端團隊,或者不同模組的開發者,可以先定義好介面,然後各自平行開發,只要最後的實作符合介面合約即可。
為了未來的你,以及你的同事
軟體最大的成本在於「維護」。你今天認為「確定只有一個實作」,半年後需求變更,你可能會感謝當初多寫一個介面的自己。
可擴充性:介面是實現開放封閉原則 (Open/Closed Principle) 的關鍵。對擴充開放(可以新增實作),對修改封閉(不用修改依賴它的程式碼)。
可讀性:當你看到一個類別依賴的是
IUserRepository而不是MySqlUserRepository,你立刻就能明白這個類別的設計意圖是與資料庫儲存細節解耦的。
結論
| 優點 | 解釋 | 為什麼即使只有一個實作也很重要 |
|---|---|---|
| 解耦合 | 讓模組之間不直接依賴,而是依賴抽象合約。 | 這是軟體架構健康的基石,讓系統更有彈性。 |
| 可測試性 | 容易用 Mock 物件取代真實依賴,進行單元測試。 | 這是保證程式碼品質和實現 TDD/CI/CD 的前提。 |
| 定義邊界 | 介面是模組的公開 API,隱藏內部實作細節。 | 讓系統各部分的職責更清晰,易於理解和維護。 |
| 可擴充性 | 未來若有新需求,只需新增實作,不需修改既有程式碼。 | 軟體需求總是在變,這是對未來最廉價的投資。 |
所以,這句話可以修正為:
「如果你寫的是一個生命週期極短、功能極其簡單、且幾乎不可能變動的拋棄式腳本,那麼不寫介面是可以接受的。但在任何需要長期維護和團隊合作的專案中,為服務或核心邏輯編寫介面,是為了程式碼的健康、彈性和可測試性所做的必要投資,和當下有幾個實作無關。」