I'm currently designing an interface for a container that is supposed to store references of different instances that derived from a common supertype. An analogy of it would be as following:
Suppose there is fruit store where people can put different kinds of fruit. The fruit store should provide abilities to retrieve all fruits of the same type, for each and every type of fruit that it currently has.
fruits
public abstract class Fruit {}
public class Apple extends Fruit {}
public class Pear extends Fruit {}
public class Orange extends Fruit {}
My initial design is to create separate data structure for each and every kind of fruit. When listing a particular kind of fruit, we only need to read from corresponding data structure for that type.
public static class FruitStore {
private final List<Apple> apples = new ArrayList<>();
private final List<Pear> pears = new ArrayList<>();
private final List<Orange> oranges = new ArrayList<>();
public void add(Apple apple) {
this.apples.add(apple);
}
public List<Apple> getApples() {
return Collections.unmodifiableList(apples);
}
// same for pear and orange, omit for simplicity
}
A few issues that I'm seeing from this design:
- this solution is verbose and has lots of boilerplate code
- its requires us to have knowledge of all possible kinds of fruits that will be put into store to write the program
- if later we decide to support a new kind of fruit, say
Peach
, we have to modify source code ofFruitStore
, adding new data structure and APIs.
It seems to be a violation of open-closed principle (i.e. "system should be open for extension, but closed for modification)private final List<Peach> peaches = new ArrayList<>(); public void add(Peach peach); public List<Peach> getPeaches();
The good part of this design is that upon compilation, everything has already been determined therefore static analysis can ensure maximum type safety of this class before types get erased.
Then I come up with this design
public class FruitStoreV2 {
private final List<Fruit> fruits = new ArrayList<>();
public void add(Fruit fruit) {
this.fruits.add(fruit);
}
public <T extends Fruit> List<T> get(Class<T> fruitType) {
List<T> res = new ArrayList<>();
for (Fruit fruit : this.fruits) {
if (fruitType.isInstance(fruit)) {
res.add(fruitType.cast(fruit));
}
}
return res;
}
}
I'm still seeing a few issues in this design
- (biggest problem) every time when program wants to retrieve a particular kind of fruit, it has to iterate through the entire collection of fruits. This really harms the performance if there exist many different fruit instances. I think hard about this but can't come up with a reasonable way of caching the result.
- in order for Java to infer the type, we have to explicitly pass Class as hint
- usage of
Class#isInstance
andClass#cast
. If I understand them correctly, they are happening during runtime, thus we might lose the benefits of static compile-time type checking feature offered by Java - explicit casting is usually a code smell? (this one I'm really not sure though)
[Question 1] Which design above is better in your opinion? Could you please let me know the reasons?
[Question 2] For this particular scenario, is there any other better design that is possible? I'm looking for one that combines good parts of both designs (i.e. efficiency, type safe and open for extension)
Thank you for your help in advance!
Fruit
in the first place; the fact that you need to know the type of each object and downcast suggests a more fundamental problem in that these classes don't seem to belong together in an inheritance hierarchy, so you might be better off just treating them all as unrelated classes, and store them in separate repositories. (Perhaps you could say they're like Apples and Oranges... :-) )