0

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 of FruitStore, adding new data structure and APIs.
    private final List<Peach> peaches = new ArrayList<>();
    
    public void add(Peach peach);
    
    public List<Peach> getPeaches();
    
    It seems to be a violation of open-closed principle (i.e. "system should be open for extension, but closed for modification)

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 and Class#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
  • 3
    The issue with this question is that you're using a contrived example; if this were a real problem I would ask what benefit you gain by deriving all of these classes from 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... :-) ) Nov 14, 2021 at 7:24

1 Answer 1

1

Your second approach seems more extensible to me. Instead of iterating through the full list of Fruits 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());
    }
}
2
  • 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 and AppleImpl), in that case if I invoke FruitStoreV2_1.get(Apple.class) and FruitStoreV2_1.get(AppleImpl.class) conceptually I would expect the same result but it won't
    – torez233
    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 map
    – torez233
    Nov 14, 2021 at 0:38

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.