About
Wiring everything together is a tedious part of application development.
The process of finding an instance of a type dependency to use at run time is known as resolving the dependency
Articles Related
Method
There are several approaches to connect data, service, and presentation classes to one another.
The object:
- can build directly its dependency by using the following methods.
- direct construction of classes (constructor, factory)
- the Service Locator.
- or receive its dependency via the dependency injection pattern
Method | Decoupling level | Building | Unit test | Dependency Validation | Description |
---|---|---|---|---|---|
constructor | 1 | Object | 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 | Object | Mock/cleanup | Compile Time | Factories decouple the client and implementation to some extent but require boilerplate code. |
service_locator | 3 | Object | Mock/cleanup | Run Time | Service locators decouple even further but reduce compile time type safety. |
dependency_injection | 4 | External | Mock | Compile Time |
All first three approaches inhibit unit testing.
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());
}
}
}
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.
Service locator
Design Pattern - Service Locator
With the service locator pattern, the clients needs to know about the system they use to find dependencies
This is the same as factory but with the dependency verification/wiring that happens at runtime.
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
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.