CodeGym /課程 /JAVA 25 SELF /Java 架構中的介面與設計模式

Java 架構中的介面與設計模式

JAVA 25 SELF
等級 20 , 課堂 4
開放

1. 介面作為契約:架構的基石

在 Java(以及其他語言)中,介面不僅是方法的集合。它是一份契約:任何實作該介面的類別都承諾支援特定行為。介面定義的是要做什麼,而不是如何去做

為什麼這很重要?

  • 分層與關注點分離。 借助介面,我們可以將「做什麼」與「怎麼做」分離。舉例來說,若你有介面 PaymentService,不同的實作可以處理信用卡、PayPal 或加密貨幣的付款,但呼叫 pay() 的程式碼不需關心細節。
  • 彈性與可擴充性。 你可以新增介面的新實作,而不必修改其餘程式碼。這在大型團隊與長期維護的專案中特別重要。
  • 可測試性。 有了介面,就能輕鬆以測試替身(mocks)取代實作,而不影響主程式碼。

範例:Service 層與 DAO

來看商務應用中的典型範例。假設我們有一個用於處理使用者的介面:


public interface UserRepository {
    User findById(int id);
    void save(User user);
}

在不同情境下,我們可以用不同方式實作這個介面:

  • DatabaseUserRepository — 將使用者儲存在資料庫中。
  • InMemoryUserRepository — 將使用者儲存在記憶體中(方便測試)。
  • FileUserRepository — 將使用者儲存在檔案中。

與使用者互動的程式碼只依賴介面:

public class UserService {
    private final UserRepository userRepository;

    // 透過建構子注入依賴
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public void registerUser(User user) {
        userRepository.save(user);
    }
}

現在我們可以輕鬆替換 UserRepository 的實作,而無需修改服務的程式碼。

2. Dependency Injection(相依性注入)與介面的角色

Dependency InjectionDI,相依性注入)是一種架構手法,將相依(例如介面的實作)從外部傳入物件,通常透過建構子或 setter。這讓應用程式更加彈性、易於測試且可擴充。

為什麼介面對 DI 很重要?

若在程式碼中硬編碼某個實作,要替換它就很困難。透過介面,我們可以在不改變主程式碼的前提下,任意切換實作。

相依性注入範例

public interface NotificationSender {
    void send(String message);
}

public class EmailNotificationSender implements NotificationSender {
    @Override
    public void send(String message) {
        System.out.println("正在傳送 Email: " + message);
    }
}

public class SmsNotificationSender implements NotificationSender {
    @Override
    public void send(String message) {
        System.out.println("正在傳送 SMS: " + message);
    }
}

// 使用 NotificationSender 的類別
public class NotificationService {
    private final NotificationSender sender;

    public NotificationService(NotificationSender sender) {
        this.sender = sender;
    }

    public void notifyUser(String message) {
        sender.send(message);
    }
}

現在你可以輕鬆測試 NotificationService,例如傳入「stub」來替代真正的訊息發送者。

3. 設計模式與介面

介面不僅關乎架構,也關乎設計模式。許多模式若無介面將難以實現。以下介紹最常見的幾種。

Observer(觀察者)

Observer 是一種讓某個物件(被觀察者)在狀態改變時,通知其他物件(觀察者)的模式。

UML 圖(簡化):

+------------------+         +------------------------+
|   Subject        |<------->|   Observer             |
+------------------+         +------------------------+
| +addObserver()   |         | +update()              |
| +removeObserver()|         +------------------------+
| +notifyObservers()|
+------------------+

程式碼範例:

import java.util.ArrayList;
import java.util.List;

// 觀察者介面
public interface Observer {
    void update(String event);
}

// 主體介面
public interface Subject {
    void addObserver(Observer observer);
    void removeObserver(Observer observer);
    void notifyObservers(String event);
}

// 主體實作
public class NewsAgency implements Subject {
    private List<Observer> observers = new ArrayList<>();

    @Override
    public void addObserver(Observer observer) {
        observers.add(observer);
    }

    @Override
    public void removeObserver(Observer observer) {
        observers.remove(observer);
    }

    @Override
    public void notifyObservers(String event) {
        for (Observer observer : observers) {
            observer.update(event);
        }
    }
}

// 觀察者實作
public class NewsReader implements Observer {
    private String name;

    public NewsReader(String name) {
        this.name = name;
    }

    @Override
    public void update(String event) {
        System.out.println(name + " 收到新聞: " + event);
    }
}

// 執行範例的主類別
public class ObserverExample {
    public static void main(String[] args) {
        // 建立「新聞機構」(主體)
        NewsAgency agency = new NewsAgency();

        // 建立觀察者
        Observer alice = new NewsReader("Alice");
        Observer bob = new NewsReader("Bob");

        // 訂閱觀察者到新聞
        agency.addObserver(alice);
        agency.addObserver(bob);

        // 發送新聞
        agency.notifyObservers("發佈了新的 Java 版本!");

        // 移除一位觀察者,然後再發送一則新聞
        agency.removeObserver(bob);
        agency.notifyObservers("下一則給訂閱者的新聞");
    }
}

結果:

Alice 收到新聞: 發佈了新的 Java 版本!
Bob 收到新聞: 發佈了新的 Java 版本!

Strategy(策略)

Strategy 是在執行期間選擇行為演算法,而不需修改客戶端程式碼的模式。

UML 圖(簡化):

+------------------+
|   Context        |
+------------------+
| -strategy: Strat.|
| +setStrategy()   |
| +execute()       |
+------------------+
         |
         v
+------------------+
|   Strategy       |<-------------------------+
+------------------+                          |
| +execute()       |                          |
+------------------+                          |
         ^                                    |
         |                                    |
+------------------+   +------------------+   |
|   ConcreteA      |   |   ConcreteB      |---+
+------------------+   +------------------+
| +execute()       |   | +execute()       |
+------------------+   +------------------+

程式碼範例:

public interface PaymentStrategy {
    void pay(int amount);
}

public class CreditCardPayment implements PaymentStrategy {
    @Override
    public void pay(int amount) {
        System.out.println("使用信用卡支付 " + amount + " 盧布");
    }
}

public class PaypalPayment implements PaymentStrategy {
    @Override
    public void pay(int amount) {
        System.out.println("透過 PayPal 支付 " + amount + " 盧布");
    }
}

public class OnlineStore {
    private PaymentStrategy paymentStrategy;

    public void setPaymentStrategy(PaymentStrategy strategy) {
        this.paymentStrategy = strategy;
    }

    public void checkout(int amount) {
        paymentStrategy.pay(amount);
    }
}

// 使用方式:
OnlineStore store = new OnlineStore();
store.setPaymentStrategy(new CreditCardPayment());
store.checkout(1000);

store.setPaymentStrategy(new PaypalPayment());
store.checkout(500);

結果:

使用信用卡支付 1000 盧布
透過 PayPal 支付 500 盧布

Command(命令)

Command 是將請求封裝為物件,從而能以參數傳遞動作的模式。

程式碼範例:

public interface Command {
    void execute();
}

public class LightOnCommand implements Command {
    @Override
    public void execute() {
        System.out.println("燈已開!");
    }
}

public class LightOffCommand implements Command {
    @Override
    public void execute() {
        System.out.println("燈已關!");
    }
}

public class RemoteControl {
    private Command command;

    public void setCommand(Command command) {
        this.command = command;
    }

    public void pressButton() {
        command.execute();
    }
}

// 使用方式:
RemoteControl remote = new RemoteControl();
remote.setCommand(new LightOnCommand());
remote.pressButton(); // 燈已開!

remote.setCommand(new LightOffCommand());
remote.pressButton(); // 燈已關!

4. 在架構中使用介面的優點

  • 低耦合(Low Coupling)。 程式碼只依賴介面,而非特定實作。這能簡化替換、測試與擴充。
  • 可測試性。 撰寫單元測試時,能輕鬆以 mock/stub 取代真實實作。
  • 可擴充性。 可在不修改既有程式碼的情況下,新增介面實作——遵循開放封閉原則(OCP)。
  • 平行開發。 只要有共同介面,多個團隊即可各自獨立實作系統的不同部分。
  • 架構彈性。 更容易導入新的模式與方法。

5. 在架構中使用介面的常見錯誤

錯誤 № 1:對實作的硬式綁定。
若你在各處都直接使用具體類別而非介面,任何實作的變動都會迫使你在多處改動程式碼。請盡量在「介面層級」進行程式設計。

錯誤 № 2:介面過於龐大(God Interface)。
介面應該精簡並針對單一責任。不要把所有東西都塞進同一個介面——否則實作會變得沉重且混亂。

錯誤 № 3:忽視可測試性的優勢。
如果你沒有利用介面在測試中替換相依,測試可能會變得緩慢且不穩定,尤其是在涉及真實資料庫或網路時。

錯誤 № 4:有多個實作,卻沒有使用 DI。
若你為介面做了多個實作,卻在程式碼中硬編碼其中之一,你就失去了架構的彈性。請使用相依性注入(DI)!

1
任務
JAVA 25 SELF, 等級 20, 課堂 4
上鎖
彈性日誌系統:主控台或檔案
彈性日誌系統:主控台或檔案
1
任務
JAVA 25 SELF, 等級 20, 課堂 4
上鎖
用戶通知:靈活的發送方式
用戶通知:靈活的發送方式
1
任務
JAVA 25 SELF, 等級 20, 課堂 4
上鎖
多種問候風格:友好或正式
多種問候風格:友好或正式
1
任務
JAVA 25 SELF, 等級 20, 課堂 4
上鎖
新聞頻道:發布者與訂閱者
新聞頻道:發布者與訂閱者
1
問卷/小測驗
介面,等級 20,課堂 4
未開放
介面
介面的概念
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION