Reputation: 71
I have been looking for design ideas to solve this problem in Java.
I am using a library (I cannot change it) that for this example, I'll just call "Animals". It contains an Animal interface, and a bunch of implementations; and I need to call different methods according to the implementation of the Animal I get:
List<Animal> animals = Service.getAnimals();
for(Animal a : animals) {
process(a);
}
private void process(Animal animal) {
if (animal instanceOf Cat) {
processCat(animal);
} else if (animal instanceOf Dog) {
processDog(animal);
} else {
System.out.println("unsopported animal");
}
}
I am currently solving this through reflection, with a class that holds all "Processors" and calling them using
String methodName = "process" + Animal getClass().getSimpleName(); //ugh
I am using Java 8 and I am pretty sure there must be better design to address this issue.
Any help is appreciated!
Upvotes: 6
Views: 811
Reputation: 661
I think that the answer provided by @RyanTheLeach addresses your problem quite nicely, via a registry based approach. However, in comments to your question the visitor pattern was brought up. I mentioned "if you create the instances yourself", and... it's not currently elaborated on by any of the existing answers. I find that subtle variations of the problem you've encountered here tend to be annoyingly frequent. Since implementing a wrapper based visitor pattern in Java can be a bit confusing, I thought I'd add some details about that particular approach.
First off, this seems like it should be extremely simple. Just create a visitor interface, a wrapper for the base type, and a wrapper method accepting visitor implementations.
public interface AnimalVisitor {
public void visit(Animal instance);
public void visit(Cat instance);
public void visit(Dog instance);
// ...
}
public class AnimalWrapper {
public final Animal instance;
// ...
public void acceptVisitor(AnimalVisitor visitor) {
visitor.visit(instance);
}
}
Method overloading will take care of the rest, right? It turns out that this won't work, because Java determines the method overload to call based on the statically determined type, as opposed to the actual type at runtime. Note that this is different from inheritance, where dynamic dispatch is used to look up the concrete implementation of a virtual method at runtime.
Perhaps we could tackle this with generics?
public class AnimalWrapper <T extends Animal> {
public final T instance;
// ...
public void acceptVisitor(AnimalVisitor visitor) {
visitor.visit(instance);
}
}
Nope. It might seem like the call to visitor.visit(instance)
should statically determine the subtype, but in reality the static type of a generic is the upper bound of its capture (in this case, Animal
).
Perhaps we could get around this by leveraging the usual Java pattern for obtaining runtime type information for generics?
public class AnimalWrapper <T extends Animal> {
public final T instance;
private final Class<T> type;
// ...
public void acceptVisitor(AnimalVisitor visitor) {
visitor.visit(type.cast(instance));
}
}
Unfortunately this won't work either, for the same reason as before; Class<T> type
is also generic, so it statically resolves to the basetype Animal
.
So how can we make this work? This answer illustrates quite clearly how to go about this using a separate wrapper class for every subtype you want to wrap. Wouldn't it be nice if we could pull this off without needing so many different unique wrapper classes though? You said you're using Java 8, so lets combine lambdas and static factory methods to achieve all of this more or less transparently (and with only a single wrapper class).
public abstract class Animal {
private final String instanceName;
private Animal(String instanceName) {
this.instanceName = instanceName;
}
public abstract String typeName();
public String instanceName() {
return instanceName;
}
public static class Cat extends Animal {
public Cat(String instanceName) {
super(instanceName);
}
@Override
public String typeName() {
return "Cat";
}
}
public static class Dog extends Animal {
public Dog(String instanceName) {
super(instanceName);
}
@Override
public String typeName() {
return "Dog";
}
}
public static class Fox extends Animal {
public Fox(String instanceName) {
super(instanceName);
}
@Override
public String typeName() {
return "Fox";
}
}
}
/*
* The Wrapper Class
*/
public class AnimalWrapper {
private final Animal instance;
private final Consumer<AnimalVisitor> visitResolver;
private AnimalWrapper(Animal instance, Consumer<AnimalVisitor> visitResolver) {
this.instance = instance;
this.visitResolver = visitResolver;
}
public Animal getInstance() {
return instance;
}
public void acceptVisitor(AnimalVisitor visitor) {
visitResolver.accept(visitor);
}
public static AnimalWrapper create(Animal instance) {
final Consumer<AnimalVisitor> visitResolver = visitor -> AnimalVisitor.visitResolver(visitor, instance);
return new AnimalWrapper(instance, visitResolver);
}
public static AnimalWrapper create(Animal.Cat instance) {
final Consumer<AnimalVisitor> visitResolver = visitor -> AnimalVisitor.visitResolver(visitor, instance);
return new AnimalWrapper(instance, visitResolver);
}
public static AnimalWrapper create(Animal.Dog instance) {
final Consumer<AnimalVisitor> visitResolver = visitor -> AnimalVisitor.visitResolver(visitor, instance);
return new AnimalWrapper(instance, visitResolver);
}
public static AnimalWrapper create(Animal.Fox instance) {
final Consumer<AnimalVisitor> visitResolver = visitor -> AnimalVisitor.visitResolver(visitor, instance);
return new AnimalWrapper(instance, visitResolver);
}
}
/*
* The Visitor Interface
*/
public interface AnimalVisitor {
public default void visit(Animal instance) {
printMessage("Default implementation", "Animal (base type)", instance);
}
public static void visitResolver(AnimalVisitor visitor, Animal instance) {
visitor.visit(instance);
}
public default void visit(Animal.Cat instance) {
printMessage("Default implementation", "Cat", instance);
}
public static void visitResolver(AnimalVisitor visitor, Animal.Cat instance) {
visitor.visit(instance);
}
public default void visit(Animal.Dog instance) {
printMessage("Default implementation", "Dog", instance);
}
public static void visitResolver(AnimalVisitor visitor, Animal.Dog instance) {
visitor.visit(instance);
}
public static void printMessage(String implementation, String signature, Animal instance) {
System.out.println();
System.out.println(implementation);
System.out.println("\tSignature: " + signature);
System.out.println("\tInstance type: " + instance.typeName());
System.out.println("\tInstance name: " + instance.instanceName());
}
}
/*
* The Visitor Implementation
*/
public class AnimalVisitorImpl implements AnimalVisitor {
@Override
public void visit(Animal instance) {
AnimalVisitor.printMessage("Specialized implementation", "Animal (base type)", instance);
}
@Override
public void visit(Animal.Cat instance) {
AnimalVisitor.printMessage("Specialized implementation", "Cat", instance);
}
}
/*
* Actual Usage
*/
public static void main(String[] args) {
final List<AnimalWrapper> wrappedAnimals = new ArrayList<>();
wrappedAnimals.add(AnimalWrapper.create(new Animal.Cat("A normal cat.")));
wrappedAnimals.add(AnimalWrapper.create((Animal) new Animal.Cat("A stealthy cat.")));
wrappedAnimals.add(AnimalWrapper.create(new Animal.Dog("A dog (only default support).")));
wrappedAnimals.add(AnimalWrapper.create(new Animal.Fox("A fox (no support).")));
final AnimalVisitor visitor = new AnimalVisitorImpl();
for (AnimalWrapper w : wrappedAnimals)
w.acceptVisitor(visitor);
}
Specialized implementation Signature: Cat Instance type: Cat Instance name: A normal cat. Specialized implementation Signature: Animal (base type) Instance type: Cat Instance name: A stealthy cat. Default implementation Signature: Dog Instance type: Dog Instance name: A dog (only default support). Specialized implementation Signature: Animal (base type) Instance type: Fox Instance name: A fox (no support).
Collection<Animal>
) from the third party code, this approach breaks down and we find ourselves back at a check-and-cast (or reflection) based approach. This is why the registry based approach outlined by @RyanTheLeach really shines; animalInstance.getClass()
returns the unique static class object that is shared by all instances of that particular subtype, thereby allowing us to retrieve the correct lambda even when presented with a basetype instance.Upvotes: 0
Reputation: 4470
If Animal's is a sealed class, that is, it isn't dynamically extendable, and has a limited, known number of sub-classes, Then the if-instanceof pattern you have stumbled across in the example is classic "pattern matching".
If Animal was a class you could control, then you could use the Visitor Pattern to create a visit method directly on Animal.
However you state that Animal is from an external library, which limits the approach you can take.
You can still use the Visitor pattern, keeping all code responsible for interacting with Animals in a single class, using method overloading to resolve the type at runtime (assuming you don't have any issues with generics).
But really this is just as inflexible as if-instanceof method, it just makes OO people feel better.
So, the approach to take comes down to code organization, and what makes sense for your code base.
Honestly, the if-instanceof is what I would go for, unless the amount of methods / behaviors starts getting too complex.
In which case, I'd create a type of registry, that registers a processor for each of the animal types.
Then, you can create a simple class that gets the required processor from the registry for the type of Animal.
This registry pattern I've seen used a lot in Minecraft, but I'm not sure if it's documented elsewhere.
Essentially it's use would look something like this.
void onApplicationStart(){
Registry registry = new Registry();
registry.register(Cat.class, cat -> catProcessor.process(cat));
registry.register(Dog.class, dog -> dogProcessor.process(dog));
registry.registerFallback(Animal.class, ani -> animalProcessor.process(ani));
}
Then later, you could fetch the registry, and the method that does the processing.
void whenNeeded(Animal animal){
Registry registry = fetchRegistrySomehow();
registry.for(animal.getClass()).apply(animal);
}
Edit: The implementation of registry is deliberately missing, as exact behavior differs depending on how you want to do the lookup, treat class hierarchies, when and if the registry should be sealed after some application started event.
Upvotes: 9
Reputation: 18245
I think you can hide you approach into beautiful candy wrapper, e.g. using Enum
or Class hierarchy
in case of many overroden methods.
E.g. this is two Animal
implementation with different methods name to get animal's name:
class Dog implements Animal {
public String getDogName() {
return "dog";
}
}
class Cat implements Animal {
public String getCatName() {
return "cat";
}
}
Then you can define an Enum
, with Functions
for each implementation:
enum AnimalRegistry {
DOG(Dog.class, animal -> ((Dog)animal).getDogName()),
CAT(Cat.class, animal -> ((Cat)animal).getCatName());
private final Class<? extends Animal> cls;
private final Function<Animal, String> getName;
AnimalRegistry(Class<? extends Animal> cls, Function<Animal, String> getName) {
this.cls = cls;
this.getName = getName;
}
public final String getName(Animal animal) {
return getName.apply(animal);
}
public static AnimalRegistry parseClass(Animal animal) {
for (AnimalRegistry registry : values())
if (registry.cls == animal.getClass())
return registry;
throw new RuntimeException("Unknown Animal implementation: " + animal.getClass().getSimpleName());
}
}
Finally, your client code could look like:
Animal animal = ...;
String animalName = AnimalRegistry.parseClass(animal).getName(animal);
Repeat, that if you have e.g. more that 2 methods to implement, Enum
becomes not so comfortable to use; then you can switch into class hierarhy and do exactly like in enum (do not forget, the in JVM each constant in Enum
is a different implementatino of Enum
interface).
P.S. Your approach is not so bad, it is pretty usefule in many cases:
private void process(Animal animal) {
if (animal instanceof Cat)
process((Cat)animal);
else if (animal instanceof Dog)
process((Dog)animal);
else
System.out.println("unsopported animal");
}
private void process(Cat cat) {
cat.getCatName();
}
private void process(Dog dog) {
dog.getDogName();
}
Upvotes: 1