I am aware that there is a similar question to this here. It considered a more generic question of class specific behaviour than my question here.
Consider the following simple implementation of a Composite pattern:
interface Item {
int getWeight();
}
class SimpleItem implements Item {
private int weight;
public int getWeight() {
return weight;
}
}
class Container implements Item {
private List<Item> items;
private int weight;
public void add(Item item) {
items.add(item);
}
public int getWeight() {
return weight + items.stream().mapToInt(Item::getWeight).sum();
}
}
Now consider how a user of Item can determine whether it is a container. For example a method is required in Container pushAdd
that pushes an item down the hierarchy to a container that has no containers inside it. The container only knows about Items, it doesn't know if those items are Containers or SimpleItems or some other class that implements Item.
There are three potential solutions:
1. Using instance of and casting
public void pushAdd(Item item) {
Optional<Container> childContainer = items.stream()
.filter(item instanceof Container)
.map(item -> (Container)item)
.findAny();
if (childContainer.isPresent()) {
childContainer.get().pushAdd(item);
} else {
add(item);
}
}
2. Implement is/as methods
public pushAdd(Item item) {
Optional<Container> childContainer = items.stream()
.filter(Item::isContainer)
.map(Item::asContainer);
....
}
3.
Visitor pattern (I've omitted the simple accept
implementation).
interface ItemVisitor {
default void visit(SimpleItem simpleItem) { throw ...}
default void visit(Container container) { throw ... };
}
public pushAdd(Item item) {
Optional<Container> childContainer = ... (using instanceOf);
if (childContainer.isPresent()) {
childContainer.get().accept(new ItemVisitor(item) {
void visit(Container container) {
container.pushAdd(item);
}
};
} else {
add(item);
}
}
The first is evil because it uses instanceof and casting. The second is evil because it forces knowledge of Container into Item - and it gets a lot worse when other subclasses of item are created. The third doesn't help you know whether you can add to the Item before calling the visitor. You can catch the exception but that seems to be a misuse of exceptions to me: much better to have a way to check before visiting.
So my question is: is there another pattern I can use to avoid casts and instanceof without having to push knowledge of subclasses up the hierarchy?