可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
I am currently developing a game in Scala where I have a number of entities (e.g. GunBattery, Squadron, EnemyShip, EnemyFighter) that all inherit from a GameEntity class. Game entities broadcast things of interest to the game world and one another via an Event/Message system. There are are a number of EventMesssages (EntityDied, FireAtPosition, HullBreach).
Currently, each entity has a receive(msg:EventMessage)
as well as more specific receive methods for each message type it responds to (e.g. receive(msg:EntityDiedMessage)
). The general receive(msg:EventMessage)
method is just a switch statement that calls the appropriate receive method based on the type of message.
As the game is in development, the list of entities and messages (and which entities will respond to which messages) is fluid. Ideally if I want a game entity to be able to receive a new message type, I just want to be able to code the logic for the response, not do that and have to update a match statement else where.
One thought I had would be to pull the receive methods out of the Game entity hierarchy and have a series of functions like def receive(e:EnemyShip,m:ExplosionMessage)
and def receive(e:SpaceStation,m:ExplosionMessage)
but this compounds the problem as now I need a match statement to cover both the message and game entity types.
This seems related to the concepts of Double and Multiple dispatch and perhaps the Visitor pattern but I am having some trouble wrapping my head around it. I am not looking for an OOP solution per se, however I would like to avoid reflection if possible.
EDIT
Doing some more research, I think what I am looking for is something like Clojure's defmulti
.
You can do something like:
(defmulti receive :entity :msgType)
(defmethod receive :fighter :explosion [] "fighter handling explosion message")
(defmethod receive :player-ship :hullbreach [] "player ship handling hull breach")
回答1:
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
}
}
回答2:
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.
回答3:
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)
回答4:
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.
回答5:
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.