How to call the correct method in Scala/Java based the types of two objects without using a switch statement?

折月煮酒 提交于 2019-11-30 03:58:27

You can easily implement multiple dispatch in Scala, although it doesn't have first-class support. With the simple implementation below, you can encode your example as follows:

object Receive extends MultiMethod[(Entity, Message), String]("")

Receive defImpl { case (_: Fighter, _: Explosion) => "fighter handling explosion message" }
Receive defImpl { case (_: PlayerShip, _: HullBreach) => "player ship handling hull breach" }

You can use your multi-method like any other function:

Receive(fighter, explosion) // returns "fighter handling explosion message"

Note that each multi-method implementation (i.e. defImpl call) must be contained in a top-level definition (a class/object/trait body), and it's up to you to ensure that the relevant defImpl calls occur before the method is used. This implementation has lots of other limitations and shortcomings, but I'll leave those as an exercise for the reader.

Implementation:

class MultiMethod[A, R](default: => R) {
  private var handlers: List[PartialFunction[A, R]] = Nil

  def apply(args: A): R = {
    handlers find {
      _.isDefinedAt(args)
    } map {
      _.apply(args)
    } getOrElse default
  }

  def defImpl(handler: PartialFunction[A, R]) = {
    handlers +:= handler
  }
}

If you're really worried about the effort it takes to create/maintain the switch statement, you could use metaprogramming to generate the switch statement by discovering all EventMessage types in your program. It's not ideal, but metaprogramming is generally one of the cleanest ways to introduce new constraints on your code; in this case that'd be the requirement that if an event type exists, there is a dispatcher for it, and a default (ignore?) handler that can be overridden.

If you don't want to go that route, you can make EventMessage a case class, which should allow the compiler to complain if you forget to handle a new message type in your switch statement. I wrote a game server that was used by ~1.5 million players, and used that kind of static typing to ensure that my dispatch was comprehensive, and it never caused an actual production bug.

Chain of Responsibility

A standard mechanism for this (not scala-specific) is a chain of handlers. For example:

trait Handler[Msg] {
  handle(msg: Msg)
}

Then your entities just need to manage a list of handlers:

abstract class AbstractEntity {

    def handlers: List[Handler]

    def receive(msg: Msg) { handlers foreach handle }
}

Then your entities can declare the handlers inline, as follows:

class Tank {

   lazy val handlers = List(
     new Handler {
       def handle(msg: Msg) = msg match {
         case ied: IedMsg => //handle
         case _           => //no-op
       }
     },
     new Handler {
       def handle(msg: Msg) = msg match {
         case ef: EngineFailureMsg => //handle
         case _                    => //no-op
       }
     }
   )

Of course the disadvantage here is that you lose readability, and you still have to remember the boilerplate which is a no-op catch-all case for each handler.

Actors

Personally I would stick with the duplication. What you have at the moment looks a lot like treating each entity as if it is an Actor. For example:

class Tank extends Entity with Actor {

  def act() { 
    loop {
      react {
         case ied: IedMsg           => //handle
         case ied: EngineFailureMsg => //handle
         case _                     => //no-op
      }
    }
  }
}

At least here you get into the habit of adding a case statement within the react loop. This can call another method in your actor class which takes the appropriate action. Of course, the benefit of this is that you take advantage of the concurrency model provided by the actor paradigm. You end up with a loop which looks like this:

react {
   case ied: IedMsg           => _explosion(ied)
   case efm: EngineFailureMsg => _engineFailure(efm)
   case _                     => 
}

You might want to look at akka, which offers a more performant actor system with more configurable behaviour and more concurrency primitives (STM, agents, transactors etc)

No matter what, you have to do some updating; the application won't just magically know which response action to do based off of the event message.

Cases are well and good, but as the list of messages your object responds to gets longer, so does its response time. Here is a way to respond to messages that will respond at the same speed no matter how many your register with it. The example does need to use the Class object, but no other reflections are used.

public class GameEntity {

HashMap<Class, ActionObject> registeredEvents;

public void receiveMessage(EventMessage message) {
    ActionObject action = registeredEvents.get(message.getClass());
    if (action != null) {
        action.performAction();
    }
    else {
        //Code for if the message type is not registered
    }
}

protected void registerEvent(EventMessage message, ActionObject action) {
    Class messageClass = message.getClass();
    registeredEventes.put(messageClass, action);
}

}

public class Ship extends GameEntity {

public Ship() {
    //Do these 3 lines of code for every message you want the class to register for. This example is for a ship getting hit.
    EventMessage getHitMessage = new GetHitMessage();
    ActionObject getHitAction = new GetHitAction();
    super.registerEvent(getHitMessage, getHitAction);
}

}

There are variations of this using Class.forName(fullPathName) and passing in the pathname strings instead of the objects themselves if you want.

Because the logic for performing an action is contained in the superclass, all you have to do to make a subclass is register what events it responds to and create an ActionObject that contains the logic for its response.

I'd be tempted to elevate every message type into a method signature and Interface. How this translates into Scala I'm not totally sure, but this is the Java approach I would take.

Killable, KillListener, Breachable, Breachlistener and so on will surface the logic of your objects and commonalities between them in a way which permits runtime inspection (instanceof) as well as helping with runtime performance. Things which don't process Kill events won't be put in a java.util.List<KillListener> to be notified. You can then avoid the creation of multiple new concrete objects all the time (your EventMessages) as well as lots of switching code.

public interface KillListener{
    void notifyKill(Entity entity);
}

After all, a method in java is otherwise understood as a message - just use raw java syntax.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!