Question

I've currently got the following classes/interfaces laid out. The type T represents the format of data returned from DataProvider implementations. I'm using a factory so I don't need to attach type information to MyStreamingOutput. I'm using HK2 to inject the DataProviderFactory into MyStreamingOutput.

public interface DataProvider<T> {
    public T next() { ... }
    ...
}

public final class SQLDataProvider<T> {
    public SQLDataProvider(final String query, final RowMapper<T> rowMapper) { ... }
}

public interface DataProviderFactory {
    public <T> DataProvider<T> getDataProvider(final String query, final RowMapper<T> rowMapper);
    ...
}

public final class SQLDataProviderFactory {
    public <T> DataProvider<T> getDataProvider(final String query, final RowMapper<T> rowMapper) {
        return new SQLDataProvider<T>(query, rowMapper);
    }
}

public final class MyStreamingOutput implements StreamingOutput {
    public MyStreamingOutput(final DataProviderFactory dpFactory) { ... }
    @Override public void write(final OutputStream outputStream) throws IOException { ... }
}

This all works fine. Now I'm trying to set up a unit test for MyStreamingOutput, but I'm running into a couple of roadblocks. I wrote the following additional class for testing purposes:

public final class DataProviderFactoryStub implements DataProviderFactory {
    private final DataProvider dataProvider;

    public DataProviderFactoryStub() {
        this.dataProvider = new DataProviderStub();
    }

    public DataProviderFactoryStub(final DataProvider dataProvider) {
        this.dataProvider = dataProvider;
    }

    @Override
    public <T> DataProvider<T> getDataProvider(final String query, final RowMapper<T> rowMapper) {
        return this.dataProvider;
    }
}

The binding occurs in

final class QueryTestResourceConfig extends ResourceConfig {

    public QueryTestResourceConfig() {
        ...

        this.register(new AbstractBinder() {
            @Override
            protected void configure() {
                bind(DataProviderFactoryStub.class).to(DataProviderFactory.class);
            }
        });
    }

}

I can successfully inject this class into MyStreamingOutput, but it has a compiler warning because the typing information used by getDataProvider() isn't shared by the instance passed into the factory. I can't add type information to the DataProviderFactoryStub class because then it no longer implements the DataProviderFactory interface. I don't want type information on the interface because it's wrong - outside of the Stub case, the factories shouldn't care about the type returned by DataProvider instances. I'd very much like to avoid using setters for the query and rowMapper parameters because I consider it bad design in this case.

I can't shake the feeling that I'm either missing something subtle in my application of generics or something obvious in my application of dependency injection. What is the right way to address this use case? It seems like this is the kind of problem DI is meant to address, but I can't see how to fix it.

Was it helpful?

Solution 2

Unfortunately, due to type erasure, it isn't possible to do what I want. I will have to look at refactoring the existing code.

OTHER TIPS

When using DI, we usually end up with factory classes that are very basic (i.e., their creation methods are typically simple enough to fit on a single line). Your SQLDataProviderFactory class is a perfect example of this.

The reason for this is because a factory object is just a placeholder for the creation of an object. We want to avoid littering our code with new keywords, because doing so tightly couples code to a specific type. So we end up with factories whose methods are essentially just glorified new keywords.

I bring this up to point out that it's the type of the product that is important here; the factory is just a conduit. When you replace a factory with a test double, what you're really doing is replacing a product with a test double. This means that whenever I define a test double factory, I always have to define a test double product as well.

For example, your stub factory is just trying to return a stub product. The problem is that the type of the stub product it's returning does not match the type expected by calling code. If you define your own stub product, the code falls into place:

public final class DataProviderStub<T> implements DataProvider<T> {
    private final T dummy;
    public DataProviderStub() { }
    public T next() { return this.dummy; } // Just for example
}

public final class DataProviderFactoryStub implements DataProviderFactory {
    public DataProviderFactoryStub() { }

    @Override
    public <T> DataProvider<T> getDataProvider(final String query, final RowMapper<T> rowMapper) {
        return new DataProviderStub<T>();
    }
}

The stub factory only exists so you can inject the stub DataProvider into your SUT.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top