依赖关系注入是一种软件设计模式,其中一个或多个依赖关系(或服务)被注入或通过引用传递到依赖对象(或客户端)中,并成为客户端状态的一部分。该模式将客户端依赖项的创建与其自身行为分开,这允许程序设计松散耦合并遵循控制反转和单一责任原则。
简单来说,依赖注入通过请求获取它们的子组件而不是通过创建它们来获取, 将依赖关系的创建与其自身行为分开。
为什么需要依赖注入 假设我们有一个下单功能, 使用信用卡支付订单:
1 2 3 public interface BillingService { Receipt chargeOrder (Order order, Card card) ; }
下面是其实现(使用信用卡和记录事务日志的下单逻辑):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class RealBillingService implements BillingService { public Receipt chargeOrder (Order order, Card card) { CreditCardProcessor processor = new CreditCardProcessor (); TransactionLog transactionLog = new DatabaseTransactionLog (); try { ChargeResult result = processor.charge(card, order.getAmount()); transactionLog.logChargeResult(result); return result.wasSuccessful() ? Receipt.forSuccessfulCharge(order.getAmount()) : Receipt.forDeclinedCharge(result.getDeclineMessage()); } catch (UnreachableException e) { transactionLog.logConnectException(e); return Receipt.forSystemFailure(e.getMessage()); } } }
很快我们有了新的支付方式,会员卡支付。同时我们想把日志直接打印在文件中。很显然,上面的代码过于耦合,我们不应该在BillingService的构造器中初始化其子组件。
通过依赖注入的思想,我们可以将客户端和服务实现类分离来解决这个问题。在依赖注入中,我们引入了注入器(有时也称为容器、提供者或工厂)向客户端提供服务。
服务和客户端:服务是包含有用功能的任何类。反过来,客户端是使用服务的任何类。
下面是一个通过构造器来实现依赖注入的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public class RealBillingService implements BillingService { private final CardProcessor processor; private final TransactionLog transactionLog; public RealBillingService (CardProcessor processor, TransactionLog transactionLog) { this .processor = processor; this .transactionLog = transactionLog; } public Receipt chargeOrder (Order order, Card card) { try { ChargeResult result = processor.charge(creditCard, order.getAmount()); transactionLog.logChargeResult(result); return result.wasSuccessful() ? Receipt.forSuccessfulCharge(order.getAmount()) : Receipt.forDeclinedCharge(result.getDeclineMessage()); } catch (UnreachableException e) { transactionLog.logConnectException(e); return Receipt.forSystemFailure(e.getMessage()); } } }
TransactionLog 和 CreditCardProcessor 作为构造函数参数传入RealBillingService。我们可以随意传入不同的服务实现,来执行下单逻辑
依赖注入的类型 客户端可以通过三种主要方式接收注入的服务:
构造函数注入,其中依赖项通过客户端的类构造函数提供。 1 2 3 4 5 6 7 8 9 10 11 public class Client { private Service service; Client(Service service) { if (service == null ) { throw new InvalidParameterException ("service must not be null" ); } this .service = service; } }
setter注入(字段注入),其中客户端提供接受依赖项的setter方法, 在某些语言中(例如Java)反射可以直接进行属性字段注入。 1 2 3 4 5 6 7 8 9 10 11 public class Client { private Service service; public void setService (Service service) { if (service == null ) { throw new InvalidParameterException ("service must not be null" ); } this .service = service; } }
接口注入, 也就是说将注入的代码放在了接口方法里,接口注入模式因为具备侵入性,它要求组件必须与特定的接口相关联,因此并不被看好,实际使用有限。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 public interface ServiceSetter { public void setService (Service service) ; } public class Client implements ServiceSetter { private Service service; @Override public void setService (Service service) { if (service == null ) { throw new InvalidParameterException ("service must not be null" ); } this .service = service; } } public class ServiceInjector { private Set<ServiceSetter> clients; public void inject (ServiceSetter client) { this .clients.add(client); client.setService(new ExampleService ()); } public void switch () { for (Client client : this .clients) { client.setService(new AnotherExampleService ()); } } } public class ExampleService implements Service {}public class AnotherExampleService implements Service {}
框架注入 对于大型项目,手动依赖注入通常很乏味且容易出错,从而促进了自动化流程的框架使用。一旦构造代码不再是应用程序自定义的,而是通用的,手动依赖关系注入就成为依赖关系注入框架。一些框架,如 Guice,可以使用配置来规划程序组合。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 public class BillingModule extends AbstractModule { @Override protected void configure () { bind(TransactionLog.class).to(DatabaseTransactionLog.class); bind(CardProcessor.class).to(PaypalCreditCardProcessor.class); bind(BillingService.class).to(RealBillingService.class); } } public class RealBillingService implements BillingService { private final CreditCardProcessor processor; private final TransactionLog transactionLog; @Inject public RealBillingService (CreditCardProcessor processor, TransactionLog transactionLog) { this .processor = processor; this .transactionLog = transactionLog; } public Receipt chargeOrder (PizzaOrder order, CreditCard creditCard) { try { ChargeResult result = processor.charge(creditCard, order.getAmount()); transactionLog.logChargeResult(result); return result.wasSuccessful() ? Receipt.forSuccessfulCharge(order.getAmount()) : Receipt.forDeclinedCharge(result.getDeclineMessage()); } catch (UnreachableException e) { transactionLog.logConnectException(e); return Receipt.forSystemFailure(e.getMessage()); } } } public static void main (String[] args) { Injector injector = Guice.createInjector(new BillingModule ()); BillingService billingService = injector.getInstance(BillingService.class); }
优缺点 优点 依赖关系注入的一个基本好处是减少了类与其依赖关系之间的耦合。 通过消除客户端对其依赖项如何实现的知识,程序变得更加可重用、可测试和维护。 这也提高了灵活性:客户端可以对支持客户端期望的内部接口的任何内容进行操作。 依赖关系注入减少了样板代码,因为所有依赖关系的创建都由单个组件处理。 依赖注入允许并发开发。两个开发人员可以独立开发相互使用的类,而只需要知道类将通过哪些接口进行通信。 缺点 创建需要配置详细信息的客户端,当有明显的默认值可用时,这些详细信息可能很繁琐。 使代码难以跟踪,因为它将行为与构造分开。 通常通过反射或动态编程实现,阻碍了IDE自动化。 通常需要更多的前期开发工作。 鼓励对框架的依赖。