ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

依赖注入(Dependency Injection)设计模式实战:以 java-design-patterns 仓库的巫师与烟草为例

依赖注入(Dependency Injection)设计模式实战:以 java-design-patterns 仓库的巫师与烟草为例 示例工程教程【免费下载链接】java-design-patternsDesign patterns implemented in Java项目地址https://gitcode.com/GitHub_Trending/ja/java-design-patterns点击查看免费下载依赖注入Dependency Injection简称 DI是一种创建型设计模式核心在于把一个对象客户端所依赖的其他对象依赖项/服务通过注入或引用传递的方式交到客户端手中并使其成为客户端状态的一部分从而将依赖的创建与依赖的使用彻底分离。本文以 java-design-patterns 仓库中 dependency-injection 模块为蓝本结合其 中文版说明文档 与真实源码带你从违背 IoC 的坏实现逐步演进到构造器注入、Setter 注入、Guice 框架注入四种写法掌握如何在 Java 项目中落地这一模式并理解其背后的控制反转与单一职责原则。模式目的解耦依赖的创建与使用依赖注入是一种软件设计模式其中一个或多个依赖项或服务被注入或通过引用传递到一个依赖对象或客户端中并成为客户端状态的一部分。该模式将客户端依赖关系的创建与其自身的行为分开使程序设计可以松散耦合并遵循控制反转Inversion of ControlIoC和单一职责原则Single Responsibility Principle。用一句话概括客户端只负责用依赖不负责造依赖。在 App.java 的类注释中仓库作者还进一步明确了 IoC 的两条具体规则这可以看作本模式的理论基石高层模块不应依赖低层模块二者都应依赖抽象High-level modules should not depend on low-level modules. Both should depend on abstractions抽象不应依赖细节细节应依赖抽象Abstractions should not depend on details. Details should depend on abstractions。这两条规则也正是面向接口编程在依赖管理层面的直接体现。核心思想解读一个老巫师与他的烟草真实世界例子老巫师喜欢不时地装满烟斗抽烟。但是他不想只依赖一个烟草品牌而是希望能够互换使用它们。在这个场景里烟草品牌就是依赖项老巫师就是客户端。如果老巫师只认准某一个品牌直接new具体实现那么想换口味就必须改动巫师类本身——这正是耦合的代价。依赖注入要解决的就是让巫师在完全不知道烟草从哪来的情况下也能随心所欲地更换品牌。通俗解释依赖注入将客户端依赖的创建与其自身行为分开。客户端不再负责组装自己的依赖而是通过外部调用方、容器或框架把依赖递进来。维基百科定义在软件工程中依赖注入是一种对象接收其依赖的其他对象的技术。这些其他对象称为依赖项。三个核心参与者由此浮现依赖项Tobacco、客户端Wizard、注入者/装配者调用方或 DI 容器。程序示例从违背 IoC 到四种注入方式本模块的源码全部位于 dependency-injection/src/main/java/com/iluwatar/dependency/injection/一共演示了四种巫师抽烟的写法恰好覆盖了坏实现 → 手动注入 → 框架注入的完整演进路径。第一步定义依赖抽象 Tobacco 与三种品牌首先介绍烟草抽象基类与具体的品牌实现。注意Tobacco是抽象类而非接口其smoke方法接收一个Wizard参数并打印日志Slf4j public abstract class Tobacco { private static final Logger LOGGER LoggerFactory.getLogger(Tobacco.class); public void smoke(Wizard wizard) { LOGGER.info( {} smoking {}, wizard.getClass().getSimpleName(), this.getClass().getSimpleName()); } } public class SecondBreakfastTobacco extends Tobacco { } public class RivendellTobacco extends Tobacco { } public class OldTobyTobacco extends Tobacco { }对应源码Tobacco.java、SecondBreakfastTobacco.java、RivendellTobacco.java、OldTobyTobacco.java。三个品牌类都是空壳子类没有任何额外行为——它们的价值在于作为可互换的具体实现让换依赖成为可能。第二步定义客户端抽象 Wizard 接口public interface Wizard { void smoke(); }对应源码Wizard.java。后续所有巫师类SimpleWizard、AdvancedWizard、AdvancedSorceress、GuiceWizard都实现该接口因此对调用方而言它们的吸烟行为完全一致区别只在于依赖的获取方式。反例SimpleWizard——直接new具体实现违背 IoCpublic class SimpleWizard implements Wizard { private final OldTobyTobacco tobacco new OldTobyTobacco(); public void smoke() { tobacco.smoke(this); } }对应源码SimpleWizard.java。这个类是刻意设计的反面教材源码注释明确写着 Naive Wizard implementation violating the inversion of control principle巫师在字段初始化时直接new OldTobyTobacco()依赖被硬编码在客户端内部想换品牌只能改源码、重新编译单元测试也无法注入 Mock 对象进行隔离。这就是违背控制反转的典型形态——高层模块直接依赖了低层具体类。构造器注入AdvancedWizardRequiredArgsConstructor public class AdvancedWizard implements Wizard { private final Tobacco tobacco; Override public void smoke() { tobacco.smoke(this); } }对应源码AdvancedWizard.java。这是构造器注入的标准写法字段类型是抽象Tobacco不依赖任何具体品牌依赖通过构造器参数传入由外部装配RequiredArgsConstructorLombok自动生成接收tobacco的构造器因为是final字段注入后不可变天然线程安全、状态稳定。使用方式极其简单任意品牌都能递给巫师var advancedWizard new AdvancedWizard(new SecondBreakfastTobacco()); advancedWizard.smoke();Setter 注入AdvancedSorceressSetter public class AdvancedSorceress implements Wizard { private Tobacco tobacco; Override public void smoke() { tobacco.smoke(this); } }对应源码AdvancedSorceress.java。这是Setter 注入的写法依赖不是final通过 LombokSetter生成的setTobacco(...)方法在对象创建后注入灵活性更高运行期可换依赖但牺牲了不可变性需要调用方保证注入时机正确。使用方式var advancedSorceress new AdvancedSorceress(); advancedSorceress.setTobacco(new SecondBreakfastTobacco()); advancedSorceress.smoke();框架注入GuiceWizard TobaccoModule最后仓库把模式再推进一步使用Google Guice框架完成自动注入。先定义一个模块声明抽象Tobacco绑定到RivendellTobaccopublic class TobaccoModule extends AbstractModule { Override protected void configure() { bind(Tobacco.class).to(RivendellTobacco.class); } }对应源码TobaccoModule.java。再让GuiceWizard的构造器标注InjectGuice 便会自动完成装配public class GuiceWizard implements Wizard { private final Tobacco tobacco; Inject public GuiceWizard(Tobacco tobacco) { this.tobacco tobacco; } Override public void smoke() { tobacco.smoke(this); } }对应源码GuiceWizard.java。调用方只需创建 Injector 并获取实例完全不感知依赖的组装过程var injector Guice.createInjector(new TobaccoModule()); var guiceWizard injector.getInstance(GuiceWizard.class); guiceWizard.smoke();Guice 依赖在 pom.xml 中声明com.google.inject:guicejavax.inject.Inject注解即来自 JSR-330 标准这意味着同一套注解可被 Spring、Guice 等主流 DI 容器识别。四种写法的统一入口 AppApp.java 的main方法按顺序演示了上述四种巫师public static void main(String[] args) { var simpleWizard new SimpleWizard(); simpleWizard.smoke(); var advancedWizard new AdvancedWizard(new SecondBreakfastTobacco()); advancedWizard.smoke(); var advancedSorceress new AdvancedSorceress(); advancedSorceress.setTobacco(new SecondBreakfastTobacco()); advancedSorceress.smoke(); var injector Guice.createInjector(new TobaccoModule()); var guiceWizard injector.getInstance(GuiceWizard.class); guiceWizard.smoke(); }运行后的输出日志格式取决于 logback 配置11:54:05.205 [main] INFO com.iluwatar.dependency.injection.Tobacco -- SimpleWizard smoking OldTobyTobacco 11:54:05.207 [main] INFO com.iluwatar.dependency.injection.Tobacco -- AdvancedWizard smoking SecondBreakfastTobacco 11:54:05.207 [main] INFO com.iluwatar.dependency.injection.Tobacco -- AdvancedSorceress smoking SecondBreakfastTobacco 11:54:05.308 [main] INFO com.iluwatar.dependency.injection.Tobacco -- GuiceWizard smoking RivendellTobacco可以看到前三种是手动装配SimpleWizard是反例内部硬编码最后一种由 Guice 容器依据TobaccoModule的绑定自动装配了RivendellTobacco。类图与序列图一图看懂协作关系模块自带的 UML 类图清晰呈现了上述结构——Wizard接口与四个实现类、Tobacco抽象类与三个品牌子类以及TobaccoModule对 Guice 绑定的声明对应的 PlantUML 源文件位于 dependency-injection.urm.puml另有 dependency-injection.ucls 为 UML 类图工具工程文件。模块还提供了时序图展示客户端App如何通过构造器/Setter/Guice 三种途径将Tobacco注入到巫师并触发smoke()的完整调用过程运行与验证如何在本仓库中亲手复现本模块是一个独立的 Maven 子模块可直接在仓库根目录下运行入口类验证行为./mvnw -pl dependency-injection compile exec:java -Dexec.mainClasscom.iluwatar.dependency.injection.App也可以直接编译并运行测试验证注入什么就抽什么这一核心行为./mvnw -pl dependency-injection test测试代码位于 dependency-injection/src/test/java/com/iluwatar/dependency/injection/AdvancedWizardTest.java构造三种品牌依次注入AdvancedWizard断言日志逐条匹配AdvancedWizard smoking 品牌名且日志总数与品牌数一致——这正印证了同一个客户端可以无缝切换任意依赖实现GuiceWizardTest.java验证 Guice 注入的GuiceWizard确实拿到TobaccoModule绑定的RivendellTobaccoSimpleWizardTest.java 与 AdvancedSorceressTest.java 分别覆盖其余两种写法。测试中使用的 InMemoryAppender.java 是一个内存版日志收集器把 Logback 的输出重定向到内存列表从而让断言巫师抽了什么烟成为可能——这正是 DI 带来的可测试性红利。适用性什么时候该用依赖注入根据仓库文档见 中文说明 与 英文版 README在以下场景应当考虑使用依赖注入当你需要从对象中移除掉具体的实现内容时当你希望降低类与类之间的耦合、提升应用的模块化程度时当对象的创建过程比较复杂、应当与类的使用逻辑分离时当你需要使用模拟对象Mock或存根Stub隔离地启用类的单元测试时当项目运行在 Spring、Jakarta EE原 Java EE等负责对象生命周期与依赖管理的框架中时。一句话判断标准如果依赖怎么来的和依赖怎么用的正在相互纠缠就该用 DI 把它们拆开。收益与权衡拥抱解耦也正视成本收益增强模块化与关注点分离客户端只关心抽象契约简化单元测试依赖可被 Mock/Stub 轻松替换提升灵活性与可维护性新增品牌/实现无需改动客户端代码参考 AdvancedWizardTest.java 的切换方式。代价大型项目中配置复杂度上升绑定、作用域、生命周期管理都需要额外设计对不熟悉 DI 概念的开发者存在学习曲线需要谨慎管理对象的生命周期与作用域避免注入滥用导致装配关系难以追踪。与其他设计模式的关系工厂方法 / 抽象工厂工厂负责创建实例而 DI 容器负责把创建好的实例注入客户端两者常组合使用——本模块中的TobaccoModule绑定机制本质上就是一种由容器驱动的工厂化装配服务定位器Service Locator是 DI 的替代方案同样能解耦查找过程但客户端仍需显式发起查找解耦程度不如 DI 彻底单例Singleton常与 DI 搭配使用为整个应用提供某个服务的单一共享实例Guice 默认即为无状态服务提供单例作用域。小结通过 dependency-injection 模块的源码我们可以清晰地看到依赖注入的完整演进SimpleWizard因直接new具体类而违背 IoCAdvancedWizard用构造器注入获得不可变且可测试的依赖AdvancedSorceress用 Setter 注入换取运行期灵活性GuiceWizard TobaccoModule则把装配责任完全交给容器。四种写法共享同一个Tobacco抽象与Wizard接口这正是 DI 的威力所在——客户端与依赖各自独立演化仅在装配点短暂相遇。想深入阅读英文原始资料可对照 dependency-injection/README.md中文译文入口为 localization/zh/dependency-injection/README.md。进一步延伸阅读可参考《Clean Code: A Handbook of Agile Software Craftsmanship》《Dependency Injection: Design patterns using Spring and Guice》《Dependency Injection Principles, Practices, and Patterns》等经典著作。赞分享示例工程教程【免费下载链接】java-design-patternsDesign patterns implemented in Java项目地址https://gitcode.com/GitHub_Trending/ja/java-design-patterns点击查看免费下载相关推荐Java 依赖注入Dependency Injection模式实战以 java-design-patterns 为例从手动注入到 Google GuiceJava 依赖注入Dependency Injection模式实战以 java design patterns 为例从手动注入到 Google Guice示例工程教程Java 设计模式之依赖注入Dependency Injection以 java-design-patterns 仓库源码深度解析松耦合实现Java 设计模式之依赖注入Dependency Injection以 java design patterns 仓库源码深度解析松耦合实现 依赖注入D示例工程教程java-design-patterns 中的依赖注入Dependency Injection模式以 Wizard 与 Tobacco 为例解析控制反转与三种注入方式java design patterns 中的依赖注入Dependency Injection模式以 Wizard 与 Tobacco 为例解析控制反转与示例工程教程上一篇如何3步构建OLED显示界面Adafruit_SH1106库实战指南下一篇hbot CLI 完全指南用非交互式命令驱动、控制与监控 Hummingbot 交易机器人创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表