Java Collections – Using Empty Superclass for Derived Classes

collectionsinterfacesjava

Basically, what I would like to obtain is a way to iterate through ONE list, and call methods specific to the interface the objects in the collection implement.

In my Java project, it would result in something like this:

//GameComponent would be my empty abstract class  
HashSet<GameComponent> components = new HashSet<GameComponent>(); 
components.add(/* instance of a class that extends GameComponent and implements Drawable || Updatable */);  

for (Drawable d : components)
    d.draw();

for (Updatable u : components)
    u.update();

My friends suggested me to just make two separate lists, but that means some objects will be in both lists.
I also think that it makes sense (abstraction-wise) to have one list of GameComponents and iterate through that one list.
But I also don't like having an empty class to inherit from, only for categorisation.

So, the question is: does my approach make sense? If so, is it elegant? If not, what pattern could I follow?

Best Answer

A couple of thoughts:

  1. It's better if game components don't know how to draw themselves. A class should have only one reason to change; here your components may change if you need to change what they do in-game, and they could change if you need to change how they're drawn as well.

  2. For this scenario the most straightforward solution is use instanceof. It's not the most general solution but it's the simplest thing that'll solve this particular problem.

    for (GameComponent component : gameComponents) {
        if (component instanceof Drawable) {
            ((Drawable) component).draw();
        }
        if (component instanceof Updatable) {
            ((Updatable) component).update();
        }
    }
    
  3. If you have a finite set of GameComponent types, what you want is a sum type, which mainstream OOP languages don't provide native support for but you can kind of emulate through the Visitor Pattern. (There's an alternative way of implementing them but it won't be elegant in Java; it works best with named arguments and lambda expressions.) This is a more general solution to taking decisions based on the particular kind of GameComponent because it gives you full access to the component type.

It would look something like this:

    public interface ComponentVisitor {
         void visit(Foo foo);
         void visit(Bar bar);
         void visit(Baz baz);
    }

    public abstract class GameComponent {
        // prevent subclassing outside this scope
        private GameComponent() {}

        public abstract void accept(ComponentVisitor visitor);

        public static final class Foo extends GameComponent implements Drawable, Updatable {
            // implement ...

            public void accept(Componentvisitor visitor) {
                visitor.visit(this);
            }
        }

        public static final class Bar extends GameComponent implements Updatable {
            // implement ...

            public void accept(Componentvisitor visitor) {
                visitor.visit(this);
            }
        }

        public static final class Baz extends GameComponent {
            // implement ...

            public void accept(Componentvisitor visitor) {
                visitor.visit(this);
            }
        }
    }

Now you can take component-specific decisions like this:

    for (GameComponent component : gameComponents) {
        component.accept(new ComponentVisitor() {
            public void visit(Foo foo) {
                // do something Foo-specific here
            }

            public void visit(Bar bar) {
                // do something Bar-specific here
            }

            public void visit(Baz baz) {
                // do something Baz-specific here
            }
        });
    }

This works because any component needs to implement accept, and for visit(this) to compile the Visitor needs to have a method that accepts that particular kind of component. It's still possible to implement a component such that accept doesn't call visit(this), so we safeguard against that by using a private constructor. That prevents outside subclassing while allowing subclassing of inner classes (because inner classes have access to private members of their enclosing class.) The end result is a finite set of components and an exhaustive way to discriminate between them (you can't forget to handle one of them because the Visitor implementation wouldn't compile.)