The Opinionated Elf
Not that long ago, I was a student studying software engineering. Early in my studies, I was introduced to a dauntingly-named concept called polymorphism. Thankfully, it wasn’t that hard to understand, at least in concept. I could grasp how, while a dog might “bark” and a cat might “meow”, both of these animals are “speaking” in a way. But at an implementation level, I sometimes ran into issues getting polymorphism to “work”.
As a student, I assumed that polymorphism was just the “definitive” way to handle some class of objects through a common interface. And in my defense, the language I was learning at the time didn’t provide obvious alternatives to polymorphism—more on that in a bit.
If you’re farther along in your career, you might not get much out of this post. But for the students, I’d suggest reading on. Polymorphism is a powerful tool, but it doesn’t work (at least elegantly) in every case. Let’s take a look.
The Problem
If you’re a student, there’s a good chance that you know a bit about Java. In Java, dynamic polymorphism effectively means overriding virtual methods. This looks something like:
interface Animal {
void Speak();
}
class Dog implements Animal {
@Override
void Speak() {
System.out.println("bark");
}
}
public static void main(String[] args) {
Animal animal = new Dog();
animal.Speak(); // bark
}
And there’s nothing wrong with this—polymorphism seems fine here. Unfortunately, it’s not always so simple. Let’s examine what I consider a “classic” example—the Opinionated Elf (as posed by Steve Yegge). I recommend you Google it and read the original article end-to-end if you haven’t. It goes something like this:
“We are coding a video game with lots of different types of monsters and other creatures. One of those creatures is an elf with lots of opinions on the other creatures in the game.”
The elf, whenever it sees something else in the game, has to give an opinion on
that creature. Let’s say that a creature is represented by some interface
Creature; it’s not clear what we can do with them yet, so we’ll leave the
interface empty.
interface Creature {}
Then, since our elf is a creature in the game, it makes sense for it to
implement Creature.
class Elf implements Creature {}
Of course, there are lots of other creatures in the game too—like the elf,
they too will implement Creature.
class Goblin implements Creature {}
class Minotaur implements Creature {}
Now, let’s talk about this opinionated nature of the elf. Remember that this elf
can’t help but give its opinion on every other creature that it sees. If we’re
in a polymorphic static of mind, the most obvious approach is to slap a method
like elfOpinion on the Creature interface. That way, we can just use dynamic
dispatch on objects with a static type of Creature to call whatever function
we actually need.
interface Creature {
void elfOpinion();
}
Of course, since our Elf, Goblin, and Minotaur all implement Creature, they
need to implement this new method:
class Elf implements Creature {
@Override
void elfOpinion() {
System.out.println("It's me!");
}
}
class Goblin implements Creature {
@Override
void elfOpinion() {
System.out.println("Yuck, a goblin!");
}
}
class Minotaur implements Minotaur {
@Override
void elfOpinion() {
System.out.println("Cool, a minotaur!");
}
}
And to be clear, there’s nothing ostensibly wrong with this approach,
depending on what tradeoffs you are willing to make. Here, the compiler can
verify that Elf, Goblin, and Minotaur each satisfy Creature. Perhaps
even more importantly, if somebody were to come along and add a Gargoyle implements Creature to the code, they will get a compile-time error telling
them to implement elfOpinion, which we can probably agree is a good thing.
On the other hand, this approach has some drawbacks.
Primarily, we are polluting the Goblin and Minotaur with behavior that has
much more to do with the Elf than them. Here, the logic contained within
elfOpinion is quite simple, but it’s not hard to imagine circumstances where
it could be much more involved, potentially introducing complex logic or even
new dependencies that the rest of the class doesn’t use.
Additionally, if we were to ask “how is the opinionated elf implemented?”, we would have to look across many different classes to get our answer. Conceptually, it seems like its own distinct functionality.
Thankfully, there’s a couple of ways to make this functionality more distinct—the most simple of which is a dynamic type check. Let’s take a look at those.
Dynamic Type Checks
A dynamic type check inspects the dynamic type of a value. In Java, it’s
performed using the instanceof operator. If we wanted to make this opinionated
elf logic more self contained, we could easily put it off in its own (static)
function. That might look like:
class Opinions {
static void elfOpinion(Creature creature) {
if (creature instanceof Elf) {
System.out.println("It's me!");
}
else if (creature instanceof Goblin) {
System.out.println("Yuck, a goblin!");
}
else if (creature instanceof Minotaur) {
System.out.println("Cool, a minotaur!");
}
}
}
Again, this has both positives and negatives. This is much more cohesive and
arguably easier to read and reason about. However, we’ve lost compile-time
verification that every class which implements Creature has a registered
opinion with our elf. If we were to come along and add our Gargoyle again,
nothing happens. We can do a bit “better” by throwing an exception:
class Opinions {
static void elfOpinion(Creature creature) {
if (creature instanceof Elf) {
System.out.println("It's me!");
}
else if (creature instanceof Goblin) {
System.out.println("Yuck, a goblin!");
}
else if (creature instanceof Minotaur) {
System.out.println("Cool, a minotaur!");
} else {
throw new RuntimeException("Unexpected creature type!");
}
}
}
Still, this will be a runtime error—the compiler won’t tell us that this is going to happen.
Visitors
A common response to such a dilemma is the visitor pattern, which just extracts polymorphic functionality (that we would otherwise just append) to a different class, which is usually more focused.
Remember that (runtime) polymorphism works via dynamic dispatch; we invoke a
method on an object with the dynamic type Creature and consult an interface
table for the correct function to actually execute for its concrete type.
Here, we’ll use a visitor to bounce off of the dynamic dispatch for the
Creature and quickly execute some other chunk of code once we are in a context
where we know our concrete type.
This is probably pretty abstract, so let’s just see an example. First, let’s
rename our opinion method to accept, since we’re going to end up having the
Creature accept a visitor.
interface Creature {
void accept();
}
And of course that changes the implementers too.
class Elf {
@Override
void accept() {
// Here, we are clearly an Elf.
}
}
Let’s also take a second to point something important out; when we’re inside
accept, we know our concrete type (an Elf). Let’s pass in a Visitor to
this context to represent the logic we are extracting. Since we know our
concrete type, we’ll call a specialized method, just for the Elf.
class Elf {
@Override
void accept(Visitor visitor) {
visitor.visitElf();
}
}
And the same goes for the Goblin and Minotaur. Let’s take a look at the
Visitor internals.
class Visitor {
void visitElf() {
System.out.println("It's me!");
}
void visitGoblin() {
System.out.println("Yuck, a goblin!");
}
void visitMinotaur() {
System.out.println("Cool, a minotaur!");
}
}
Notice that the logic for the elf’s opinionated nature is now encapsulated in just one class, rather than being spread over several.
Runtime Checks
You might be thinking that was complicated; you’re probably not wrong. If we
didn’t want to break out a design pattern for this, we could always rely on
plain old runtime type checking. In Java, that uses the instanceof keyword. We
might stuff this logic over in an Opinions helper class.
class Opinions {
void opinion(Creature creature) {
if (creature instanceof Elf) {
System.out.println("It's me!");
}
else if (creature instanceof Goblin) {
System.out.println("Yuck, a goblin!");
}
else if (creature instanceof Minotaur) {
System.out.println("Cool, a minotaur!");
}
else {
// It's some other creature...
}
}
}