依赖注入框架Google Guice 对象图

时间:2022-09-10 17:44:20
GettingStarted · google/guice Wiki https://github.com/google/guice/wiki/GettingStarted
sameb edited this page on Jul 8 2014 · 1 revision

How to start doing dependency injection with Guice.

Getting Started

With dependency injection, objects accept dependencies in their constructors. To construct an object, you first build its dependencies. But to build each dependency, you need its dependencies, and so on. So when you build an object, you really need to build an object graph.

Building object graphs by hand is labour intensive, error prone, and makes testing difficult. Instead, Guice can build the object graph for you. But first, Guice needs to be configured to build the graph exactly as you want it.

To illustrate, we'll start the BillingService class that accepts its dependent interfaces CreditCardProcessor and TransactionLog in its constructor. To make it explicit that the BillingServiceconstructor is invoked by Guice, we add the @Inject annotation:

class BillingService {
private final CreditCardProcessor processor;
private final TransactionLog transactionLog; @Inject
BillingService(CreditCardProcessor processor,
TransactionLog transactionLog) {
this.processor = processor;
this.transactionLog = transactionLog;
} public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard) {
...
}
}

We want to build a BillingService using PaypalCreditCardProcessor and DatabaseTransactionLog. Guice uses bindings to map types to their implementations. A module is a collection of bindings specified using fluent, English-like method calls:

public class BillingModule extends AbstractModule {
@Override
protected void configure() { /*
* This tells Guice that whenever it sees a dependency on a TransactionLog,
* it should satisfy the dependency using a DatabaseTransactionLog.
*/
bind(TransactionLog.class).to(DatabaseTransactionLog.class); /*
* Similarly, this binding tells Guice that when CreditCardProcessor is used in
* a dependency, that should be satisfied with a PaypalCreditCardProcessor.
*/
bind(CreditCardProcessor.class).to(PaypalCreditCardProcessor.class);
}
}

The modules are the building blocks of an injector, which is Guice's object-graph builder. First we create the injector, and then we can use that to build the BillingService:

 public static void main(String[] args) {
/*
* Guice.createInjector() takes your Modules, and returns a new Injector
* instance. Most applications will call this method exactly once, in their
* main() method.
*/
Injector injector = Guice.createInjector(new BillingModule()); /*
* Now that we've got the injector, we can build objects.
*/
BillingService billingService = injector.getInstance(BillingService.class);
...
}

By building the billingService, we've constructed a small object graph using Guice. The graph contains the billing service and its dependent credit card processor and transaction log.

Motivation · google/guice Wiki https://github.com/google/guice/wiki/Motivation

Sam Berlin edited this page on Apr 21 2015 · 4 revisions

Motivation

Wiring everything together is a tedious part of application development. There are several approaches to connect data, service, and presentation classes to one another. To contrast these approaches, we'll write the billing code for a pizza ordering website:

public interface BillingService {

  /**
* Attempts to charge the order to the credit card. Both successful and
* failed transactions will be recorded.
*
* @return a receipt of the transaction. If the charge was successful, the
* receipt will be successful. Otherwise, the receipt will contain a
* decline note describing why the charge failed.
*/
Receipt chargeOrder(PizzaOrder order, CreditCard creditCard);
}

Along with the implementation, we'll write unit tests for our code. In the tests we need a FakeCreditCardProcessor to avoid charging a real credit card!

Direct constructor calls

Here's what the code looks like when we just new up the credit card processor and transaction logger:

public class RealBillingService implements BillingService {
public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard) {
CreditCardProcessor processor = new PaypalCreditCardProcessor();
TransactionLog transactionLog = new DatabaseTransactionLog(); 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());
}
}
}

This code poses problems for modularity and testability. The direct, compile-time dependency on the real credit card processor means that testing the code will charge a credit card! It's also awkward to test what happens when the charge is declined or when the service is unavailable.

Factories

A factory class decouples the client and implementing class. A simple factory uses static methods to get and set mock implementations for interfaces. A factory is implemented with some boilerplate code:

public class CreditCardProcessorFactory {

  private static CreditCardProcessor instance;

  public static void setInstance(CreditCardProcessor processor) {
instance = processor;
} public static CreditCardProcessor getInstance() {
if (instance == null) {
return new SquareCreditCardProcessor();
} return instance;
}
}

In our client code, we just replace the new calls with factory lookups:

public class RealBillingService implements BillingService {
public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard) {
CreditCardProcessor processor = CreditCardProcessorFactory.getInstance();
TransactionLog transactionLog = TransactionLogFactory.getInstance(); 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());
}
}
}

The factory makes it possible to write a proper unit test:

public class RealBillingServiceTest extends TestCase {

  private final PizzaOrder order = new PizzaOrder(100);
private final CreditCard creditCard = new CreditCard("1234", 11, 2010); private final InMemoryTransactionLog transactionLog = new InMemoryTransactionLog();
private final FakeCreditCardProcessor processor = new FakeCreditCardProcessor(); @Override public void setUp() {
TransactionLogFactory.setInstance(transactionLog);
CreditCardProcessorFactory.setInstance(processor);
} @Override public void tearDown() {
TransactionLogFactory.setInstance(null);
CreditCardProcessorFactory.setInstance(null);
} public void testSuccessfulCharge() {
RealBillingService billingService = new RealBillingService();
Receipt receipt = billingService.chargeOrder(order, creditCard); assertTrue(receipt.hasSuccessfulCharge());
assertEquals(100, receipt.getAmountOfCharge());
assertEquals(creditCard, processor.getCardOfOnlyCharge());
assertEquals(100, processor.getAmountOfOnlyCharge());
assertTrue(transactionLog.wasSuccessLogged());
}
}

This code is clumsy. A global variable holds the mock implementation, so we need to be careful about setting it up and tearing it down. Should the tearDown fail, the global variable continues to point at our test instance. This could cause problems for other tests. It also prevents us from running multiple tests in parallel.

But the biggest problem is that the dependencies are hidden in the code. If we add a dependency on a CreditCardFraudTracker, we have to re-run the tests to find out which ones will break. Should we forget to initialize a factory for a production service, we don't find out until a charge is attempted. As the application grows, babysitting factories becomes a growing drain on productivity.

Quality problems will be caught by QA or acceptance tests. That may be sufficient, but we can certainly do better.

Dependency Injection

Like the factory, dependency injection is just a design pattern. The core principle is to separate behaviour from dependency resolution. In our example, the RealBillingService is not responsible for looking up the TransactionLog and CreditCardProcessor. Instead, they're passed in as constructor parameters:

public class RealBillingService implements BillingService {
private final CreditCardProcessor processor;
private final TransactionLog transactionLog; 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());
}
}
}

We don't need any factories, and we can simplify the testcase by removing the setUp and tearDownboilerplate:

public class RealBillingServiceTest extends TestCase {

  private final PizzaOrder order = new PizzaOrder(100);
private final CreditCard creditCard = new CreditCard("1234", 11, 2010); private final InMemoryTransactionLog transactionLog = new InMemoryTransactionLog();
private final FakeCreditCardProcessor processor = new FakeCreditCardProcessor(); public void testSuccessfulCharge() {
RealBillingService billingService
= new RealBillingService(processor, transactionLog);
Receipt receipt = billingService.chargeOrder(order, creditCard); assertTrue(receipt.hasSuccessfulCharge());
assertEquals(100, receipt.getAmountOfCharge());
assertEquals(creditCard, processor.getCardOfOnlyCharge());
assertEquals(100, processor.getAmountOfOnlyCharge());
assertTrue(transactionLog.wasSuccessLogged());
}
}

Now, whenever we add or remove dependencies, the compiler will remind us what tests need to be fixed. The dependency is exposed in the API signature.

Unfortunately, now the clients of BillingService need to lookup its dependencies. We can fix some of these by applying the pattern again! Classes that depend on it can accept a BillingService in their constructor. For top-level classes, it's useful to have a framework. Otherwise you'll need to construct dependencies recursively when you need to use a service:

  public static void main(String[] args) {
CreditCardProcessor processor = new PaypalCreditCardProcessor();
TransactionLog transactionLog = new DatabaseTransactionLog();
BillingService billingService
= new RealBillingService(processor, transactionLog);
...
}

Dependency Injection with Guice

The dependency injection pattern leads to code that's modular and testable, and Guice makes it easy to write. To use Guice in our billing example, we first need to tell it how to map our interfaces to their implementations. This configuration is done in a Guice module, which is any Java class that implements the Module interface:

public class BillingModule extends AbstractModule {
@Override
protected void configure() {
bind(TransactionLog.class).to(DatabaseTransactionLog.class);
bind(CreditCardProcessor.class).to(PaypalCreditCardProcessor.class);
bind(BillingService.class).to(RealBillingService.class);
}
}

We add @Inject to RealBillingService's constructor, which directs Guice to use it. Guice will inspect the annotated constructor, and lookup values for each parameter.

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());
}
}
}

Finally, we can put it all together. The Injector can be used to get an instance of any of the bound classes.

  public static void main(String[] args) {
Injector injector = Guice.createInjector(new BillingModule());
BillingService billingService = injector.getInstance(BillingService.class);
...
}

Getting started explains how this all works.

总结:

创建对象,即创建对象的依赖图

Guice中文文档 - 简书 https://www.jianshu.com/p/97e354423612

原文链接:https://github.com/google/guice/wiki/Motivation

总结:

依赖注入:行为和解析依赖分开

Motivation

将所有东西整合到一起是开发中一项乏味的工作,现在有多种方法将不同的数据、服务和展现层互相联系起来。为了对比这些不同的方法, 我们为一个披萨在线订购网站编写了计费代码

public interface BillingService {

  /**
* 尝试通过信用卡支付,无论是否成功都将被记录下来
* @return 支付成功时返回成功信息,否则,返回失败原因
*
*/
Receipt chargeOrder(PizzaOrder order, CreditCard creditCard);
}

在实现这个接口之前,我们要写一个单元测试。在测试中我们需要一个FakeCreditCardProcessor,因为我们不能真的从一张信用卡中刷钱=。=

Direct constructor calls

下面展示了如果我们只new一个信用卡processor和transaction logger我们的代码会是神马样

public class RealBillingService implements BillingService {
public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard) {
CreditCardProcessor processor = new PaypalCreditCardProcessor();
TransactionLog transactionLog = new DatabaseTransactionLog(); 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());
}
}
}

如上代码耦合性很高,并且不易测试。单元测试里不可能对一张真实的信用卡进行操作。

Factories

一个工厂类解耦了客户端和它的实现类。一个简单的工厂使用静态方法来get和set一个mock实现类。

public class CreditCardProcessorFactory {

  private static CreditCardProcessor instance;

  public static void setInstance(CreditCardProcessor processor) {
instance = processor;
} public static CreditCardProcessor getInstance() {
if (instance == null) {
return new SquareCreditCardProcessor();
} return instance;
}
}

在我们的客户端代码中,我们只是在原先new的地方调用工厂方法。

public class RealBillingService implements BillingService {
public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard) {
CreditCardProcessor processor = CreditCardProcessorFactory.getInstance();
TransactionLog transactionLog = TransactionLogFactory.getInstance(); 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());
}
}
}

有了工厂,我们就可以实现一个正确的UT

public class RealBillingServiceTest extends TestCase {

  private final PizzaOrder order = new PizzaOrder(100);
private final CreditCard creditCard = new CreditCard("1234", 11, 2010); private final InMemoryTransactionLog transactionLog = new InMemoryTransactionLog();
private final FakeCreditCardProcessor processor = new FakeCreditCardProcessor(); @Override public void setUp() {
TransactionLogFactory.setInstance(transactionLog);
CreditCardProcessorFactory.setInstance(processor);
} @Override public void tearDown() {
TransactionLogFactory.setInstance(null);
CreditCardProcessorFactory.setInstance(null);
} public void testSuccessfulCharge() {
RealBillingService billingService = new RealBillingService();
Receipt receipt = billingService.chargeOrder(order, creditCard); assertTrue(receipt.hasSuccessfulCharge());
assertEquals(100, receipt.getAmountOfCharge());
assertEquals(creditCard, processor.getCardOfOnlyCharge());
assertEquals(100, processor.getAmountOfOnlyCharge());
assertTrue(transactionLog.wasSuccessLogged());
}
}

这看起来很笨拙。一个全局变量持有mock实现,所以在设置和销毁时我们需要很小心。如果tearDown失败了,这个全局变量将会继续指向我们的测试实例,这将会对其他的UT产生影响,我们也不能并行地运行多个测试用例。
但是,最大的问题在于,依赖被隐藏在代码里。如果我们要添加一个新的依赖CreditCardFraudTracker,我们必须重新运行UT来找到which ones will break(没看懂)

Dependency Injection

跟工厂一样,DI也只是一个设计模式,其核心原则是将行为和解析依赖分离开(separate behaviour from dependency resolution)。在我们的栗子里,RealBillingService不负责寻找TransactionLog和CreditCardProcessor,相反,它们作为构造函数的参数被传递给BillingService

public class RealBillingService implements BillingService {
private final CreditCardProcessor processor;
private final TransactionLog transactionLog; 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());
}
}
}

我们不再需要任何工厂,而且我们可以扔掉setUp和tearDown方法来简化我们的测试用例。

public class RealBillingServiceTest extends TestCase {

  private final PizzaOrder order = new PizzaOrder(100);
private final CreditCard creditCard = new CreditCard("1234", 11, 2010); private final InMemoryTransactionLog transactionLog = new InMemoryTransactionLog();
private final FakeCreditCardProcessor processor = new FakeCreditCardProcessor(); public void testSuccessfulCharge() {
RealBillingService billingService
= new RealBillingService(processor, transactionLog);
Receipt receipt = billingService.chargeOrder(order, creditCard); assertTrue(receipt.hasSuccessfulCharge());
assertEquals(100, receipt.getAmountOfCharge());
assertEquals(creditCard, processor.getCardOfOnlyCharge());
assertEquals(100, processor.getAmountOfOnlyCharge());
assertTrue(transactionLog.wasSuccessLogged());
}
}

现在,无论什么时候我们添加或删除依赖,编译器都会提醒我们哪些测试出错了。依赖关系暴露在API接口中。
不过令人不开心的是,现在BillingService需要去寻找它的依赖了。我们可以再次应用这个模式来解决这个问题。可以提供一个BillingService给依赖它的类作为构造器的参数。简而言之,提供一个框架给顶端的类总不是个坏事。

public static void main(String[] args) {
CreditCardProcessor processor = new PaypalCreditCardProcessor();
TransactionLog transactionLog = new DatabaseTransactionLog();
BillingService billingService
= new RealBillingService(processor, transactionLog);
...
}

Dependency Injection with Guice

下面就要介绍到我们的Guice了,DI模式使得代码更加易于测试和可维护,Guice使得代码更容易编写。为了在我们的栗子中使用Guice,首先我们需要建立接口及其实现的映射关系。这个可以在一个实现了Module接口的java类中进行配置:

public class BillingModule extends AbstractModule {
@Override
protected void configure() {
bind(TransactionLog.class).to(DatabaseTransactionLog.class);
bind(CreditCardProcessor.class).to(PaypalCreditCardProcessor.class);
bind(BillingService.class).to(RealBillingService.class);
}
}

我们在RealBillingService的构造函数上加了一个@Inject注解,这个会告诉Guice来使用它。Guice会检查被注解的构造函数,然后找到每个参数的值。

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);
...
}

下一章解释了这一切是怎么工作的。