问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

解耦的艺术:通过DPI依赖倒置实现解耦

创作时间:
作者:
@小白创作中心

解耦的艺术:通过DPI依赖倒置实现解耦

引用
CSDN
1.
https://blog.csdn.net/yangshangwei/article/details/145729263

解耦是软件工程中的一个重要概念,它关系到系统的灵活性、可维护性和扩展性。本文将深入探讨依赖倒置原则在解耦中的应用,通过具体的案例分析,帮助读者理解如何通过抽象和接口实现真正的解耦。

概述

耦合带来了业务前台和业务中台高昂的协作和认知成本,抵消了复用节省的时间成本,总体上反而造成了研发效率的下降。由此可见,软件设计的一大目标就是“解耦”。模块之间的联系越少,耦合越小,系统就越灵活,可修改性越好。在一个设计良好的系统中,数据库代码和用户界面应该是正交的。这样我们可以改动界面,而不影响数据库;在更换数据库时,可以不用改动界面。

在软件工程领域,我们一直在强调“高内聚,低耦合”,即希望通过降低模块之间的耦合性来提升模块的独立性、扩展性和重用性。虽然我们在最初学编程时就知道要“高内聚,低耦合”,但是在实际工作中还是会干出很多“低内聚,高耦合”的事情,那么问题出在哪里呢?

在程序设计过程中,解耦设计至关重要,要设计一个易维护且扩展性好的程序,有时并不像我们想得那么简单。很多耦合的存在是隐式的,我们无法轻易发现,所以往往无从入手。

可见,要想真正地掌握解耦思维并不是一件容易的事情。为此,我们首先要知道什么是耦合,如何才能发现耦合,其次还要知道如何进行解耦。

耦合与解耦

在软件领域,“耦合”是指两个事物之间联系的紧密程度。联系越紧密,耦合性越高;联系越少,耦合性越低。解耦就是要减少事物之间联系的紧密程度。

如果一个类、组件、服务从来不和其他类、组件、服务发生联系,没有耦合性,那么其存在的价值也没有了。软件只能通过被使用才能产生价值,而建立这个“被使用”的过程就是产生联系、产生耦合的过程。因此,耦合不可能被完全消除,只能设法减少。

从这个意义上来说,解耦并不会完全解开耦合,而是使用一些方法降低耦合的程度

解耦有两种主要方式

在软件世界中,解耦有两种主要方式——依赖倒置解耦和中间层映射解耦。

方式一: 依赖倒置解耦

依赖倒置是SOLID设计原则中的“D”,全称是Dependence Inversion Principle,即依赖倒置原则,其定义如下。

  • 上层模块不应该依赖底层模块,它们都应该依赖于抽象。(High level modules should not depend upon low level modules.Both should depend upon abstractions.)
  • 抽象不应该依赖于细节,细节应该依赖于抽象。(Abstractions should not depend upon details.Details should depend upon abstractions.)

根据定义,我们知道依赖倒置实际上倒置的是依赖方向。如图所示,有两个模块A和B,本来A是直接依赖B的,依赖方向是A→B,通过增加一个抽象C,然后让模块B去实现这个抽象,从而反转了依赖的方向,变成B→A,这就是依赖倒置

可是为什么要依赖倒置?反转依赖的方向,从依赖具体到依赖抽象又有什么好处?

抽象比具体灵活

例如,小张是某高校计算机专业的应届毕业生,他热爱编程,学习成绩也很好,期望毕业之后能去阿里巴巴工作,并对自己信心十足,在找工作之前就放出狠话——“非阿里不去”。然而天不遂人愿,小张在面试阿里的过程中遇到了一些麻烦,HR觉得他比较傲慢,并没有给他发offer。与此同时,小张在华为的面试倒还挺顺利的,拿到了令人满意的offer。这下就尴尬了,难道就为了一句“非阿里不去”而拒绝华为这么好的机会吗?

这么做显然是不明智的,而起因正是因为小张一开始把话说得太满(太具体),没有给自己回旋的余地。 实际上,作为一名应届毕业生,小张想要的无外乎只是一份好工作而已,具体这个工作是在阿里、华为,还是腾讯,这并不是最关键的。

在《系统架构:复杂系统的产品设计与开发》一书中,作者提出一个解决方案中立原则(Principle of Solution-Neutral Function),说的也是抽象的灵活性问题。

所谓的解决方案中立,是指我们在思考解决方案的时候,不要一开始就陷入功能细节中,要尽量抽象一点,保留更多的可能性,为创新留下空间

面向接口编程

根据系统压力情况来决定是否对一个业务操作进行降级处理,因此希望引入一个开关功能。最初,这个开关的配置是存在数据库中的,所以最直接的做法是直接依赖数据库获取配置数据

然而这种依赖具体实现的做法显然不够灵活,因为后续我们很有可能将这个配置放到Nacos配置。

这种扩展性的需求是可以预见的,因此有必要提前进行解耦设计。如图所示,我们可以设计一个抽象的开关接口,把直接依赖数据库配置改成依赖开关接口。通过开关接口,我们实现了应用代码和开关功能的解耦,当需要变更开关功能实现的时候,可以保证应用代码不受影响。

依赖倒置的本质是为了解耦,它提倡依赖抽象,而在编程中,抽象通常以接口(或抽象类)的形式出现,因此我们有时也把这种解耦思想叫作“面向接口编程”。

应用与日志框架的解耦

前文提到的耦合比较显性化,基本在设计的时候就能感知到。然而还有一种耦合比较隐蔽,如果它不出现问题,我们根本不会意识到耦合性的存在。

比如对于日志框架的使用,如果使用Commons-Logging(通用日志),则直接依赖Commons-Logging;如果使用Log4j,则直接依赖Log4j;如果使用Logback,则直接依赖Logback。

倘若我们一直只使用一个日志框架,那么这个问题其实不会被暴露,然而一旦我们要切换日志框架,那么这种耦合性就会带来巨大的麻烦。因为不同日志框架的包名和API用法都不尽相同,其切换成本会非常高。

举个例子:应用依赖于Commons-Logging日志框架。随着时间的推移,需要切换成Log4j框架。再后来,大家意识到标准的重要性,所以又在系统中引入了SLF4J(Simple Logging Façade for Java)。经过一系列的演化之后,为了向后兼容,不得不使用两个桥接(Bridge),将Commons-Logging、SLF4J、Log4j桥接起来,最后形成一个日志系统依赖链。

实际上,如果我们在系统设计之初就能具备解耦思维,将应用和日志框架解耦,那么这种因为耦合带来的麻烦是可以避免的。

这也是SLF4J出现的原因,它作为一个Facade(门面)、一个纯抽象概念(没有具体的实现)对应用和具体日志实现框架进行了解耦。

实际上,即使没有SLF4J,我们也可以通过依赖倒置让日志框架依赖我们自己定义的接口,而不是直接依赖具体的日志框架。我们可以引入一个新的日志抽象,比如叫作MyLogger。在这个新抽象里,我们定义日志需要用到的接口,比如Debug、Info、Warn和Error等方法,然后使用一个实现类去实现MyLogger这个接口,同时让该实现类调用日志框架,去做真正的日志输出工作。

虽然SLF4J已经大大降低了迁移日志框架的潜在成本,但仍然建议大家用MyLogger做一层防腐。假如有一天出现一个更好的日志框架,而它又没有遵守SLF4J的规范,那么如果我们强依赖SLF4J,同样会导致变更困难;而如果我们用的是自己的MyLogger,那么迁移工作将会非常简单,只要将slf4jLogger替换成新的Logger就好。

对于这个解耦过程,我们所要做的事情只是在系统中加两个类而已,一个是定义接口的MyLogger,另一个是代理具体日志实现的代理实现类MyLoggerProxy 。

对隐蔽耦合的深入分析与解耦实践

  1. 问题本质:隐式技术耦合
    日志框架的耦合是典型的技术实现耦合,这类耦合的特点是:
  • 隐蔽性:在功能正常时完全无感,但一旦需要切换技术方案,就会暴露高昂的迁移成本。
  • 侵入性:直接依赖具体框架的 API 和包结构,导致代码与框架深度绑定。
  • 历史包袱:当系统存在多个历史框架版本(如 Commons-Logging → Log4j → SLF4J),最终会形成复杂的桥接依赖链,维护成本指数级增长。
  1. SLF4J 的局限性
    SLF4J 作为日志门面框架,通过抽象层解耦解决了大部分问题,但仍存在潜在风险:
  • 规范依赖:若新日志框架不遵循 SLF4J 规范(例如阿里云日志服务 SLS),仍需通过桥接适配。
  • 版本兼容性:SLF4J 自身版本升级可能引入兼容性问题(如接口方法变更)。
  • 框架污染:若团队直接使用 SLF4J 的 API,代码中会遍布
    org.slf4j
    的包名,依然存在技术耦合。
  1. 终极解耦方案:自定义防腐层
    通过依赖倒置 + 适配器模式,彻底将业务代码与日志框架隔离。
    实现步骤
    3.1 定义业务侧抽象接口

// 自定义日志接口(完全面向业务需求设计)
public interface MyLogger {
    void debug(String message);
    void info(String message);
    void warn(String message);
    void error(String message, Throwable throwable);
    // 可扩展业务定制方法,例如埋点日志
    void trackEvent(String eventName, Map<String, String> properties);
}

3.2 实现适配器层


// SLF4J 适配器实现
public class Slf4jLoggerAdapter implements MyLogger {
    private final org.slf4j.Logger delegate;
    public Slf4jLoggerAdapter(Class<?> clazz) {
        this.delegate = LoggerFactory.getLogger(clazz);
    }
    @Override
    public void debug(String message) {
        delegate.debug(message);
    }
    // 其他方法实现类似...
    @Override
    public void trackEvent(String eventName, Map<String, String> properties) {
        // 将业务埋点逻辑转换为 SLF4J 的 MDC 或结构化日志
        delegate.info("TRACKING_EVENT: {} - {}", eventName, properties);
    }
}
// 未来切换为新框架的适配器示例
public class NewLoggerAdapter implements MyLogger {
    private final com.newlogger.Logger delegate;
    public NewLoggerAdapter(Class<?> clazz) {
        this.delegate = NewLoggerFactory.getLogger(clazz);
    }
    @Override
    public void debug(String message) {
        delegate.logDebug(message);
    }
    // 其他方法适配...
}

3.3 通过工厂模式动态绑定


public class LoggerFactory {
    private static LoggerAdapterType adapterType = LoggerAdapterType.SLF4J;
    public static MyLogger getLogger(Class<?> clazz) {
        switch (adapterType) {
            case SLF4J:
                return new Slf4jLoggerAdapter(clazz);
            case NEW_LOGGER:
                return new NewLoggerAdapter(clazz);
            default:
                throw new IllegalArgumentException("Unsupported logger adapter");
        }
    }
    // 运行时动态切换日志实现(例如通过配置中心)
    public static void switchAdapter(LoggerAdapterType newType) {
        adapterType = newType;
    }
}
  1. 方案优势
    维度 直接依赖具体框架 使用 SLF4J 自定义防腐层
    技术耦合度 高(直接依赖具体实现) 中(依赖 SLF4J 规范) 低(仅依赖业务接口)
    迁移成本 高(需修改所有调用点) 中(需处理桥接兼容性) 低(仅修改适配器实现)
    业务扩展性 无(受限于框架能力) 有限(依赖 SLF4J 接口) 高(可自由扩展业务语义)
    团队协作成本 高(需统一框架版本) 中(需遵循 SLF4J 规范) 低(接口定义即契约)

  2. 何时需要自定义防腐层?

  • 长期演进系统:预期未来可能更换技术栈(如云原生改造)。
  • 多团队协作:需要统一日志规范,但允许各团队自主选择实现。
  • 业务语义增强:需在日志中嵌入业务元数据(如链路追踪、领域事件)。
  • 架构敏感场景:如核心中间件、基础服务等,需彻底控制第三方依赖。
  1. 总结
  • 第一层解耦:使用 SLF4J 等门面框架,解决技术实现替换问题。
  • 第二层解耦:自定义防腐层,解决业务语义扩展架构控制力问题。
  • 终极目标:通过抽象接口定义契约+适配器隔离变化,让系统核心逻辑像数学定理一样稳定,技术细节像乐高积木一样可插拔。
© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号