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!
1 Answer 1
Your second approach seems more extensible to me. Instead of iterating through the full list of Fruit
s on each retrieval, you could store them in a map:
class FruitStoreV2_1 {
private final Map<Class<? extends Fruit>, List<Fruit>> fruitMap = new HashMap<>();
public void add(Fruit fruit) {
fruitMap.computeIfAbsent(fruit.getClass(), c -> new ArrayList<>()).add(fruit);
}
public <T extends Fruit> List<T> get(Class<T> fruitType) {
return (List<T>) fruitMap.getOrDefault(fruitType, Collections.emptyList());
}
}
-
Thanks Archie. I tried to upvote u but my current reputation is not enough. Yeah I also think about using Map w/ Class the key. However, a few issues that I can foresee with this approach: 1) Map equality becomes be tricky if multiple ClassLoaders are involved, which is inevitable in web container application 2) Let say If I separately define the type using interface (e.g.
Apple
andAppleImpl
), in that case if I invokeFruitStoreV2_1.get(Apple.class)
andFruitStoreV2_1.get(AppleImpl.class)
conceptually I would expect the same result but it won'ttorez233– torez23311/14/2021 00:34:15Commented Nov 14, 2021 at 0:34 -
also, I think #2 is still a problem even if we switch to use FQDN of class instead of class itself as the key for the maptorez233– torez23311/14/2021 00:38:41Commented Nov 14, 2021 at 0:38
Explore related questions
See similar questions with these tags.
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... :-) )