Search code examples
javagenericsfactory-patterndesign-principles

How to resolve this dilemma when using generics in a simple factory


I have a Data class with several sub-class such as JSONData, XMLData, IntegerData. The task is to process different types of incoming data. So based on program to an interface, not an implementation, I created the following interface with generic type for compile-time type checking:

interface DataProcessor<T extends Data> {
    void process(T data);
}

There are several implemenations based on this interface:

* `class JSONDataProcessor implements DataProcessor<JSONData>`
* `class XMLDataProcessor implements DataProcessor<XMLData>`
* `class IntegerDataProcessor implements DataProcessor<IntegerData>`

The rest of the work is to make a simple factory for creating the corresponding DataProcessor instance. So I made the following simple factory, or say it is in fact just a processor mapper as the concrete processor can be cached as static variables inside the ProcessorFactory:

public class ProcessorFactory {
    public static DataProcessor<?> create() {
          //logic of return an instance
    }
}

The design above has a problem - the process method on the returned instance of DataProcessor cannot be called directly:

Data data = jsonData;
ProcessorFactory.create().process(data);

Question: the code above has a compilation error due to the compile-time typing checking as the data has to be the concrete sub-class of Data, how to resolve this? Or is the design per se. bad? if so what would be a better design?


Solution

  • While design patterns are cool and all, the compilation error you reported in your question, was not caused by the lack of double dispatch.

    You get the compilation error because by declaring, for example, this: JSONDataProcessor implements DataProcessor<JSONData>{...} you've declared this method: void process(JSONData data).

    You are probably assuming that <T extends Data> means you can pass an object instance with the static type Data into void process(JSONData data) because, after all, Data extends Data. Except that's not how Java works.

    One way to look at the cause of your compilation error is to consider a method declaration like: public static void main(String arg){...}. Even though String extends Object, it is illegal to pass a reference with static type Object into a method declared as main(String).

    You would get the same compilation error you got with your DataProcessor if you tried to call that method as main(new Object()). It would be overkill to correct a mistake that you made, by introducing an unnecessary design pattern. In both your case and in the case of main(String), the simplest correction is to pass in the type that the method is declared to take.

    …how to resolve this?…

    The simplest solution, in my opinion, is to use your methods the way you declared them originally. If yours is implemented in a similar way that mine is, then I've confirmed that this works…

    ...
    JSONData data = new JSONData( ... );
    ProcessorFactory.create().process(data);
    ...
    

    This also works in my demo (no design patterns required)…

    DataProcessor< Data< ? > > dProc = DataProcessor.Factory.create( );
    Data<String> data = new JSONData( ... );
    dProc.process( data );
    

    …is the design per se. bad?…

    To call a design „good“ or „bad“ is subjective. It's more objective to ask yourself: Is the design correct? Does it correctly do what you intend it to do? If it does what you intend it to do, then it's correct. If it doesn't, then back to the white drawing board.

    Another design option you have is to decide not to use Generics at all — nor design patterns. Something way simpler might be all you need.

    You mentioned: „program to an interface“. Maybe all your design needs is plain old subtype polymorphism in the form of old-fashioned interfaces. Generics might not be the best design choice for what you want to do.