8代码整洁之道

边界

边界处理

第三方代码学习测试边界条件

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第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 变化,只需要修改 SensorRepository

2.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 编写学习测试。

用适配器模式封装一个第三方接口。

8/10边界
0%