1. 接口即契约:架构之基
在 Java(以及其他语言)中,接口不仅仅是一组方法。这是一份契约:任何实现该接口的类都承诺支持特定的行为。接口规定的是应该实现什么,而不是如何实现。
为什么这很重要?
- 分层与职责分离。借助接口,我们可以把“做什么”和“怎么做”分离。例如,如果你有接口 PaymentService,那么不同的实现可以处理银行卡、PayPal 或加密货币的支付,但调用 pay() 的代码无需关心这些细节。
- 灵活与可扩展性。你可以在不修改其余代码的情况下添加新的接口实现。这对大型团队和长生命周期项目尤为重要。
- 可测试性。借助接口,可以轻松将实现替换为测试用的模拟(mock),而不触动主业务代码。
示例:服务层与 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,例如传入一个“桩对象”来替代真实的消息发送器。
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) {
// 创建“新闻社”(Subject)
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 + " CNY,使用银行卡");
}
}
public class PaypalPayment implements PaymentStrategy {
@Override
public void pay(int amount) {
System.out.println("支付 " + amount + " CNY,通过 PayPal");
}
}
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 CNY,使用银行卡
支付 500 CNY,通过 PayPal
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