ARTICLE DETAIL

资讯详情

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

Java 设计模式之依赖注入(Dependency Injection):在 java-design-patterns 中实现解耦与可测试性

Java 设计模式之依赖注入(Dependency Injection):在 java-design-patterns 中实现解耦与可测试性 示例工程教程【免费下载链接】java-design-patternsDesign patterns implemented in Java项目地址https://gitcode.com/GitHub_Trending/ja/java-design-patterns点击查看免费下载依赖注入Dependency InjectionDI是 Java 世界中用于解耦对象依赖创建与使用的最重要设计模式之一本指南以当前仓库 java-design-patterns 中 dependency-injection 模块的完整源码与测试为实证基础系统讲解该模式的核心思想、构造器注入/Setter 注入/框架注入三种落地方式以及它在 Spring、Jakarta EE、Google Guice 等框架中的真实应用。读完本文你将能够识别违反依赖倒置的坏代码并亲手把手动装配、Guice 模块绑定等手段应用到自己的项目中。模式概览别名与意图Dependency Injection 模式在业界还有两个常见别名Inversion of ControlIoC控制反转Dependency Inversion依赖倒置模式的核心意图是将“对象所需依赖的创建过程”与“依赖的使用过程”解耦从而使代码更加灵活、更加易于测试。仓库中 App.java 的类注释进一步点明了 IoC 的两条具体规则高层模块不应依赖低层模块两者都应依赖抽象抽象不应依赖细节细节应依赖抽象。也就是说依赖注入的本质是让对象不再自己“造”依赖而是从外部“接收”依赖。现实世界的类比餐厅与供应商设想一家高档餐厅主厨做菜需要各种各样的食材。他不必为每种食材亲自跑去找各自的制造商而是依赖一位可靠的供应商由供应商每天从各个制造商那里采购新鲜食材送上门。这样主厨就能专注于烹饪无需操心采购。在 Dependency Injection 模式中供应商扮演注入器Injector的角色把所需依赖食材提供给对象——主厨。主厨可以使用这些食材却不必知道它们的来源依赖的获取与使用被清晰分离。这种思路同时提升了厨房以及软件系统的效率、灵活性与可维护性。用一句最简单的话概括依赖注入把“依赖的获取”与“使用者的自身行为”分离开来。维基百科对它的定义是在软件工程中依赖注入是一种对象接收其所需的其他对象称为依赖的技术。模式时序图下图展示了依赖注入的核心交互流程——客户端Wizard通过注入器获得抽象依赖Tobacco并调用其方法程序示例三种注入方式的渐进实现仓库中的示例围绕一位老巫师展开他时不时想填满烟斗抽上一斗烟但他不希望被锁定在单一烟草品牌上而是想随时互换不同品牌。下面我们沿着源码逐步实现。1. 抽象与具体实现Tobacco 体系首先定义Tobacco抽象类与具体品牌类。注意当前仓库版本中的Tobacco使用了 Lombok 的Slf4j注解Tobacco.javaSlf4j public abstract class Tobacco { 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 { }三个具体类 SecondBreakfastTobacco、RivendellTobacco、OldTobyTobacco 均继承自Tobacco它们共享smoke(Wizard)的日志行为同时又是可互换的具体实现。2. Wizard 接口与违反 IoC 的反例SimpleWizard所有巫师都实现统一的 Wizard 接口public interface Wizard { void smoke(); }接着是SimpleWizard——它是一段故意违反控制反转原则的反面教材SimpleWizard.javapublic class SimpleWizard implements Wizard { private final OldTobyTobacco tobacco new OldTobyTobacco(); public void smoke() { tobacco.smoke(this); } }可以看到SimpleWizard在字段初始化时直接new了一个具体实现OldTobyTobacco它依赖的是“细节”而非“抽象”既无法在测试中替换为 Mock/Stub也无法在运行时切换品牌。这正是 DI 模式要解决的问题。3. 构造器注入AdvancedWizardAdvancedWizard通过构造器接收依赖字段为final一旦注入即不可变AdvancedWizard.javapublic class AdvancedWizard implements Wizard { private final Tobacco tobacco; public AdvancedWizard(Tobacco tobacco) { this.tobacco tobacco; } Override public void smoke() { tobacco.smoke(this); } }它只依赖抽象Tobacco具体品牌由调用方在构造时决定——创建与使用被彻底分离这也让单元测试可以自由注入任意品牌。4. Setter 注入AdvancedSorceressAdvancedSorceress展示的是Setter 注入变体借助 Lombok 的Setter注解生成setTobacco(...)方法AdvancedSorceress.javaSetter public class AdvancedSorceress implements Wizard { private Tobacco tobacco; Override public void smoke() { tobacco.smoke(this); } }与构造器注入相比Setter 注入允许在对象创建之后再更换依赖适合依赖需要在运行时动态切换的场景代价是字段无法声明为final失去了不可变性保证。5. 框架注入Guice 与 TobaccoModule第四种方式把模式推进到框架层面使用Google Guice完成自动装配。首先定义一个 Guice 模块将抽象绑定到具体实现TobaccoModule.javapublic class TobaccoModule extends AbstractModule { Override protected void configure() { bind(Tobacco.class).to(RivendellTobacco.class); } }bind(Tobacco.class).to(RivendellTobacco.class)意味着只要容器需要Tobacco就自动提供RivendellTobacco实例。接下来GuiceWizard通过构造器上的Inject注解声明注入点GuiceWizard.javapublic class GuiceWizard implements Wizard { private final Tobacco tobacco; Inject public GuiceWizard(Tobacco tobacco) { this.tobacco tobacco; } Override public void smoke() { tobacco.smoke(this); } }6. 运行入口 App 与程序输出最终App.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(); }程序输出时间戳因运行时刻而异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因硬编码只抽OldTobyTobacco而GuiceWizard抽的是RivendellTobacco——这正体现了“由容器决定依赖而不是由使用者决定”的控制反转。如何在仓库中运行与验证dependency-injection模块是一个独立 Maven 模块其 pom.xml 声明了以下依赖与配置日志slf4j-apilogback-classic测试junit-jupiter-enginescope 为 test依赖注入框架com.google.inject:guice打包插件maven-assembly-plugin将主类配置为com.iluwatar.dependency.injection.App。在仓库根目录java-design-patterns下可使用随仓库提供的 Maven Wrapper 运行# 运行该模块的全部单元测试 ./mvnw -pl dependency-injection -am test # 打包可执行 jarassembly 插件会生成含主类的可运行包 ./mvnw -pl dependency-injection -am package若直接运行主类可执行java -cp ... com.iluwatar.dependency.injection.Appclasspath 需包含模块编译产物与 guice、slf4j 等依赖实践中最简单的方式是使用上面的 assembly 打包产物。测试如何验证注入行为仓库为每种注入方式都配备了单元测试它们共同印证了“依赖可注入、可替换”这一核心主张。AdvancedWizardTest.java 验证构造器注入将三种烟草依次通过构造器传入AdvancedWizard断言每次日志都精确匹配AdvancedWizard smoking 品牌名并且没有额外输出ListTobacco tobaccos List.of(new OldTobyTobacco(), new RivendellTobacco(), new SecondBreakfastTobacco()); tobaccos.forEach(tobacco - { final AdvancedWizard advancedWizard new AdvancedWizard(tobacco); advancedWizard.smoke(); String lastMessage appender.getLastMessage(); assertEquals(AdvancedWizard smoking tobacco.getClass().getSimpleName(), lastMessage); }); assertEquals(tobaccos.size(), appender.getLogSize());GuiceWizardTest.java 则更进一步包含两个用例testSmokeEveryThingThroughConstructor直接向构造器传参验证Inject构造器本身的行为testSmokeEveryThingThroughInjectionFramework在测试中动态构建匿名AbstractModule把Tobacco.class分别绑定到三种品牌再由Guice.createInjector(...).getInstance(GuiceWizard.class)获取实例并验证日志。该用例证明只需改动绑定配置即可更换依赖实现业务代码GuiceWizard完全无需改动。这些测试依赖utils.InMemoryAppenderInMemoryAppender.java在内存中捕获日志从而断言行为而无需依赖真实控制台输出——这本身就是“依赖抽象、可被替换”思想在测试基建上的体现。何时使用依赖注入模式根据仓库文档以下场景适合采用 DI需要降低类与类之间的耦合、提升应用模块化程度时对象创建过程复杂或应当与类的使用过程分离时应用需要更轻松的单元测试允许依赖以 Mock 或 Stub 形式注入时在使用管理对象生命周期与依赖的框架如 Spring、Jakarta EE即原 Java EE时。Java 中的真实应用Spring、Jakarta EE 与 Google Guice等框架大量使用依赖注入来管理组件生命周期与依赖关系需要灵活架构、组件易于互换的桌面应用与 Web 应用广泛受益于 DI 带来的可配置性。优点与权衡优点提升模块化程度与关注点分离简化单元测试——依赖可轻松被 Mock 替换通过促进松耦合提升灵活性与可维护性。权衡与代价可能使配置复杂化尤其是在大型项目中对不熟悉 DI 概念或框架的开发者存在学习曲线需要谨慎管理对象的生命周期与作用域scope否则容易引发作用域泄漏或内存问题。相关设计模式在 java-design-patterns 仓库中与 DI 密切相关的模式包括工厂方法Factory Method 与 抽象工厂Abstract Factory用于创建将被 DI 机制注入的实例两者常配合使用服务定位器Service LocatorDI 的一种替代方案用于定位服务或组件但对“查找过程”的解耦效果不如 DI 彻底单例Singleton常与 DI 结合使用为整个应用提供服务的单一实例。参考资料本文内容以仓库文档与源码为准以下为文档中列出的延伸阅读书目供深入学习参考Clean Code: A Handbook of Agile Software CraftsmanshipRobert C. MartinDependency Injection: Design Patterns Using Spring and GuiceDhanji R. PrasannaDependency Injection Principles, Practices, and PatternsSteven van Deuren / Mark SeemannGoogle Guice: Agile Lightweight Dependency Injection FrameworkJava 9 Dependency Injection: Write Loosely Coupled Code with Spring 5 and GuiceJava Design Pattern EssentialsPro Java EE Spring Patterns: Best Practices and Design StrategiesSpring in ActionCraig Walls赞分享示例工程教程【免费下载链接】java-design-patternsDesign patterns implemented in Java项目地址https://gitcode.com/GitHub_Trending/ja/java-design-patterns点击查看免费下载相关推荐Java 设计模式之依赖注入Dependency Injection以 java-design-patterns 仓库源码深度解析松耦合实现Java 设计模式之依赖注入Dependency Injection以 java design patterns 仓库源码深度解析松耦合实现 依赖注入D示例工程教程Java 依赖注入Dependency Injection模式实战以 java-design-patterns 为例从手动注入到 Google GuiceJava 依赖注入Dependency Injection模式实战以 java design patterns 为例从手动注入到 Google Guice示例工程教程java-design-patterns 中的依赖注入Dependency Injection模式以 Wizard 与 Tobacco 为例解析控制反转与三种注入方式java design patterns 中的依赖注入Dependency Injection模式以 Wizard 与 Tobacco 为例解析控制反转与示例工程教程创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表