Reputation: 13501
I've got a hangover, and I'm being particularly slow today.
I want to pass a subclass of Intent
to some factory, and get back an Handler<? extends Intent>
, where the parameterised type matches that of the argument passed to the factory.
public interface HandlerFactory
{
<I extends Intent> Handler<I> build(I intent);
}
I'm struggling with the implementation of this. I don't want a big nasty if/else checking for all the different types of Intent
, and then creating the appropriate Handler<>
for each one, and casting it based on which conditional block we end up in. I don't want to use some variation of the Visitor pattern, as then my factory becomes coupled to every subclass of Intent
/Handler<? extends Intent>
. Something more 'pluggable' seems desirable.
How can I implement this without some filthy casting? The simplified case below doesn't compile because whilst OrgHandler
implements Handler<OrgIntent>
, the compiler doesn't know that the Intent
passed (I
) was an OrgIntent
and not any other subclass.
public class SimpleHandlerFactory implements HandlerFactory
{
@Override
public <I extends Intent> Handler<I> build(I intent)
{
// Filthy conditional that I want to avoid
if(intent instanceof OrgIntent)
{
// Cast seems wrong
return (Handler<I>) new SimpleOrgHandler(intent);
}
else if(intent instanceof SomeOtherIntent)
{
...
}
}
Update
The client code has a List<Intent>
and doesn't really care about the subtypes; it needs to build a List<Handler>
(I figure I'll need to have a raw types suppression there) and then call a method on each of those.
List<Intent> intents = orderedIntentBuilder.getOrderedIntents(declaration);
for(Intent intent : intents)
{
Handler<Intent> handler = handlerFactory.build(intent);
handler.resolve(someUnimportantArg);
}
Upvotes: 2
Views: 72
Reputation: 33885
As it happens, the Intent classes are just data, and the rationale for the Handler classes is that I want to provide users of the library different ways of implementing behaviour that acts upon those data.
So here's the other idea I had.
You could instantiate the functionality before ambiguating the intent type, here is a rough example:
In this, SomeInputType
would be the type from which you derive
an intent's data.
The type Intent
is renamed to IntentModel
(since it's just a
data class).
The type Handler
is renamed to Intent
(since this is where
the real functionality is).
class OrgIntentModel implements IntentModel {
public OrgIntentModel(SomeInputType input) {...}
}
// Abstract factory pattern
interface IntentFactory {
// Constructs an Intent with custom behaviour.
// Using the data in the specific model.
Intent getOrgIntent(OrgIntentModel model);
// Getters for each model type
}
// This class is implemented by the user.
// You could provide a 'DefaultFactory' as a template to extend.
class UserProvidedFactory implements IntentFactory {...}
class IntentBuilder { // i.e. IntentDeserializer
private Map<String, Function<? super SomeInputType, ? extends Intent>> map;
public IntentBuilder(IntentFactory factory) {
map.put("OrgIntent", makeFunc(OrgIntentModel::new, factory::getOrgIntent));
// other mappings...
}
// Helper method, to help with casting the method references.
// 'U' represents the specific IntentModel.
// SomeInputType -> IntentModel -> Intent
private static <T, U, R> Function<T, R> makeFunc(
Function<T, ? extends U> f1,
Function<? super U, R> f2) {
return f1.andThen(f2);
}
public Intent getIntent(SomeInputType input) {
String intentType = input.getIntentType();
return map.get(intentType).apply(input);
}
}
Upvotes: 1
Reputation: 33885
I see the potential for a circular dependency here. You want to go from an Intent
to a Handler
while Intent
is not supposed to know about Handler
, and Handler
already knows about Intent
.
One could also wonder why a class needs a dedicated handler, shouldn't the handler functionality by part of the class itself then?
Any ways, one easy way is to add a getHandler
method to Intent
:
interface Handler<T extends Intent> {}
interface Intent {
Handler<?> getHandler();
}
class OrgIntent {
@Override
public Handler<OrgIntent> getHandler() {
return new SimpleOrgHandler(this);
}
}
...
List<Intent> intents = orderedIntentBuilder.getOrderedIntents(declaration);
for(Intent intent : intents)
{
Handler<?> handler = intent.getHandler();
handler.resolve(someUnimportantArg);
}
Which does create a circular dependency between the Intent
and the Handler
. Which you could be okay with, but there is also a way to solve that.
By extracting the data 'model' the handler operates on, into a third class:
interface IntentModel {...}
The Handler
now operates on this model instead of the Intent
itself.
class OrgIntent {
@Override
public Handler<OrgIntentModel> getHandler() { // <-- Handler<OrgIntentModel>
return new SimpleOrgHandler(this.model); // passing only model
}
}
Which creates the dependencies:
Intent -> IntentModel
Intent -> Handler
Handler -> IntentModel
In this case you might want to merge the Handler
functionality with the IntentModel
. There would be no circular dependencies there, because the model only operates on itself.
Upvotes: 2
Reputation: 26084
You can add the parameterized type to the factory class:
public interface HandlerFactory<I extends Intent> {
Handler<I> build(I intent);
}
and your SimpleHandlerFactory
:
public class SimpleHandlerFactory implements HandlerFactory<OrgIntent> {
@Override
public Handler<OrgIntent> build(OrgIntent intent) {
return new SimpleOrgHandler(intent);
}
}
Upvotes: 1