Как вызвать правильный метод в Scala / Java на основе типов двух объектов без использования оператора switch? - PullRequest
9 голосов
/ 20 декабря 2011

В настоящее время я разрабатываю игру в Scala, в которой у меня есть несколько сущностей (например, GunBattery, Squadron, EnemyShip, EnemyFighter), которые все наследуются от класса GameEntity. Игровые объекты транслируют вещи, представляющие интерес для игрового мира и друг друга, через систему событий / сообщений. Существует несколько сообщений EventMesssages (EntityDied, FireAtPosition, HullBreach).

В настоящее время каждый объект имеет receive(msg:EventMessage), а также более конкретные методы получения для каждого типа сообщений, на которые он отвечает (например, receive(msg:EntityDiedMessage)). Общий метод receive(msg:EventMessage) - это просто оператор switch, который вызывает соответствующий метод приема в зависимости от типа сообщения.

Поскольку игра находится в разработке, список сущностей и сообщений (и какие сущности будут отвечать на какие сообщения) является плавным. В идеале, если я хочу, чтобы игровая сущность могла получать новый тип сообщения, я просто хочу иметь возможность кодировать логику ответа, а не делать то, что и должны обновлять оператор сопоставления где-либо еще.

Одна мысль, которая у меня была, состоит в том, чтобы вытащить методы получения из иерархии сущностей Game и иметь ряд функций, таких как def receive(e:EnemyShip,m:ExplosionMessage) и def receive(e:SpaceStation,m:ExplosionMessage), но это усугубляет проблему, так как теперь мне нужно выражение match, чтобы охватить оба сообщение и типы игровых сущностей.

Это, похоже, связано с понятиями Double и Multiple dispatch и, возможно, с шаблоном Visitor, но у меня возникли некоторые проблемы, когда я оборачиваюсь вокруг него. Я не ищу решение ООП как таковое, однако я хотел бы избежать отражения, если это возможно.

EDIT

Проводя еще какие-то исследования, я думаю, что я ищу что-то вроде Clojure defmulti.

Вы можете сделать что-то вроде:

(defmulti receive :entity :msgType)

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

Ответы [ 5 ]

9 голосов
/ 22 декабря 2011

Вы можете легко реализовать множественную диспетчеризацию в 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
  }
}
3 голосов
/ 21 декабря 2011

Цепочка ответственности

Стандартным механизмом для этого (не специфичным для 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 в цикл реакции.Это может вызвать другой метод в вашем классе актера, который предпримет соответствующее действие.Конечно, выгода от этого заключается в том, что вы используете модель параллелизма, предоставляемую парадигмой актера.В итоге получается цикл, который выглядит следующим образом:

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

Возможно, вы захотите взглянуть на akka , который предлагает более производительную систему актеров с более настраиваемыми параметрами.поведение и другие примитивы параллелизма (STM, агенты, транзакции и т. д.)

3 голосов
/ 20 декабря 2011

Если вы действительно обеспокоены усилиями, которые требуются для создания / поддержки оператора switch, вы можете использовать метапрограммирование для генерации оператора switch, обнаруживая все типы EventMessage в вашей программе.Это не идеально, но метапрограммирование - это, как правило, один из самых чистых способов введения новых ограничений в ваш код;в этом случае это будет требованием, что если тип события существует, для него есть диспетчер и обработчик по умолчанию (игнорировать?), который можно переопределить.

Если вы не хотите идти по этому пути, вы можете сделать EventMessage классом case, который должен позволить компилятору пожаловаться, если вы забудете обработать новый тип сообщения в вашем операторе switch.Я написал игровой сервер, которым пользовались ~ 1,5 миллиона игроков, и использовал такую ​​статическую типизацию, чтобы гарантировать, что моя рассылка была всеобъемлющей, и это никогда не вызывало реальной производственной ошибки.

1 голос
/ 21 декабря 2011

Я хотел бы поднять каждый тип сообщения в сигнатуру метода и интерфейс. Как это переводится на Scala, я не совсем уверен, но я бы выбрал такой подход на Java.

Killable, KillListener, Breachable, Breachlistener и т. Д. Будут отображать логику ваших объектов и сходства между ними таким образом, который позволяет проводить проверку во время выполнения (instanceof), а также помогает повысить производительность во время выполнения. Вещи, которые не обрабатывают события Kill, не будут помещены в java.util.List<KillListener> для уведомления. После этого вы можете все время избегать создания нескольких новых конкретных объектов (ваши EventMessages), а также большого количества кода переключения.

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

В конце концов, метод в java иначе понимается как сообщение - просто используйте необработанный синтаксис java.

1 голос
/ 21 декабря 2011

Независимо от того, что вы должны сделать некоторые обновления;приложение не просто волшебным образом знает, какое действие отклика сделать на основе сообщения о событии.

Случаи хороши и хороши, но по мере того, как список сообщений, на которые реагирует ваш объект, становится длиннее, так же, как и время его ответа.,Вот способ ответить на сообщения, которые будут отвечать с одинаковой скоростью, независимо от того, сколько вы зарегистрировались с ним.В примере нужно использовать объект 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, который содержит логику для его ответа.

...