</> 技術筆記Tech Notes

為什麼即使只有一個實作,也值得寫介面?

這句話在軟體開發的討論中很常見,它觸及了一個核心問題:我們應該為「現在的需求」寫程式,還是為「未來的可能性」設計?

從表面上看,這句話是完全合理的,也符合 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", "訂單已成立");
    }
}
  • 問題OrderServiceEmailService 緊緊地綁在一起。

  • 如果明天老闆說:「我們也要支援簡訊通知!」你該怎麼辦?你必須修改 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)

這是現代軟體開發中極其重要的一環。如果你不寫介面,單元測試會變得非常困難或不可能。

回到剛剛的例子,如果你想測試 OrderServiceplaceOrder 方法,但不希望它真的寄出一封 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,隱藏內部實作細節。 讓系統各部分的職責更清晰,易於理解和維護。
可擴充性 未來若有新需求,只需新增實作,不需修改既有程式碼。 軟體需求總是在變,這是對未來最廉價的投資。

所以,這句話可以修正為:

「如果你寫的是一個生命週期極短、功能極其簡單、且幾乎不可能變動的拋棄式腳本,那麼不寫介面是可以接受的。但在任何需要長期維護和團隊合作的專案中,為服務或核心邏輯編寫介面,是為了程式碼的健康、彈性和可測試性所做的必要投資,和當下有幾個實作無關。」