Advanced Java Programming Dependencies Among Objects
Consider the below set of dependent objects The Dependency Injection design pattern decouples dependent objects so that they may be configured and tested independently. Google BookInventory BookStore CreditCardService remove(Book) purchase(List
The Dependency Injection design pattern • A BookStore requires a BookInventory and a Testing with mock objects using Mockito CreditCardService to get its work done • Managing dependencies with Google Guice 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 • BookInventory and CreditCardService without affecting the code in BookStore
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 2
How can we implement BookStore? How can we implement BookStore? package edu.pdx.cs410J.di; public double purchase( List
When the BookStore is created, it also creates its dependencies
Dependencies are stored in final fields because • they only need to be created once
3 4 What is wrong with this solution? “Don’t call us, we’ll call you”
Sure, this code works, but.. The “Hollywood Principle” states that dependent code should require that its dependencies are provided to it You can’t test this code! • Practically speaking, it means pass dependencies – Tests would have to execute against the actual • into the constructor database (slow) and credit card service ($$$) public BookStore( BookInventory inventory, – Lots of data setup: create database file and credit CreditCardService cardService ) { card account this.inventory = inventory; this.cardService = cardService; The BookStore has to know how to configure its } • dependencies Now the BookStore class doesn’t have to know about the – Does the BookStore really care what port the concrete types of its dependencies credit card service runs on? Let the main program create and configure • dependencies If another class wants to use the BookDatabase, it • would have to create another instance public class BookStoreApp { public static void main(String... args) { – Two BookDatabases can’t work with the same file String tmpdir = System.getProperty( "java.io.tmpdir" ); File directory = new File( tmpdir ); If you want to switch out another implementation of BookInventory inventory = • the dependencies, you have to change the new BookDatabase( directory, "books.txt" ); BookStore CreditCardService cardService = new FirstBankOfPSU( "localhost", 8080 ); – Again, does the BookStore really care what kind BookStore store = of BookInventory is uses? new BookStore(inventory, cardService); 5 6
Testing with Mock Objects Testing with Mock Objects
Now that a BookStore gets passed its dependencies, you Test that BookStore invokes the expected methods of its can test the class without using the “production” dependencies BookInventory and CreditCardService Override interface methods to keep track of how it • Instead of using real dependencies, test with “mock” was invoked objects that provide an implementation of the interface package edu.pdx.cs410J.di; that is useful for testing import org.junit.Test; package edu.pdx.cs410J.di; import java.util.Collections; import static org.junit.Assert.assertEquals; public abstract class MockObject { import static org.junit.Assert.assertNotNull; protected void shouldNotInvoke() { throw new UnsupportedOperationException( public class BookStoreTest { "Did not expect this method to be invoked"); @Test } public void testBookIsPurchased() { } final Book[] removedBook = new Book[1]; BookInventory inventory = new MockBookInventory() { public void remove( Book book ) { package edu.pdx.cs410J.di; removedBook[0] = book; } public class MockBookInventory extends MockObject }; implements BookInventory { CreditCardService cardService = new MockCreditCardService() { public CreditTransactionCode debit( CreditCard card, double amount ) { public void remove( Book book ) { return CreditTransactionCode.SUCCESS; shouldNotInvoke(); } }; } }
7 8 Testing with Mock Objects Mock Objects are Awkward
Once the mock dependencies are set up Mock Objects get the job done, but...
BookStore Create the Lots of verbose boilerplate code • • Invoke the method you want to test • When interface changes (new method, e.g.), mock • Verify that the dependency was invoked as you has to change • expected Subclassing mock objects is hokey BookStore store = • new BookStore(inventory, cardService); – Lots of anonymous inner classes CreditCard card = new CreditCard( "123" ); final Book testBook = – Have to keep track of state that was passed to new Book("title", "author", 1.00d); dependency double total = store.purchase( Collections.singletonList(testBook), card ); assertEquals( testBook.getPrice(), total, 0.0d ); There’s got to be a better way, right?
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 10
Mock Objects with Mockito Mock Objects with Mockito
There are several testing libraries for Java that allow you @Test to easily create mock objects public void testCreditCardIsCharged() { Book book = mock(Book.class); double price = 1.00d; We’re going to learn about Mockito (http://mockito.org/) when( book.getPrice() ).thenReturn( price ); A Java API that creates mock objects for interfaces • BookInventory inventory = and classes mock(BookInventory.class); Contains a domain specific language (DSL) for • CreditCardService cardService = constructing mock objects mock(CreditCardService.class); when(cardService.debit(any( CreditCard.class ), You can specify what behavior you expect out of the • anyDouble() )) mock objects .thenReturn( CreditTransactionCode.SUCCESS );
Add the dependency in your pom.xml BookStore store =
Let’s take a closer look...
11 12 Creating mock objects Specifying behavior of mock objects
The most interesting methods are all in the The when method specifies how a method of the mock org.mockito.Mockito class object should behave
static
static
13 14
Method Argument Matching Method Argument Matching
The same mock method can be configured with different If at least one arguments to a mock method is one of the argument values “matchers”, then all arguments must be matchers
Use the eq() methods for matching on a single value when( cardService.debit(card, price) • .thenReturn(SUCCESS)) when( cardService.debit( any(CreditCard.class), when( cardService.debit(card, -1.0) eq(-1.0) ) .thenReturn(INVALID_AMOUNT)) .thenReturn(INVALID_AMOUNT);
Sometimes the behavior of the mock method doesn’t The AdditionalMatchers class has even more matchers depend on the value of its arguments Boolean operations on matchers: and, or, not, etc. • Comparison operations: gt, lt, geq 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
15 16 Verifying Mock Objects Managing Dependencies
After configuring your mock objects and sending them to As your application scales, wiring together dependencies the API you want to test, the mock objects are verified can become very cumbersome
Mock objects keep a history of what methods were Large initialization code that references tons of • invoked with which arguments • classes
The verify method tests whether or not a mock Difficult to change out old implementations for new • method was invoked with the expected arguments • implementations
To verify that cardService was invoked with the expected End up relying on design patterns to create instances card and price: • – Factory Pattern verify( cardService ).debit( card, price ); Create instances of a class using a static ∗ method You can also use matchers with verify – Singleton Pattern verify(cardService).debit( same(card), eq(price) ); 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
17 18
Google Guice Managing Dependencies without Guice
Google Guice (pronounced “juice”) is a framework for From BookStoreApp.java configuring and injecting dependencies into Java classes public static void main(String... args) throws JAXBException, IOException { Leverages annotations to provide type-safety • String tmpdir = Well-known objects are registered in one place (a • System.getProperty("java.io.tmpdir"); “module”) File directory = new File(tmpdir); BookInventory inventory = Objects that depend on well-known objects are • new BookDatabase(directory); created by Guice via reflection addBooks(inventory); – Dependencies are injected into constructors and CreditCardService cardService = fields new FirstBankOfPSU("localhost", 8080);
Maven dependency: Logger logger = Logger.getLogger("edu.pdx.cs410J.Logger");
If we want to introduce another panel that is for inventory control, we’d have to pass the well-known BookInventory instance to that code, too.
19 20 Requesting Objects from Guice Guice Modules
Annotating one of a class’s constructors with @Inject A Guice “module” tells the Guice infrastructure about instructs Guice to invoke that constructor when an well-known objects instance is requested Your modules subclass Guice will provide the well-known instances of the • com.google.inject.AbstractModule • constructors arguments* Objects are “bound” into Guice using the bind method @Inject public CheckoutPanel(BookInventory inventory, Linking a type to its implementation (like a factory CreditCardService cardService, • method) Logger logger ) { bind(BookInventory.class).to(BookDatabase.class); If Guice is asked to create an instance of CheckoutPanel, Whenever an object depends on a BookInventory, it will invoke this constructor and provide an instance of Guice will provide it with an instance of BookDatabase BookInventory and CreditCardService By default, Guice will create a new instance of a bound Your code doesn’t need to create the dependencies class when it is requested • any more! Singleton objects in Guice can be configured in two ways @Inject can also be applied to methods and non-final Annotating the object’s class with @Singleton instance fields methods that are invoked by Guice • Binding the type in the singleton scope @Inject methods are invoked by Guice after the • • instance is created bind(CreditCardService.class) .to(FirstBankOfPSU.class) *Guice automatically injects a Logger whose name is the name of the .in(Singleton.class); class into which it is injected. And you can’t change it.
21 22
Annotations for Binding Annotations for Binding
The @Named annotation can be used to name well-known You can also create your own annotation types for objects well-known objects
From FirstBankOfPSU.java DataDirectory is an annotation for the directory in which application data is stored @Inject public FirstBankOfPSU( package edu.pdx.cs410J.di; @Named("ServerHost") String serverHost, @Named("ServerPort") int serverPort ) import com.google.inject.BindingAnnotation;
import java.lang.annotation.*; In your module, you can bind the values of the named objects: @BindingAnnotation @Retention(RetentionPolicy.RUNTIME) bind(String.class) @Target({ElementType.PARAMETER, ElementType.FIELD}) .annotatedWith(Names.named("ServerHost")) @Documented .toInstance("localhost"); public @interface DataDirectory { bind(Integer.class) .annotatedWith(Names.named("ServerPort")) } .toInstance( 8080 ); @BindingAnnotation means that the annotation being When Guice instantiates FirstBankOfPSU it will provide defined can be used with Guice the values of the server host and port.
23 24 Annotations for Binding Creating an Injector
You can annotate constructor parameters A Guice module is used to create an Injector which provides access to well-known objects @Inject public BookDatabase(@DataDirectory File directory) public class GuicyBookStoreApp extends BookStoreApp { And bind the value in your module: public static void main(String... args) { Injector injector = String tmpdir = Guice.createInjector(new BookStoreModule()); System.getProperty("java.io.tmpdir"); BookInventory inventory = File directory = new File(tmpdir); injector.getInstance(BookInventory.class); bind(File.class) addBooks(inventory); .annotatedWith(DataDirectory.class) .toInstance(directory); 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
25 26
Provider Methods Provider classes
Sometimes, objects need more than a constructor call in A Provider class is used to create very expensive order to be initialized objects or objects that you want lazily initialize
Or the constructor needs dependencies that are not • public class EncyrptionKeyProvider provided by Guice implements Provider
A module method that is annotated with @Provides private final EncryptionAlgorithm algorithm; constructs a Guice-managed instance of its return type @Inject This is probably a better way to configure the public EncyrptionKeyProvider(EncryptionAlgorithm algorithm) { DataDirectory File: this.algorithm = alrogithm; @Provides } @DataDirectory protected File provideDataDirectory() { @Override String tmpdir = public EncyrptionKey get() { System.getProperty("java.io.tmpdir"); // Perform expensive operation on demand return new File(tmpdir); return new EncryptionKey(algorithm); } } } It would probably be better to construct the FirstBankOfPSU using a provider method, also. Bind with: You wouldn’t need the @Named constructor • parameters that reused anywhere bind(EncyrptionKey.class) .toProvider(EncyrptionKeyProvider.class);
27 28 Provider classes Summary
Guice can inject a provider into an object Dependency Injection is the ultimate expression of “programming to the interface” @Singleton The “Hollywood Principle” states that objects should public class ConnectionManager { • @Inject receive their dependencies upon construction private Provider
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 30