Как я могу улучшить иерархию моих классов событий? - PullRequest
3 голосов
/ 17 ноября 2010

Для интерфейса XMPP для чата Stack Overflow Я анализирую ленту JSON из чата и генерирую объекты Ruby для всех событий чата, таких как отправленные сообщения, отправленные правки, вход и выход пользователей, и т. Д. Я также генерирую события для «команд слеша», отправляемых на сервер XMPP, например «/ help» или «/ auth», чтобы позволить пользователю XMPP проходить аутентификацию с помощью учетной записи чата Stack Overflow.

Я настроил эти классы в иерархии, которая, как мне кажется, имеет логический смысл:

class SOChatEvent # base class
 |
 |--- class SOXMPPEvent # base for all events that are initiated via XMPP
 | |
 | |--- class SOXMPPMessage # messages sent to the XMPP bridge via XMPP
 | | |
 | | |--- class SOXMPPMessageToRoom # messages sent from an XMPP user to an XMPP MUC
 | | |
 | | |--- class SOXMPPUserCommand # class for "slash commands", that is, messages starting
 | | | |                          # with /, used for sending commands to the bridge
 | | | |
 | | | |--- class SOXMPPUserHelpCommand
 | | | |--- class SOXMPPUserLoginCommand
 | | | |--- class SOXMPPUserBroadcastCommand
 |
 |--- class SOChatRoomEvent # base class for all events that originate from an SO chat room
 | |
 | |--- class SOChatMessage # messages sent to an SO chat room via the SO chat system
 | | |
 | | |--- class SOChatMessageEdit # edits made to a prior SOChatMessage
 | |
 | |--- class SOChatUserEvent # events related to SO chat users
 | | |
 | | |--- class SOChatUserJoinRoom #Event for when a So user joins a room
 | | |--- class SOChatUserLeaveRoom #Event for when a So user leaves a room

 (etc)

Вы можете увидеть полную иерархию и источник в Trac или через SVN .

У меня двоякий вопрос: во-первых, как лучше всего описать эти события? В настоящее время я выполняю синтаксический анализ событий JSON с помощью гигантского оператора switch - ну, это рубин, так что это оператор case - и это не гигантский пока , но это будет если я продолжу этот путь:

rooms.each do |room|
  rid = "r"+"#{room.room_id}"
  if !data[rid].nil?
    @last_update = data[rid]['t'] if data[rid]['t']

    if data[rid]["e"]
      data[rid]["e"].each do |e|
        puts "DEBUG: found an event: #{e.inspect}"
        case e["event_type"]
          when 1
            event = SOChatMessage.new(room,e['user_name'])
            event.encoded_body = e['content']
            event.server = @server
            events.push event
          when 2
            event = SOChatMessageEdit.new(room,e['user_name'])
            event.encoded_body = e['content']
            event.server = @server
            events.push event
          when 3
            user = SOChatUser.new(e['user_id'], e['user_name'])
            event = SOChatUserJoinRoom.new(room,user)
            event.server = @server
            events.push event
          when 4
            user = SOChatUser.new(e['user_id'], e['user_name'])
            event = SOChatUserLeaveRoom.new(room,user)
            event.server = @server
            events.push event
        end
      end
    end
  end
end

Но я думаю, должен быть лучший способ справиться с этим! Что-то вроде SOChatEvent.createFromJSON( json_data ) ... Но какой лучший способ структурировать мой код так, чтобы объекты соответствующего подкласса создавались в ответ на данный event_type?

Во-вторых, я еще не использую подклассы муравья SOXMPPUserCommand. Прямо сейчас все команды являются просто экземплярами самого SOXMPPUserCommand, и этот класс имеет единственный метод execute, который переключается на основе регулярного выражения команды. Примерно та же проблема - я знаю, что есть лучший способ, но я просто не уверен, что лучший:

def handle_message(msg)
    puts "Room \"#{@name}\" handling message: #{msg}"
    puts "message: from #{msg.from} type #{msg.type} to #{msg.to}: #{msg.body.inspect}"

    event = nil

    if msg.body =~ /\/.*/
      #puts "DEBUG: Creating a new SOXMPPUserCommand"
      event = SOXMPPUserCommand.new(msg)
    else
      #puts "DEBUG: Creating a new SOXMPPMessageToRoom"
      event = SOXMPPMessageToRoom.new(msg)
    end

    if !event.nil?
      event.user = get_soxmpp_user_by_jid event.from
      handle_event event
    end
  end

и

class SOXMPPUserCommand < SOXMPPMessage
  def execute
    case @body
      when "/help"
        "Available topics are: help auth /fkey /cookie\n\nFor information on a topic, send: /help <topic>"
      when "/help auth"
        "To use this system, you must send your StackOverflow chat cookie and fkey to the system. To do this, use the /fkey and /cookie commands"
      when "/help /fkey"
        "Usage: /fkey <fkey>. Displays or sets your fkey, used for authentication. Send '/fkey' alone to display your current fkey, send '/fkey <something>' to set your fkey to <something>. You can obtain your fkey via the URL: javascript:alert(fkey().fkey)"
      when "/help /cookie"
        "Usage: /cookie <cookie>. Displays or sets your cookie, used for authentication. Send '/cookie' alone to display your current fkey, send '/cookie <something>' to set your cookie to <something>"
      when /\/fkey( .*)?/
        if $1.nil?
          "Your fkey is \"#{@user.fkey}\""
        else
          @user.fkey = $1.strip
          if @user.authenticated?
            "fkey set to \"#{@user.fkey}\". You are now logged in and can send messages to the chat"
          else
            "fkey set to \"#{@user.fkey}\". You must also send your cookie with /cookie before you can chat"
          end
        end
      when /\/cookie( .*)?/
        if $1.nil?
          "Your cookie is: \"#{@user.cookie}\""
        else
          if $1 == " chocolate chip"
            "You get a chocolate chip cookie!"
          else
            @user.cookie = $1.strip
            if @user.authenticated?
              "cookie set to \"#{@user.cookie}\". You are now logged in and can send messages to the chat"
            else
              "cookie set to \"#{@user.cookie}\". You must also send your fkey with /fkey before you can chat"
            end
          end
        end
      else
        "Unknown Command \"#{@body}\""
    end
  end
end

Я знаю, что есть лучший способ сделать это, но не уверен, что именно. Должна ли ответственность за создание подклассов SOXMPPUserCommand ложиться на SOXMPPUserCommand сама? Должны ли все подклассы регистрироваться у родителя? Нужен ли мне новый класс?

Каков наилучший способ создания экземпляров объектов подклассов в такой иерархической структуре?

1 Ответ

2 голосов
/ 19 ноября 2010

Решение вашего первого вопроса.Вот некоторые идеи, которые вы могли бы рассмотреть

Во-первых, структурируйте свои подклассы так, чтобы они все использовали одинаковые параметры инициации.Кроме того, вы могли бы поместить туда и другой исходный код (например, свои encoded_body и серверные средства доступа. Вот скелет того, что я имею в виду:

# SOChat Class skeleton structure
class SOChatSubClass  #< inherit from whatever parent class is appropriate
  attr_accessor :encoded_body, :server, :from, :to, :body

  def initialize(event, room, server)
    @encoded_body = event['content']
    @server = server
    SOChatEvent.events.push event

    #class specific code 
    xmpp_message = event['message']
    @from = xmpp_message.from
    @to = xmpp_message.to
    @body = xmpp_message.body
    #use super to call parent class initialization methods and to DRY up your code
  end
end 

Обратите внимание, что в моем примере вы все равно будете иметьдублированный код в подклассах. В идеале вы должны удалить дубликаты, поместив их в соответствующий родительский класс.

Если у вас возникли проблемы при создании общего списка параметров инициации, то вместо передачи спискааргументов (событие, комната, сервер), измените классы так, чтобы они принимали список аргументов как хэш {: событие => событие,: комната => комната,: сервер => сервер и т. д.}.

Независимо от того,если у вас есть общая структура параметров для инициализации классов, вы можете инициализировать их более динамично, что устраняет необходимость в операторе case.

class SOChatEvent
     class << self; attr_accessor :events; end
     @events = []

      @@event_parser = {
                                0 => SOChatSubClass, #hypothetical example for testing
                                1 => SOChatMessage,
                                2 => SOChatMessageEdit,
                                #etc
                              }
    def self.create_from_evt( json_event_data, room=nil, server=nil)
      event_type = json_event_data["event_type"]
      event_class =  @@event_parser[event_type]
      #this creates the class defined by class returned in the @@event_parser hash
      event_obj = event_class.new(json_event_data, room, server)
    end

    #rest of class
end

@@event_parser содержит отображение между типом события икласс для реализации этого типа события. Вы просто присваиваете соответствующий класс переменной и обрабатываете его как фактический класс.

Код, подобный следующему, создаст объект соответствующего класса:

event_obj = SOChatEvent.create_from_evt( json_event_data,
                                        "some room", 
                                        "some server")

Примечание: есть дополнительные оптимизации, которые я мог бы сделать, чтобы то, что я предоставил, было еще чище и более кратким, но, надеюсь, это поможетВы преодолеваете горстку оператора case.

Редактировать: я забыл упомянуть переменную экземпляра класса SOChatEvent.events, созданную с помощью этого: class << self; attr_accessor :events; end @events = []

Вы помещали события в стек событий, но мне было непонятно, где вы хотели, чтобы этот стек существовал, и был ли это глобальный список событий или специфический для определенного класса.Тот, который я сделал, является глобальным, поэтому смело изменяйте его, если хотите, чтобы стек событий ограничивался определенными классами или экземплярами.

...