Google Guice's sledgehammer test

Google Guice is an open source software launched by Google. It is an ultra-lightweight, next-generation, dependency injection container designed for Java 5 and subsequent versions. Its function is similar to the booming Spring.

Let’s take a look at Guice. Before that, let’s take a look at an official example: in an application, it is a tedious event to assemble everything, which involves connecting data, services, and presentation layer classes. In other aspects, this is an example of a billing code for a pizza ordering website for comparison of these aspects.

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

BillingServiceFor the implementation class, we will use unit tests to test, and the rest we need one FakeCreditCardProcessorto avoid directly CreditCarddealing with it, which is the performance of encapsulation in object-oriented.

The first way to achieve: directly call the construction method

public class RealBillingService implements BillingService {
  public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard) {
    CreditCardProcessor processor = new PaypalCreditCardProcessor();//构造方法创建CreditCardProcessor
    TransactionLog transactionLog = new DatabaseTransactionLog();//构造方法创建TransactionLog对象

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

Such code lacks modularity and testability, because it directly relies on the CreditCardProcessorimplementation class at compile time , and the coupling is too strong.

The second way to achieve: using the factory pattern:

Using a factory class can decouple the client from the implementation. A simple factory uses a static method to get or set the interface implementation. The following is the same version:

public class CreditCardProcessorFactory {

  private static CreditCardProcessor instance;

  public static void setInstance(CreditCardProcessor creditCardProcessor) {
    instance = creditCardProcessor;
  }

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

    return instance;
  }
}public class CreditCardProcessorFactory {

  private static CreditCardProcessor instance;

  public static void setInstance(CreditCardProcessor creditCardProcessor) {
    instance = creditCardProcessor;
  }

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

    return instance;
  }
}

In the client code, just use the factory class to newreplace the keywords:

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

Unit testing after using the factory pattern: Unit testing after using the factory pattern:

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 creditCardProcessor = new FakeCreditCardProcessor();

  @Override public void setUp() {
    TransactionLogFactory.setInstance(transactionLog);
    CreditCardProcessorFactory.setInstance(creditCardProcessor);
  }

  @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, creditCardProcessor.getCardOfOnlyCharge());
    assertEquals(100, creditCardProcessor.getAmountOfOnlyCharge());
    assertTrue(transactionLog.wasSuccessLogged());
  }
}

This code is still a bit clumsy. A global variable saves the implementation instance, so we must be very careful about the variable assignment and value release. If the tailDownmethod fails, the global variable is still valid, which may cause problems for other tests. It is not yet possible to run multiple test cases in parallel. The biggest problem is that as the application expands, more and more factories will appear when there are new dependencies, which will reduce the efficiency of the application.

The third way: dependency injection

Like the factory pattern as Dependency Injection is a design pattern, which is separate from the main principles of behavior and to rely, in the above example RealBillingServiceis not responsible for TransactionLogand CreditCardProcessorcreate an object, for it is these two objects in RealBillingServicethe constructor parameters Pass in.

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

In this way, we don't need any factory classes, and we can remove the setUpand tearDownmethods to simplify unit testing:

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 creditCardProcessor = new FakeCreditCardProcessor();

  public void testSuccessfulCharge() {
    RealBillingService billingService
        = new RealBillingService(creditCardProcessor, transactionLog);
    Receipt receipt = billingService.chargeOrder(order, creditCard);

    assertTrue(receipt.hasSuccessfulCharge());
    assertEquals(100, receipt.getAmountOfCharge());
    assertEquals(creditCard, creditCardProcessor.getCardOfOnlyCharge());
    assertEquals(100, creditCardProcessor.getAmountOfOnlyCharge());
    assertTrue(transactionLog.wasSuccessLogged());
  }
}

Unfortunately, BillingServicethe client needs to create its dependencies. Now it is better to have a framework to automatically create these dependencies, otherwise we have to create these circular dependencies manually.

Now when Guice comes out, use Guice for dependency injection.

Dependency injection mode can make the code more modular and easier to test, and Guice makes it easier to write. In the above billing example, the first step is to tell Guice how to map the interface and implementation class. This is configured through Guice's Module, which can be any ModuleJava class that implements the 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);
  }
}

When performing dependency injection, objects receive dependencies in their construction parameters. To create an object, you must first create its dependencies, but to create each dependency, you must create each dependency of the dependency, and so on. So when you create an object, what you really want to create is an object graph. Manually creating an object graph is time-consuming and laborious, tends to be wrong, and makes testing difficult. Fortunately, Guice can create this object graph for us, and all we have to do is to configure and tell it how to create this object graph accurately.

In the RealBillingServiceconstructor add @Injectannotations, Guice checks add a constructor annotated, and find the value for each parameter. Adding @Injectannotations is doing configuration work. Tell Guice that if you create an object graph, of course @Injectannotations can be placed not only on the constructor, but also on the setter methods and fields.

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 put these together as follows, the Injectorclass is used to obtain an instance of any bound class:

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

-------------------------------- END -------------------------------

For more exciting articles in time, please pay attention to the public account "Java Essentials".

Guess you like

Origin blog.51cto.com/xtayfjpk/2667446