《代码整洁之道》第8章 边界 - 笔记

  • 甚至是你团队里其他组写的你无法随意修改的代码。

这些外部代码是你的**"边界"。它们可能会升级、可能会有 Bug、可能会有反人类的设计、甚至你将来可能想换一个类似的库或服务。如果你的应用代码直接且紧密地依赖**这些外部代码的具体类、方法、异常等细节,那么一旦外部代码发生变化,你的大量应用代码就可能需要修改,变得非常脆弱和难以维护。

我们要将外来代码干净利落的整合进自己的项目中。

使用边界层分隔第三方程序

举例说明

当你使用的第三方库或外部系统返回给你一个通用的数据结构(比如 Map,或者 List<String> 等),并且这个数据结构里的数据的意义和结构(比如 Map 的 Key 是什么,Value 是什么类型)是由第三方定义的,那么你的代码就和这个第三方的具体实现细节耦合(绑定)在了一起

场景: 假设你使用了一个第三方库,这个库提供一个方法来获取某个配置信息。这个方法返回给你一个 Map<String, String>

java 复制代码
// --- 假设这是第三方库的代码 ---
package com.thirdparty.config;

import java.util.HashMap;
import java.util.Map;

class ThirdPartyConfigLib {
    // 这个方法返回配置信息,使用 Map<String, String>
    public Map<String, String> getConfiguration(String configName) {
        Map<String, String> config = new HashMap<>();
        // 这些 Key ("timeout", "retries", "featureEnabled") 是第三方库定义好的
        config.put("timeout", "5000");
        config.put("retries", "3");
        config.put("featureEnabled", "true");
        // 第三方库可能随时添加、删除或修改这些 Key
        return config;
    }
}

你的应用代码(直接使用第三方返回的 Map):

java 复制代码
package com.myproject.app;

import com.thirdparty.config.ThirdPartyConfigLib;
import java.util.Map; // 需要导入 java.util.Map

class MyAppConfig {
    private ThirdPartyConfigLib configLib = new ThirdPartyConfigLib();

    public int getTimeout() {
        Map<String, String> config = configLib.getConfiguration("network");
        // !!! 你的代码必须知道第三方库返回的 Map 里有一个 Key 叫 "timeout" !!!
        // !!! 并且知道 Value 是 String 类型,需要手动转换为 int !!!
        String timeoutStr = config.get("timeout");
        if (timeoutStr != null) {
            try {
                return Integer.parseInt(timeoutStr);
            } catch (NumberFormatException e) {
                // 格式错误,也需要处理
                e.printStackTrace();
                return 1000; // 使用默认值
            }
        }
        return 1000; // 如果 Key 不存在,使用默认值
    }

    public boolean isFeatureEnabled() {
        Map<String, String> config = configLib.getConfiguration("network");
        // !!! 你的代码必须知道 Key 叫 "featureEnabled" !!!
        // !!! 并且知道 Value 是 String 类型,需要手动转换为 boolean !!!
        String enabledStr = config.get("featureEnabled");
         if (enabledStr != null) {
             return Boolean.parseBoolean(enabledStr);
         }
         return false; // 默认不启用
    }
    // ... 更多依赖 Map Key 的方法 ...
}

问题在于:

  • 耦合: 你的 MyAppConfig 类需要了解第三方库返回的 Map内部结构 (具体的 Key 字符串 "timeout", "retries", "featureEnabled")以及 Value 的类型(都是 String,需要手动转换)。你的代码和第三方库的这个 Map 结构紧紧地耦合在一起了。
  • 脆弱性: 如果第三方库在新版本中,把 Key "timeout" 改成了 "requestTimeout",或者把 Value 从 String 改成了 int(虽然 Map 限制了是 String,但如果第三方返回 Map&lt;String, Object> 就更乱了),或者删除了某个 Key,你的 MyAppConfig 类中的相关方法就会立即出错(比如 config.get("timeout") 返回 null,或者类型转换失败)。
  • 不直观: 你的应用代码看到 Map<String, String> 并不知道里面具体有什么配置项,需要查阅第三方库的文档才能知道有哪些 Key 可用。

**解决:**自己写一个要用到的第三方配置类,然后加一个边界层,专门用来对接第三方程序,返回一个配置类给业务代码使用,这样以后第三方程序发生变动,我们只需要找到对应的边界层修改,配置类不修改,业务代码也不用修改,否则在业务代码中找第三方程序参数修改能把人累死。

java 复制代码
package com.myproject.app; // 你的应用核心层

// 定义一个你自己的配置类,不依赖于第三方的 Map 结构
class AppConfigSettings {
    private int timeout;
    private int retries;
    private boolean featureEnabled;

    // 使用构造函数来接收处理好的数据
    public AppConfigSettings(int timeout, int retries, boolean featureEnabled) {
        this.timeout = timeout;
        this.retries = retries;
        this.featureEnabled = featureEnabled;
    }

    public int getTimeout() { return timeout; }
    public int getRetries() { return retries; }
    public boolean isFeatureEnabled() { return featureEnabled; }
    // Getters...
}

// 边界层:负责与第三方库的 Map 打交道,并转换为我们自己的对象
class ThirdPartyConfigAdapter { // 也可以叫 ThirdPartyConfigWrapper

    private ThirdPartyConfigLib configLib = new ThirdPartyConfigLib();

    /**
     * 从第三方库获取配置,并转换为我们自己的 AppConfigSettings 对象。
     * 隐藏了第三方 Map 的细节。
     */
    public AppConfigSettings getAppSettings(String configName) {
        Map<String, String> configMap = configLib.getConfiguration(configName);

        // !!! 在边界层内部处理 Map 的 Key 和 Value 转换细节 !!!
        // 即使第三方改了 Key,只需要改这里
        int timeout = 1000; // 默认值
        String timeoutStr = configMap.get("timeout"); // 这里的 Key 是第三方定义的
        if (timeoutStr != null) {
            try {
                timeout = Integer.parseInt(timeoutStr);
            } catch (NumberFormatException e) {
                // 在边界层处理转换错误,可以记录日志或使用默认值
                System.err.println("Warning: Invalid timeout format from third party.");
            }
        }

        int retries = 1; // 默认值
        String retriesStr = configMap.get("retries"); // 这里的 Key 是第三方定义的
        if (retriesStr != null) {
             try {
                retries = Integer.parseInt(retriesStr);
            } catch (NumberFormatException e) {
                System.err.println("Warning: Invalid retries format from third party.");
            }
        }

        boolean featureEnabled = false; // 默认值
        String enabledStr = configMap.get("featureEnabled"); // 这里的 Key 是第三方定义的
         if (enabledStr != null) {
             featureEnabled = Boolean.parseBoolean(enabledStr);
         }

        // 返回我们自己定义好的对象,隐藏了底层 Map 的存在
        return new AppConfigSettings(timeout, retries, featureEnabled);
    }
}

// 你的应用代码(使用封装后的 Adapter)
class MyAppConfig {
    // 依赖的是我们自己定义的 Adapter
    private ThirdPartyConfigAdapter configAdapter = new ThirdPartyConfigAdapter();

    public int getTimeout() {
        // 调用 Adapter 的方法,直接获取我们自己的 AppConfigSettings 对象
        AppConfigSettings settings = configAdapter.getAppSettings("network");
        // !!! 直接调用我们自己的对象的方法,完全不知道底层是 Map !!!
        return settings.getTimeout();
    }

    public boolean isFeatureEnabled() {
        // 直接调用我们自己的对象的方法
        AppConfigSettings settings = configAdapter.getAppSettings("network");
        return settings.isFeatureEnabled();
    }
     // ... 其他方法都依赖 AppConfigSettings 对象 ...
}

Adapter 模式分隔第三方程序

Adapter 模式是什么?

  • 目的: 将一个类的接口,转换成客户期望的另一个接口。让原本接口不兼容的那些类可以一起工作。
  • 角色:
    • 目标接口 (Target Interface): 这是客户端(你的应用代码)期望使用的接口。
    • 被适配者 (Adaptee): 这是那个接口你不兼容的第三方类或对象。
    • 适配器 (Adapter): 这是一个类,它实现目标接口 ,同时内部持有被适配者的实例。适配器的方法会将对目标接口的调用,转发并转换成对被适配者方法的调用。

Adapter 模式如何用于边界层?

当你使用第三方库时:

  • 你的应用代码 期望使用一个符合你项目规范、易于理解的接口(目标接口)。
  • 第三方库 提供的 API 是一个不兼容的接口(被适配者)。

这时,你可以创建一个适配器类,它实现了你期望的接口,并在内部使用第三方库的功能来完成实际工作。你的应用代码就只需要与你定义的这个"目标接口"打交道,完全不知道背后是哪个第三方库在干活。

代码例子:使用 Adapter 模式封装第三方日志库

假设你不想直接使用第三方日志库(比如 Log4j 或 Logback)的 API,而是想在你的应用中定义一个简单的日志接口,并且能够方便地切换底层日志实现。

java 复制代码
// --- 你的应用代码,定义了目标接口 ---
package com.myproject.app.logging;

// 应用内部使用的标准日志接口
public interface AppLogger {
    void logInfo(String message); // 记录信息日志
    void logError(String message, Throwable error); // 记录错误日志
}

假设的第三方日志库(Adaptee): 这是一个第三方库,它有自己的日志类和方法名,与你的 AppLogger 接口不完全兼容。

java 复制代码
// --- 假设这是第三方日志库的代码 ---
package com.thirdparty.loglib;

// 第三方日志库的日志类 (接口可能不兼容 AppLogger)
public class ThirdPartyLogger {
    private String name;

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

    // 第三方库记录信息日志的方法 (方法名和参数可能与你的接口不同)
    public void logMessage(String msg) {
        System.out.println("[INFO] (" + name + ") " + msg);
    }

    // 第三方库记录错误日志的方法 (方法名和参数可能与你的接口不同)
    public void logErrorMsg(String msg, Throwable e) {
        System.err.println("[ERROR] (" + name + ") " + msg);
        if (e != null) {
            e.printStackTrace(System.err);
        }
    }

    // 第三方库可能有自己的配置、初始化方法等,这里省略
}

创建适配器(Adapter): 这个类实现了你的 AppLogger 接口,并在内部使用了 ThirdPartyLogger。它将对 AppLogger 方法的调用,"适配"到 ThirdPartyLogger 相应的方法上。

java 复制代码
// --- 你的应用代码,实现了适配器 ---
package com.myproject.app.logging.adapters; // 通常放在 adapters 包下

import com.myproject.app.logging.AppLogger; // 导入你的目标接口
import com.thirdparty.loglib.ThirdPartyLogger; // 导入第三方库

// 这是一个适配器类,将 ThirdPartyLogger 适配到 AppLogger 接口
public class ThirdPartyLoggerAdapter implements AppLogger {

    private final ThirdPartyLogger thirdPartyLogger; // 内部持有被适配者 (第三方 Logger)

    // 构造函数接收必要的参数来初始化第三方 Logger
    public ThirdPartyLoggerAdapter(String loggerName) {
        // 在适配器内部处理第三方 Logger 的初始化
        this.thirdPartyLogger = new ThirdPartyLogger(loggerName);
    }

    // !!! 实现 AppLogger 接口的方法 !!!
    // 在实现方法内部,调用第三方 Logger 的相应方法
    @Override
    public void logInfo(String message) {
        // 将 logInfo 的调用适配到 thirdPartyLogger.logMessage
        thirdPartyLogger.logMessage(message);
    }

    @Override
    public void logError(String message, Throwable error) {
        // 将 logError 的调用适配到 thirdPartyLogger.logErrorMsg
        thirdPartyLogger.logErrorMsg(message, error);
    }

    // 如果 AppLogger 有其他方法,也要在这里实现并适配到第三方 Logger 的方法
}

你的应用代码(使用适配器后的方式):

你的应用代码现在只需要依赖你自己的 AppLogger 接口,而不需要知道 ThirdPartyLoggerAdapterThirdPartyLogger 的存在。

java 复制代码
// --- 你的应用代码,使用 AppLogger 接口 ---
package com.myproject.app.service;

import com.myproject.app.logging.AppLogger; // 只导入你的接口
// 假设这里通过某种方式获取 AppLogger 的实现 (比如工厂或依赖注入)
// 实际项目中,你会在初始化时决定使用哪个日志适配器 (比如 ThirdPartyLoggerAdapter)
// 并将其注入到需要日志功能的类中

class BusinessService {
    private final AppLogger logger; // 依赖的是你自己的 AppLogger 接口

    // 构造函数通过接口注入 logger 实例
    public BusinessService(AppLogger logger) {
        this.logger = logger;
    }

    public void processOrder(String orderId) {
        logger.logInfo("开始处理订单: " + orderId); // 调用你自己的接口方法

        try {
            // ... 处理订单的业务逻辑 ...
            // 模拟一个可能出错的操作
            if (Math.random() < 0.2) {
                throw new RuntimeException("模拟处理订单时出错");
            }
            logger.logInfo("订单 " + orderId + " 处理成功。");

        } catch (Exception e) {
            // 使用你的接口方法记录错误
            logger.logError("处理订单 " + orderId + " 失败!", e); // 调用你自己的接口方法
            // ... 处理失败逻辑 ...
        }
    }
}

// --- 应用启动时的初始化代码 (或者依赖注入框架配置) ---
class AppRunner {
    public static void main(String[] args) {
        // !!! 在应用的入口或配置层,决定使用哪个具体的日志实现 !!!
        // 这里使用 ThirdPartyLoggerAdapter 作为 AppLogger 的实现
        AppLogger appLoggerInstance = new ThirdPartyLoggerAdapter("OrderProcessor");

        // 将这个实例传递给需要日志功能的业务类
        BusinessService businessService = new BusinessService(appLoggerInstance);

        // 运行业务逻辑
        businessService.processOrder("ORDER_123");
        businessService.processOrder("ORDER_456");
    }
}

这个例子展示了 Adapter 模式作为边界层的好处:

  1. 隔离第三方细节: BusinessService 完全不知道 ThirdPartyLogger 的存在,它只认识 AppLogger 接口。第三方库的类名、方法名、构造函数等细节都被隐藏在 ThirdPartyLoggerAdapter 内部。
  2. 易于替换底层实现: 如果将来你想换成另一个日志库(比如提供 AnotherLogger 类),你只需要编写一个新的适配器 AnotherLoggerAdapter,让它也实现 AppLogger 接口,并在内部使用 AnotherLogger。然后修改 AppRunner 中初始化 AppLogger 实例的代码,将其切换为 new AnotherLoggerAdapter(...) 即可。BusinessService 的代码完全不需要改动
  3. 提供统一接口: 无论底层用什么日志库,你的应用代码都通过统一的 AppLogger 接口来记录日志,代码风格保持一致。
  4. 适配 API: 适配器将第三方库与你的应用所需的接口进行了转换,即使第三方库的方法名或参数不同,你也能在适配器中进行映射。

当你刚开始接触一个陌生的第三方库时,你应该怎么去了解它,怎么和它"玩熟",而不是直接把它集成到你的核心业务代码里。

不要直接在你的正式项目代码中使用第三方库的功能。你应该在一个独立的、临时的地方,去尝试调用它的各种 API,看看它怎么工作,在什么情况下会抛出异常,有什么奇怪的行为或限制。

推荐方法:编写"学习测试"(Learning Tests)。 这不是你项目中的单元测试,而是一种特殊的测试,目的不是验证你的代码是否正确,而是用来验证你对第三方库的理解是否正确。

  • 比如,如果你要用一个新的 HTTP 客户端库,你可以写一个简单的测试,尝试用它发 GET 请求,看看它返回什么,错误连接时抛什么异常,设置超时怎么起作用等等。
  • 这些学习测试可以帮助你快速摸清第三方库的脾气,理解它的 API 设计,发现它可能存在的陷阱。

为什么重要: 通过独立的学习测试,你对第三方库有了清晰的认识后,再设计你的边界层就会更有谱。你知道边界层需要封装哪些功能,需要处理哪些特定的第三方异常,如何将第三方的数据结构转换成你的应用需要的格式。这避免了在正式代码中"摸着石头过河",写出紧耦合、脆弱的代码。

使用边界

了解了第三方库,并设计并实现了你的边界层之后,你的应用代码应该如何与这个边界层打交道

核心思想: 你的应用核心代码(业务逻辑)只应该调用你设计的那个边界层(Wrapper/Adapter)提供的接口或方法。它不应该再直接引用或调用任何第三方库的类或方法。

为什么重要: 这是在实践中实现"隔离变化"的关键一步。通过只依赖边界层,你的应用核心代码就与底层的第三方实现解耦了。将来第三方库升级、更换,甚至你决定自己实现这部分功能,只要你边界层对外的接口不变,你的核心业务代码就完全不需要改动

相关推荐
我爱挣钱我也要早睡!1 天前
Java 复习笔记
java·开发语言·笔记
汇能感知1 天前
摄像头模块在运动相机中的特殊应用
经验分享·笔记·科技
阿巴Jun1 天前
【数学】线性代数知识点总结
笔记·线性代数·矩阵
茯苓gao1 天前
STM32G4 速度环开环,电流环闭环 IF模式建模
笔记·stm32·单片机·嵌入式硬件·学习
是誰萆微了承諾1 天前
【golang学习笔记 gin 】1.2 redis 的使用
笔记·学习·golang
DKPT1 天前
Java内存区域与内存溢出
java·开发语言·jvm·笔记·学习
ST.J1 天前
前端笔记2025
前端·javascript·css·vue.js·笔记
Suckerbin1 天前
LAMPSecurity: CTF5靶场渗透
笔记·安全·web安全·网络安全
小憩-1 天前
【机器学习】吴恩达机器学习笔记
人工智能·笔记·机器学习
UQI-LIUWJ1 天前
unsloth笔记:运行&微调 gemma
人工智能·笔记·深度学习