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 Injection(DI,相依性注入)是一種架構手法,將相依(例如介面的實作)從外部傳入物件,通常透過建構子或 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)!
GO TO FULL VERSION