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
There are several approaches to connect data, service, and presentation classes to one another.
The object:
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.
Direct constructor calls: Invoke a constructor, hard-wiring an object
With the below example, there is testing problems:
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());
}
}
}
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:
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.
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:
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 :
Instead of the programmer calling a constructor or factory, a tool called a dependency injector passes dependencies to objects.
Steps:
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:
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:
An injector could rely on:
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.