Advanced Java Programming
The Dependency Injection design pattern decouples dependent objects so that they may be configured and tested independently. Google Guice manages dependencies among objects and handles the complexity of wiring together complex object relationships.
Dependency Injection
• The Dependency Injection design pattern • Testing with mock objects using Mockito • Managing dependencies with Google Guice
Copyright ©2010-2021 by David M. Whitlock. Permission to make digital or hard copies of part or all of this work for personal or classroom use is granted without fee provided that copies are not made or distributed for profit or commercial advantage and that copies bear this notice and full citation on the first page. To copy otherwise, to republish, to post on servers, or to redistribute to lists, requires prior specific permission and/or fee. Request permission to publish from
[email protected]. Last updated March 13, 2021.
1
Dependencies Among Objects
Consider the below set of dependent objects
REST
purchase(List<Book>, CreditCard) : double refund(List<Book> CreditCard) : double
BookStore remove(Book) add(Book) inStock(Book) : int BookInventory directory : File fileName : String BookDatabase
debit(CreditCard, dollars : int, cents : int) credit(CreditCard, dolars : int, cents : int)
CreditCardService
serverHost : String serverPort : int
FirstBankOfPSU FirstBankOfPortlandStateServlet
A BookStore requires a BookInventory and a CreditCardServiceto get its work done
• We’ve already extracted interfaces to allow us to abstract out the behavior that we need
• We want to be able to replace the implementations of BookInventoryand CreditCardService without affecting the code in BookStore
2
How can we implement BookStore?
package edu.pdx.cs410J.di; import java.io.File; import java.util.List; public class BookStore {
private final BookInventory inventory; private final CreditCardService cardService; public BookStore() {
String tmpdir =
System.getProperty( "java.io.tmpdir" ); File directory = new File( tmpdir ); this.inventory =
new BookDatabase( directory, "books.txt" ); this.cardService =
new FirstBankOfPSU( "localhost", 8080 ); }
When the BookStore is created, it also creates its dependencies
• Dependencies are stored in final fields because they only need to be created once
How can we implement BookStore?
public double purchase( List<Book> books, CreditCard card) { double total = 0.0d;
for (Book book : books) { inventory.remove(book); total += book.getPrice(); } CreditTransactionCode code = cardService.debit(card, total); if (code == CreditTransactionCode.SUCCESS ) { return total; } else {
throw new CreditCardTransactionException(code); }
}
What is wrong with this solution?
Sure, this code works, but.. • You can’t test this code!
– Tests would have to execute against the actual
database (slow) and credit card service ($$$)
– Lots of data setup: create database file and credit
card account
• The BookStore has to know how to configure its dependencies
– Does the BookStore really care what port the
credit card service runs on?
• If another class wants to use the BookDatabase, it would have to create another instance
– Two BookDatabases can’t work with the same file
• If you want to switch out another implementation of the dependencies, you have to change the
BookStore
– Again, does the BookStore really care what kind
of BookInventory is uses?
5
“Don’t call us, we’ll call you”
The “Hollywood Principle” states that dependent code should require that its dependencies are provided to it • Practically speaking, it means pass dependencies
into the constructor
public BookStore( BookInventory inventory,
CreditCardService cardService ) { this.inventory = inventory;
this.cardService = cardService; }
Now the BookStore class doesn’t have to know about the concrete types of its dependencies
• Let the main program create and configure dependencies
public class BookStoreApp {
public static void main(String... args) {
String tmpdir = System.getProperty( "java.io.tmpdir" ); File directory = new File( tmpdir );
BookInventory inventory =
new BookDatabase( directory, "books.txt" ); CreditCardService cardService =
new FirstBankOfPSU( "localhost", 8080 ); BookStore store =
new BookStore(inventory, cardService);
6
Testing with Mock Objects
Now that a BookStore gets passed its dependencies, you can test the class without using the “production”
BookInventoryand CreditCardService
Instead of using real dependencies, test with “mock” objects that provide an implementation of the interface that is useful for testing
package edu.pdx.cs410J.di;
public abstract class MockObject { protected void shouldNotInvoke() {
throw new UnsupportedOperationException(
"Did not expect this method to be invoked"); }
}
package edu.pdx.cs410J.di;
public class MockBookInventory extends MockObject implements BookInventory {
public void remove( Book book ) { shouldNotInvoke();
} }
Testing with Mock Objects
Test that BookStore invokes the expected methods of its dependencies
• Override interface methods to keep track of how it was invoked
package edu.pdx.cs410J.di; import org.junit.Test;
import java.util.Collections;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; public class BookStoreTest {
@Test
public void testBookIsPurchased() { final Book[] removedBook = new Book[1];
BookInventory inventory = new MockBookInventory() { public void remove( Book book ) {
removedBook[0] = book; }
};
CreditCardService cardService = new MockCreditCardService() {
public CreditTransactionCode debit( CreditCard card, double amount ) { return CreditTransactionCode.SUCCESS;
Testing with Mock Objects
Once the mock dependencies are set up • Create the BookStore
• Invoke the method you want to test
• Verify that the dependency was invoked as you expected
BookStore store =
new BookStore(inventory, cardService); CreditCard card = new CreditCard( "123" ); final Book testBook =
new Book("title", "author", 1.00d); double total = store.purchase(
Collections.singletonList(testBook), card ); assertEquals( testBook.getPrice(), total, 0.0d ); final Book removed = removedBook[0];
assertNotNull( removed ); assertEquals(testBook.getTitle(), removed.getTitle()); assertEquals(testBook.getAuthor(), removed.getAuthor()); assertEquals(testBook.getPrice(), removed.getPrice(), 0.0d); } } 9
Mock Objects are Awkward
Mock Objects get the job done, but... • Lots of verbose boilerplate code
• When interface changes (new method, e.g.), mock has to change
• Subclassing mock objects is hokey
– Lots of anonymous inner classes
– Have to keep track of state that was passed to
dependency
There’s got to be a better way, right?
10
Mock Objects with Mockito
There are several testing libraries for Java that allow you to easily create mock objects
We’re going to learn about Mockito (http://mockito.org/) • A Java API that creates mock objects for interfaces
and classes
• Contains a domain specific language (DSL) for constructing mock objects
• You can specify what behavior you expect out of the mock objects
Add the dependency in your pom.xml <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>1.8.5</version> <scope>test</scope> </dependency>
Mock Objects with Mockito
@Test
public void testCreditCardIsCharged() { Book book = mock(Book.class);
double price = 1.00d;
when( book.getPrice() ).thenReturn( price ); BookInventory inventory = mock(BookInventory.class); CreditCardService cardService = mock(CreditCardService.class); when(cardService.debit(any( CreditCard.class ), anyDouble() )) .thenReturn( CreditTransactionCode.SUCCESS ); BookStore store =
new BookStore(inventory, cardService); CreditCard card = mock(CreditCard.class); double total = store.purchase(
Collections.singletonList(book), card ); assertEquals( book.getPrice(), total, 0.0d ); verify( cardService ).debit( card, price ); }
Creating mock objects
The most interesting methods are all in the org.mockito.Mockitoclass
The mock method creates a mock object for an interface or class
• static <T> T mock(Class<T> classToMock) • Unless instructed otherwise, invoking a method of
the mock object will noop or return null
• Mockito uses crazy class loading tricks to accomplish this
We mocked all of our objects in the previous example • No need to invoke constructors
• Only had to specify the state we needed
– The test doesn’t care about the title or author
of the Book
• We can add a method to an interface without having to update a mock implementation
13
Specifying behavior of mock objects
The when method specifies how a method of the mock object should behave
• static <T> OngoingStubbing<T> when(T methodCall)
• Because they cannot be overridden by the magical class loader, final methods cannot be sent to when The OngoingStubbing class specifies what the behavior of the mock method
• thenReturn(T value) will cause the mock method to return a given value
• thenThrow(Throwable t) will throw an exception when the mock method is invoked
• thenCallRealMethod() will invoked the real implementation of the mock object to be invoked In general, you want to chain the when and then calls together
when( book.getPrice() ).thenReturn( price );
14
Method Argument Matching
The same mock method can be configured with different argument values
when( cardService.debit(card, price) .thenReturn(SUCCESS))
when( cardService.debit(card, -1.0) .thenReturn(INVALID_AMOUNT))
Sometimes the behavior of the mock method doesn’t depend on the value of its arguments
• Always return SUCCESS no matter which card is passed in
Matchers, the superclass of Mockito, has methods for filtering which values apply to the when
• Basically, there are bunch of type safe any methods • any() object (or null) or any(Class<T>) object of a
given type
• anyInt(), anyDouble(), anyString(), etc.
• isNull, isNotNull, and same(T), matches(String regex)
Method Argument Matching
If at least one arguments to a mock method is one of the “matchers”, then all arguments must be matchers
• Use the eq() methods for matching on a single value when( cardService.debit( any(CreditCard.class),
eq(-1.0) ) .thenReturn(INVALID_AMOUNT);
The AdditionalMatchers class has even more matchers • Boolean operations on matchers: and, or, not, etc. • Comparison operations: gt, lt, geq
Verifying Mock Objects
After configuring your mock objects and sending them to the API you want to test, the mock objects are verified
• Mock objects keep a history of what methods were invoked with which arguments
• The verify method tests whether or not a mock method was invoked with the expected arguments To verify that cardService was invoked with the expected cardand price:
verify( cardService ).debit( card, price ); You can also use matchers with verify
verify(cardService).debit( same(card), eq(price) );
17
Managing Dependencies
As your application scales, wiring together dependencies can become very cumbersome
• Large initialization code that references tons of classes
• Difficult to change out old implementations for new implementations
• End up relying on design patterns to create instances
– Factory Pattern
∗ Create instances of a class using a static method
– Singleton Pattern
∗ Access the one-and-only instance of a class via a static method
• Lots of boilerplate code that is tricky to get right
– Concurrent access to singletons – Life cycle of pooled/cached objects
18
Google Guice
Google Guice (pronounced “juice”) is a framework for configuring and injecting dependencies into Java classes
• Leverages annotations to provide type-safety • Well-known objects are registered in one place (a
“module”)
• Objects that depend on well-known objects are created by Guice via reflection
– Dependencies are injected into constructors and
fields Maven dependency: <dependency> <groupId>com.google.code.guice</groupId> <artifactId>guice</artifactId> <version>2.0</version> </dependency>
Managing Dependencies without Guice
From BookStoreApp.java
public static void main(String... args) throws JAXBException, IOException { String tmpdir =
System.getProperty("java.io.tmpdir"); File directory = new File(tmpdir); BookInventory inventory = new BookDatabase(directory); addBooks(inventory); CreditCardService cardService = new FirstBankOfPSU("localhost", 8080); Logger logger = Logger.getLogger("edu.pdx.cs410J.Logger"); logger.setLevel(Level.INFO); CheckoutPanel panel = new CheckoutPanel(inventory,cardService,logger); BookStoreGUI gui = new BookStoreGUI(panel);
gui.pack();
gui.setVisible( true ); }
If we want to introduce another panel that is for inventory control, we’d have to pass the well-known
Requesting Objects from Guice
Annotating one of a class’s constructors with @Inject instructs Guice to invoke that constructor when an instance is requested
• Guice will provide the well-known instances of the constructors arguments*
@Inject
public CheckoutPanel(BookInventory inventory, CreditCardService cardService, Logger logger ) {
If Guice is asked to create an instance of CheckoutPanel, it will invoke this constructor and provide an instance of BookInventoryand CreditCardService
• Your code doesn’t need to create the dependencies any more!
@Injectcan also be applied to methods and non-final instance fields methods that are invoked by Guice
• @Inject methods are invoked by Guice after the instance is created
*Guice automatically injects a Logger whose name is the name of the
class into which it is injected. And you can’t change it.
21
Guice Modules
A Guice “module” tells the Guice infrastructure about well-known objects
• Your modules subclass
com.google.inject.AbstractModule
Objects are “bound” into Guice using the bind method • Linking a type to its implementation (like a factory
method)
bind(BookInventory.class).to(BookDatabase.class); Whenever an object depends on a BookInventory, Guice will provide it with an instance of BookDatabase By default, Guice will create a new instance of a bound class when it is requested
Singleton objects in Guice can be configured in two ways • Annotating the object’s class with @Singleton • Binding the type in the singleton scope bind(CreditCardService.class)
.to(FirstBankOfPSU.class) .in(Singleton.class);
22
Annotations for Binding
The @Named annotation can be used to name well-known objects
From FirstBankOfPSU.java @Inject
public FirstBankOfPSU(
@Named("ServerHost") String serverHost, @Named("ServerPort") int serverPort ) In your module, you can bind the values of the named objects: bind(String.class) .annotatedWith(Names.named("ServerHost")) .toInstance("localhost"); bind(Integer.class) .annotatedWith(Names.named("ServerPort")) .toInstance( 8080 );
When Guice instantiates FirstBankOfPSU it will provide the values of the server host and port.
Annotations for Binding
You can also create your own annotation types for well-known objects
DataDirectoryis an annotation for the directory in which application data is stored
package edu.pdx.cs410J.di; import com.google.inject.BindingAnnotation; import java.lang.annotation.*; @BindingAnnotation @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.PARAMETER, ElementType.FIELD}) @Documented
public @interface DataDirectory { }
@BindingAnnotationmeans that the annotation being defined can be used with Guice
Annotations for Binding
You can annotate constructor parameters @Inject
public BookDatabase(@DataDirectory File directory) And bind the value in your module:
String tmpdir =
System.getProperty("java.io.tmpdir"); File directory = new File(tmpdir); bind(File.class)
.annotatedWith(DataDirectory.class) .toInstance(directory);
25
Creating an Injector
A Guice module is used to create an Injector which provides access to well-known objects
public class GuicyBookStoreApp extends BookStoreApp {
public static void main(String... args) { Injector injector = Guice.createInjector(new BookStoreModule()); BookInventory inventory = injector.getInstance(BookInventory.class); addBooks(inventory); BookStoreGUI gui = injector.getInstance(BookStoreGUI.class); gui.pack(); gui.setVisible(true); } }
Guice takes care of creating and configuring all of the dependencies of BookStoreGUI and wiring them together Ultimately, your code is more decoupled and can be reconfigured more easily
26
Provider Methods
Sometimes, objects need more than a constructor call in order to be initialized
• Or the constructor needs dependencies that are not provided by Guice
A module method that is annotated with @Provides constructs a Guice-managed instance of its return type This is probably a better way to configure the
DataDirectory File: @Provides
@DataDirectory
protected File provideDataDirectory() { String tmpdir =
System.getProperty("java.io.tmpdir"); return new File(tmpdir);
}
It would probably be better to construct the FirstBankOfPSUusing a provider method, also.
• You wouldn’t need the @Named constructor parameters that reused anywhere
Provider classes
A Provider class is used to create very expensive objects or objects that you want lazily initialize public class EncyrptionKeyProvider
implements Provider<EncryptionKey> {
private final EncryptionAlgorithm algorithm; @Inject
public EncyrptionKeyProvider(EncryptionAlgorithm algorithm) { this.algorithm = alrogithm;
}
@Override
public EncyrptionKey get() {
// Perform expensive operation on demand return new EncryptionKey(algorithm); }
}
Bind with:
bind(EncyrptionKey.class)
Provider classes
Guice can inject a provider into an object @Singleton
public class ConnectionManager { @Inject
private Provider<EncryptionKey> keyProvider; public ConnectionManager() {
// Should inject Provider as parameter instead }
public Connection createConnection() { return new Connection(keyProvider.get()); }
}
Even though the ConnectionManager is a singleton, it can get multiple EncyrptionKeys by injecting the Provider*
*Okay, ConnectionManager is a factory and we could probably doing
this Guicier, but you get the point
29
Summary
Dependency Injection is the ultimate expression of “programming to the interface”
• The “Hollywood Principle” states that objects should receive their dependencies upon construction • When dependencies are decoupled, finer-grained
testing is possible
• Mockito provides facilities for mocking objects and specifying mocked behavior
• Google Guice manages dependencies by separating configuration code from application code