Type - Dependency Resolve (Dependency Resolution)

> Code - (Programming|Computer) Language > (Data) Type - (Datatype|Type of data)

1 - About

The process of finding an instance of a type dependency to use at run time is known as resolving the dependency

  • Wiring everything together is a tedious part of application development.
  • wiring elements together

same as Design Pattern - Dependency Injection

The bean can build directly its dependency by using:

  • direct construction of classes
  • or the Service Locator pattern.

DI should maximize reusability, testability and maintainability compared to traditional approaches such as:

Advertising

3 - Method

There are several approaches to connect data, service, and presentation classes to one another.

Method Decoupling level Unit test Dependency Validation Description
Constructor 1 Mock/cleanup Compile Time Constructors are more concise but restrictive. If a programmer initially elects to use a constructor but later decides that more flexibility is required, the programmer must replace every call to the constructor.
Factory 2 Mock/cleanup Compile Time Factories decouple the client and implementation to some extent but require boilerplate code.
Service Locator 3 Mock/cleanup Run Time Service locators decouple even further but reduce compile time type safety.
Dependency injection 3 Mock Compile Time

All first three approaches inhibit unit testing.

3.1 - Constructor

Direct constructor calls: Invoke a constructor, hard-wiring an object

With the below example, there is testing problems:

  • testing the code will charge a credit card
  • what if the charge is declined
  • what if the service is unavailable.
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());
    }
  }
}
Advertising

3.2 - Factory

Design Pattern - (Static) Factory - A factory class decouples the client and implementing class.

In the code, we just replace the new constructor 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. Each test will have to mock out the factory and remember to clean up after itself.

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

Problem:

  • dependencies are hidden in the code. If we add a dependency (says a CreditCardFraudTracker), we have to re-run the tests to find out which ones will break.
  • the mock is a global variable:
    • If the tearDown fail, the global variable continues to point at our test instance causing problems for other tests.
    • it prevents tests to be run in parallel.

3.3 - Service locator

Design Pattern - Service Locator

Same as factory but with the dependency verification that happens at runtime.

Advertising

3.4 - Dependency injection

Design Pattern - Dependency Injection.

The dependency injection pattern leads to code that's modular and testable

The class is not responsible for looking up the dependency. Instead, they're passed in as constructor parameters. The dependency is therefore exposed in the signature.

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

Test:

  • does not need a setup and teardown.
  • will break at compile time
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());
  }
}

But now, the clients of BillingService need to construct/lookup its dependencies.

 public static void client() {
    CreditCardProcessor processor = new PaypalCreditCardProcessor();
    TransactionLog transactionLog = new DatabaseTransactionLog();
    BillingService billingService = new RealBillingService(processor, transactionLog);
    ...
  }

This can be fixed :

  • for intermediate class, by applying the pattern again
  • for top-level classes, by using a framework

3.5 - Dependency Injection Framework

Instead of the programmer calling a constructor or factory, a tool called a dependency injector passes dependencies to objects.

Steps:

  • Programmers annotate constructors, methods, and fields to advertise their injectability.
  • A dependency injector identifies a class's dependencies by inspecting these annotations, and injects the dependencies at run time.

Moreover, the injector can verify that all dependencies have been satisfied at build time.

class Stopwatch {
 final TimeSource timeSource;
 @Inject Stopwatch(TimeSource TimeSource) {
   this.TimeSource = TimeSource;
 }
 void start() { ... }
 long stop() { ... }
}

The injector further passes dependencies to other dependencies until it constructs the entire object graph.

For instance with this class:

/** GUI for a Stopwatch */
class StopwatchWidget {
 @Inject StopwatchWidget(Stopwatch sw) { ... }
 ...
}

The injector might:

  • Find a TimeSource
  • Construct a Stopwatch with the TimeSource
  • Construct a StopwatchWidget with the Stopwatch

In unit tests, the programmer can now construct objects directly (without an injector) and pass in mock dependencies. The programmer no longer needs to set up and tear down factories or service locators in each test. This greatly simplifies our unit test:

void testStopwatch() {
 Stopwatch sw = new Stopwatch(new MockTimeSource());
 ...
}

Injector implementations can take many forms. An injector could configure itself using:

  • XML, annotations,
  • a DSL (domain-specific language),
  • or even plain code.

An injector could rely on:

  • reflection
  • or code generation.

An injector that uses compile-time code generation may not even have its own run time representation. Other injectors may not be able to generate code at all, neither at compile nor run time.

4 - Documentation