如何在 Scala/Java 中根据两个对象的类型调用正确的方法而不使用 switch 语句?

发布于 2024-12-22 04:42:47 字数 1267 浏览 1 评论 0 原文

我目前正在 Scala 中开发一款游戏,其中有许多实体(例如 GunBattery、Squadron、EnemyShip、EnemyFighter),它们都继承自 GameEntity 类。游戏实体通过事件/消息系统向游戏世界和彼此广播感兴趣的事物。有许多 EventMesssages(EntityDied、FireAtPosition、HullBreach)。

目前,每个实体都有一个 receive(msg:EventMessage) 以及针对其响应的每种消息类型的更具体的接收方法(例如 receive(msg:EntityDiedMessage) )。一般的receive(msg:EventMessage)方法只是一个switch语句,它根据消息的类型调用适当的接收方法。

随着游戏的开发,实体和消息的列表(以及哪些实体将响应哪些消息)是可变的。理想情况下,如果我希望游戏实体能够接收新的消息类型,我只想能够编写响应的逻辑,而不是这样做并且必须更新其他地方的匹配语句。

我的一个想法是将接收方法从游戏实体层次结构中拉出来,并具有一系列函数,例如 def receive(e:EnemyShip,m:ExplosionMessage) 和 def receive(e :SpaceStation,m:ExplosionMessage) 但这使问题变得更加复杂,因为现在我需要一个匹配语句来涵盖消息和游戏实体类型。

这似乎与 Double多重 调度,也许还有访客模式,但我在理解它时遇到了一些麻烦。我本身并不是在寻找 OOP 解决方案,但我希望尽可能避免反射。

编辑

做了更多研究,我认为我正在寻找类似于 Clojure 的 defmulti 的东西。

你可以这样做:

(defmulti receive :entity :msgType)

(defmethod receive :fighter :explosion [] "fighter handling explosion message")
(defmethod receive :player-ship :hullbreach []  "player ship handling hull breach")

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")

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

空宴 2024-12-29 04:42:47

尽管 Scala 没有一流的支持,但您可以轻松地在 Scala 中实现多重分派。通过下面的简单实现,您可以按如下方式对示例进行编码:

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" }

您可以像任何其他函数一样使用多方法:

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

请注意,每个多方法实现(即 defImpl 调用)必须包含在顶级定义(类/对象/特征体),您需要确保在使用该方法之前发生相关的 defImpl 调用。此实现还有许多其他限制和缺点,但我将把它们作为练习留给读者。

执行:

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
  }
}

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
  }
}
宫墨修音 2024-12-29 04:42:47

如果您确实担心创建/维护 switch 语句所需的工作量,则可以使用元编程通过发现程序中的所有 EventMessage 类型来生成 switch 语句。这并不理想,但元编程通常是对代码引入新约束的最干净的方法之一;在这种情况下,要求如果事件类型存在,则有一个调度程序,以及一个可以覆盖的默认(忽略?)处理程序。

如果您不想走这条路,您可以将 EventMessage 设为一个案例类,这样,如果您忘记在 switch 语句中处理新的消息类型,编译器应该可以发出抱怨。我编写了一个约有 150 万玩家使用的游戏服务器,并使用这种静态类型来确保我的调度是全面的,并且它从未导致实际的生产错误。

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.

薔薇婲 2024-12-29 04:42:47

责任链

一个标准机制(不是 scala 特定的)是处理程序的。例如:

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

那么您的实体只需要管理处理程序列表:

abstract class AbstractEntity {

    def handlers: List[Handler]

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

那么您的实体可以内联声明处理程序,如下所示:

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
       }
     }
   )

当然,这里的缺点是您失去了可读性,并且您仍然必须记住样板文件,它是每个处理程序的无操作包罗万象的情况。

演员

我个人会坚持重复。您现在所拥有的看起来很像将每个实体视为一个Actor。例如:

class Tank extends Entity with Actor {

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

至少在这里你养成了在反应循环中添加 case 语句的习惯。这可以调用 Actor 类中的另一个方法来执行适当的操作。当然,这样做的好处是您可以利用参与者范例提供的并发模型。你最终会得到一个如下所示的循环:

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

你可能想看看 akka,它提供了一个性能更高的参与者系统,具有更多可配置的行为和更多的并发原语(STM、代理、交易者等)

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)

月隐月明月朦胧 2024-12-29 04:42:47

无论如何,你都得做一些更新;应用程序不会神奇地根据事件消息知道要执行哪个响应操作。

情况很好,但随着对象响应的消息列表变长,其响应时间也会变长。这是一种回复消息的方法,无论您注册了多少条消息,都会以相同的速度回复。该示例确实需要使用 Class 对象,但没有使用其他反射。

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);
}

如果需要

可以使用 Class.forName(fullPathName) 并传入路径名字符串而不是对象本身来实现此目的。

由于执行操作的逻辑包含在超类中,因此创建子类所需要做的就是注册它响应的事件并创建一个包含其响应逻辑的 ActionObject。

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.

ぃ弥猫深巷。 2024-12-29 04:42:47

我很想将每种消息类型提升为方法签名和接口。我不太确定如何将其转化为 Scala,但这是我会采用的 Java 方法。

Killable、KillListener、Breachable、Breachlistener 等将以允许运行时检查(instanceof)并帮助提高运行时性能的方式展现对象的逻辑以及它们之间的共性。不处理 Kill 事件的事物不会被放入 java.util.List 中以得到通知。然后,您可以一直避免创建多个新的具体对象(您的 EventMessages)以及大量切换代码。

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

毕竟,java 中的方法被理解为消息 - 只需使用原始 java 语法。

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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文