To implement your own custom actor in Akka (Java binding) you extend the UntypedActor
base class. This requires you to define your own onReceive(...)
method:
@Override
public void onReceive(Object message) {
// TODO
}
The problem at hand is determining a message handling strategy that enables actors to handle multiple types of messages. One strategy would be to use reflection/types. The problem here is that:
message
parameter and prevents us from being able to pass anything dynamic or meaningfulExample of an empty shell class:
public class EmptyShellMessage { }
Then in the onReceive
method would look like:
@Override
public void onReceive(Class<?> message) {
if(message.isAssignableFrom(EmptyShellMessage.class)) {
// TODO
} else {
// TODO
}
}
So not only do we create an otherwise-useless class, but since the Object message
is now being used to convery what class/type the message is, we can't use it to contain any more info, especially dynamic/runtime info that another actor might want to pass it.
Sometimes I see a variation of this:
@Override
public void onReceive(Object message) {
if(message instanceof FizzEvent) {
// TODO
} else {
// TODO
}
}
But here we're using instanceof
which is considered by many to be a huge anti-pattern (just google "instanceof antipattern").
Then we have enums:
public enum ActorMessage {
FizzEvent,
BuzzEvent,
FooEvent,
BarEvent
}
Now onReceive
looks like:
@Override
public void onReceive(ActorMessage message) {
if(message.equals(ActorMessage.FizzEvent)) {
// TODO
} else {
// TODO
}
}
The problem here is that we may have a large actor system with hundreds or even thousands of different event/message types to handle. This enum becomes large and difficult to maintain. It also has the same problem as the reflection strategy above where it prevents us from sending any dynamic info between actors.
The last thing I can think of is to use Strings:
@Override
public void onReceive(String message) {
if(message.equals("FizzEvent")) {
// TODO
} else {
// TODO
}
}
But I hate this. Period. End of sentence.
So I ask: am I missing something obvious here, another strategy perhaps? How are Java/Akka apps supposed to be handle large numbers of event/message types, and specify which one they are handling in the onReceive
method?
No you are not missing anything. I am not a fan either. In Scala it is a bit better because the onReceive method can be swapped out to simulate the changing state of a protocol and it uses partial functions which are a little nicer than if/elseif/else... but it is still icky. It is nicer in Erlang, which is where this model originated but that said given the limitations faced by the akka team, they made the right design choices and did an excellent job.
An alternative strategy is to perform a double dispatch. So pass the command into the actor to be actions, or lookup in a map a handler for the message. Akka agents are essentially the former, and when used to their strength are quite nice. But in general a double dispatch just adds complexity, so for most cases one just has to get used to the standard approach. Which in java means either if instanceof or a switch statement.
An example of double dispatch (psuedo code), included here for completeness, to aid understanding. As an approach it comes with health warnings, so I reiterate; the standard approach is standard for a reason and that one should use that 99% of the time.
onReceive( msg ) {
msg.doWork()
}
The problem with this approach is that now the message needs to know how to process itself, which is dirty; it causes tight coupling and can be fragile. Yuck.
So we can go with using a lookup for handlers (command pattern stylie)
val handlers = Map( "msgid1"->Handler1, "msgid2->Handler2 )
onReceive( msg ) {
val h = handlers(msg.id)
h.doWork( msg )
}
the problem here is that now it is unclear what the commands are and following the code through involves jumping around more. But there are times that this is worth while.
As pointed out by Roland, care must be taken when passing around a reference to the actor itself. The examples above do not fall foul of that problem, but it would be an easy temptation to do.