第8章 边界
一、导读
1.1 本章学习目标
第8章讨论了如何处理代码边界。Uncle Bob 指出,我们经常使用第三方代码(框架、库),但如果不加控制,这些代码会污染整个代码库。本章的核心学习目标包括:
- 理解使用第三方代码的风险:接口可能变化。
- 掌握包装类:隔离第三方代码的影响。
- 学会学习测试:验证你对第三方代码的理解。
- 掌握边界条件:测试极端情况。
- 理解封装边界:让系统其他部分不受边界影响。
- 学会适配器模式:转换第三方接口。
1.2 为什么本章如此重要
现代软件开发大量使用第三方库和框架。Uncle Bob 指出:如果不加控制,第三方代码会渗透到你代码库的每个角落。当第三方接口变化时,你需要修改大量代码。
本章的重要性在于它提供了管理第三方代码边界的策略。你会理解如何用包装类隔离第三方代码,如何编写学习测试来理解 API,如何处理边界条件。
1.3 核心问题引导
你的代码中有多少第三方代码?它们渗透到了哪些地方?
如果第三方 API 变化,你需要修改多少代码?
你编写过学习测试吗?
如何处理边界条件?
如何用适配器模式隔离第三方代码?
二、核心概念详解
2.1 使用第三方代码的风险
2.1.1 接口可能变化
java
// 差:第三方代码渗透到整个代码库
public class OrderService {
// 直接使用第三方 Map
private Map<String, Sensor> sensors;
public void addSensor(String id, Sensor sensor) {
sensors.put(id, sensor);
}
public double getTemperature(String id) {
Sensor s = sensors.get(id);
return s.readTemperature(); // 直接调用第三方方法
}
}
// 如果 Sensor API 变化,需要修改所有使用它的地方2.2 包装类
2.2.1 隔离第三方代码
java
// 好:用包装类隔离
public class SensorRepository {
private final Map<String, Sensor> sensors = new HashMap<>();
public void addSensor(String id, Sensor sensor) {
sensors.put(id, sensor);
}
public double getTemperature(String id) {
Sensor sensor = sensors.get(id);
if (sensor == null) {
throw new SensorNotFoundException("Sensor not found: " + id);
}
// 包装第三方调用
return convertToCelsius(sensor.readTemperature());
}
private double convertToCelsius(double fahrenheit) {
return (fahrenheit - 32) * 5 / 9;
}
}
// 系统其他部分只与 SensorRepository 交互
// 如果 Sensor API 变化,只需要修改 SensorRepository2.3 学习测试
2.3.1 验证对第三方代码的理解
java
// 学习测试:验证你对第三方 API 的理解
public class ThirdPartyApiTest {
@Test
public void testApiBehavior() {
// 测试第三方 API 的实际行为
ThirdPartyApi api = new ThirdPartyApi();
// 你期望的行为
Result result = api.doSomething("input");
assertEquals("expected", result.getValue());
// 边界情况
Result nullResult = api.doSomething(null);
// 发现:返回 null 而不是抛异常
assertNull(nullResult);
}
@Test
public void testApiEdgeCases() {
ThirdPartyApi api = new ThirdPartyApi();
// 测试空字符串
Result empty = api.doSomething("");
// 发现:返回空结果
// 测试超长字符串
Result long = api.doSomething("a".repeat(10000));
// 发现:抛出异常
}
}2.4 边界条件
2.4.1 测试极端情况
java
// 边界条件测试
public class OrderServiceTest {
@Test
public void processOrder_withZeroItems_shouldThrow() {
Order order = new Order(Collections.emptyList());
assertThrows(IllegalArgumentException.class,
() -> orderService.processOrder(order));
}
@Test
public void processOrder_withMaxItems_shouldSucceed() {
List<OrderItem> items = generateItems(1000); // 最大数量
Order order = new Order(items);
OrderResult result = orderService.processOrder(order);
assertTrue(result.isSuccess());
}
@Test
public void processOrder_withNegativePrice_shouldThrow() {
OrderItem item = new OrderItem("P001", -10.0, 1);
Order order = new Order(List.of(item));
assertThrows(IllegalArgumentException.class,
() -> orderService.processOrder(order));
}
}2.5 适配器模式
2.5.1 转换第三方接口
java
// 第三方接口
public interface ThirdPartyPaymentGateway {
PaymentResponse charge(String merchantId, double amount, String currency);
}
// 我们的接口
public interface PaymentGateway {
PaymentResult charge(MerchantId merchantId, Money amount);
}
// 适配器:转换接口
public class ThirdPartyPaymentAdapter implements PaymentGateway {
private final ThirdPartyPaymentGateway thirdParty;
public ThirdPartyPaymentAdapter(ThirdPartyPaymentGateway thirdParty) {
this.thirdParty = thirdParty;
}
@Override
public PaymentResult charge(MerchantId merchantId, Money amount) {
// 转换参数
String merchantIdStr = merchantId.getValue();
double amountValue = amount.getValue();
String currency = amount.getCurrency();
// 调用第三方
PaymentResponse response = thirdParty.charge(
merchantIdStr, amountValue, currency);
// 转换结果
if (response.isSuccess()) {
return PaymentResult.success(response.getTransactionId());
} else {
return PaymentResult.failure(response.getErrorMessage());
}
}
}三、实战案例
3.1 案例一:封装第三方库
java
// 改善前:直接使用第三方库
public class EmailService {
// 直接使用 JavaMail API
public void send(String to, String subject, String body) {
Properties props = new Properties();
props.put("mail.smtp.host", "smtp.example.com");
Session session = Session.getDefaultInstance(props);
MimeMessage message = new MimeMessage(session);
message.setFrom(new InternetAddress("noreply@example.com"));
message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(to));
message.setSubject(subject);
message.setText(body);
Transport.send(message);
}
}
// 改善后:封装第三方库
public class EmailService {
private final EmailClient emailClient;
public EmailService(EmailClient emailClient) {
this.emailClient = emailClient;
}
public void sendEmail(String to, String subject, String body) {
Email email = Email.builder()
.to(to)
.from("noreply@example.com")
.subject(subject)
.body(body)
.build();
emailClient.send(email);
}
}
// EmailClient 封装了第三方库的细节
public class JavaMailEmailClient implements EmailClient {
@Override
public void send(Email email) {
// 所有 JavaMail 的细节都在这里
// 如果更换邮件库,只需要修改这个类
}
}四、本章小结
第8章讲解了如何处理代码边界,核心要点包括:
第三方代码的风险:接口可能变化,会渗透到代码库。
包装类隔离:用包装类隔离第三方代码。
学习测试:验证你对第三方 API 的理解。
边界条件:测试极端情况。
适配器模式:转换第三方接口。
Uncle Bob 的核心观点是:管理好边界,让第三方代码的变化不影响你的系统。
五、思考与练习
思考题
你的代码中有多少第三方代码?它们渗透到了哪些地方?
如果第三方 API 变化,你需要修改多少代码?
你编写过学习测试吗?
编程练习
为一段使用第三方库的代码编写包装类。
为一个第三方 API 编写学习测试。
用适配器模式封装一个第三方接口。