In my two previous posts I wrote about some problems with DI and a solution to part of those problems: assisted inject (as known in Guice)/autofactories (my implementation for CDI). Some problems remained however; for example in the bean constructor, the dependencies (or the environment) are mixed with the data obtained from the factory. That is also why the @FactoryParameter
annotation is required.
This shortcoming can be quite easily overcome, by using field injection for dependencies, and constructors for data from the factory. How does it work? And what about testing? Read on :).
The problem
The main goal is to be able to define beans, in which dependency injection works as usual and which can be constructed on-demand with user-supplied data. Something more object-oriented than creating stateless beans where the user-supplied data is passed in method parameters, and something with less boilerplate than implementing factories.
Just to remind, as an example throughout the posts I am using a ProductShipper
bean, which needs some outside dependencies (“services”, for example a PriceCalculatorService
), and must be provided with a Product
object.
The solution
The base interface remains the same:
public interface ProductShipper {
void ship(User target);
interface Factory {
ProductShipper create(Product product);
}
}
Again just to remind, the biggest advantage of the nested factory interface is that it is clear what is needed to create a bean (here the shipper), and we spare some typing as we don’t need to write a long class name for the factory.
Now for the implementation. Before both outside dependencies and factory parameters were passed in the constructor. Now, we’ll use field injection, making a clear separation:
@CreatedWith(ProductShipper.Factory.class)
public class ProductShipperImpl implements ProductShipper {
@Inject
private PriceCalculatorService priceCalculator;
@Inject
private TransportService transport;
private final Product product;
public ProductShipperImpl(Product product) {
this.product = product;
}
// implement shipTo(User)
}
Looks quite simple. The only thing we needed to do is to specify the factory for the bean (@CreatedWith
) and provide a constructor matching the parameters of the factory. Hence, one problem remains: there’s no compile-time checking (only deployment-time), if the arguments lists match. So there’s still room for improvement! :)
Usage
To use the autofactory, you simply have to inject the factory bean. The important thing is that you don’t have to implement the factory bean by hand, it is auto-generated. For example:
public class Test {
@Inject
private ProductShipper.Factory productShipperFactory;
public void test() {
ProductShipper shipper = productShipperFactory
.create(new Product("butter"));
shipper.shipTo(new User("me"));
}
}
Testing
Many would argue that using field injection instead of constructor injection makes the bean much harder to test. But we can improve that quite easily, thanks to the CDIInjector
helper class, from <a href="https://github.com/softwaremill/softwaremill-common/tree/master/softwaremill-util">softwaremill-util</a>
. Here’s how we can test our bean:
import static pl.softwaremill.common.util.CDIInjector.*;
public class ProductShipperTest {
@Test
public void testShipping() {
Product product = ...;
ProductShipper shipper = new ProductShipperImpl(product)
// Here's the important part: "dependency injection"
into(shipper).inject(mockPriceCalculator, mockService);
// Test goes on ...
}
}
So instead of setting the fields explicitly via reflection (where we would have to write the field names), we just need to provide the target of injection (<strong>into(shipper)</strong>
), and the objects that we want to be injected (<strong>.inject(...)</strong>
). We can also inject objects with qualifiers.
As always the code is available on github, as part of the softwaremill-common project (<a href="https://github.com/softwaremill/softwaremill-common/tree/master/softwaremill-cdi">softwaremill-cdi</a>
release 9 and <a href="https://github.com/softwaremill/softwaremill-common/tree/master/softwaremill-util">softwaremill-util</a>
release 13). The autofactories implementation is a portable extension (supports both styles: constructor and field/setter injection for dependencies), so all you need to do is to include the jar in your deployment.
Adam
comments powered by Disqus