如何改进事件类的层次结构?

发布于 2024-10-02 14:21:03 字数 6006 浏览 1 评论 0 原文

对于 Stack Overflow 聊天的 XMPP 界面 我我正在解析聊天中的 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

我的问题有两个:首先,实例化这些事件的最佳方法是什么?我目前正在做的是使用一个巨大的 switch 语句解析 JSON 事件 —— 嗯,它是 ruby​​,所以它是一个 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 的 ant 子类。现在,所有命令都只是 SOXMPPUserCommand 本身的实例,并且该类具有单个执行方法,该方法根据命令的正则表达式进行切换。几乎同样的问题 - 我知道有更好的方法,我只是不确定最好的方法是什么:

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 本身承担?所有子类都应该向父类注册吗?我需要新课程吗?

在这种层次结构中实例化子类对象的最佳方法是什么?

For the XMPP interface for the Stack Overflow chat I am parsing the JSON feed from chat and generating Ruby objects for every chat events, such as messages sent, edits sent, users logging in or out, etc. I also generate events for "slash-commands" sent to the XMPP server, like "/help" or "/auth" in order to allow the XMPP user to authenticate with their Stack Overflow chat account.

I have set up these classes in a hierarchy I feel makes good logical sense:

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)

You can see the full hierarchy and source in Trac or via SVN.

My question is twofold: First, what is the best way to instantiate these events? What I'm currently doing is parsing the JSON events using a giant switch statement --well, it's ruby so it's a case statement -- and, it's not giant yet, but it will be if I continue this way:

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

But I imagine there has to be a better way to handle that! Something like SOChatEvent.createFromJSON( json_data )... But, what's the best way to structure my code so that objects of the proper subclass are created in response to a given event_type?

Second, I'm not actually using ant subclasses of SOXMPPUserCommand yet. Right now all commands are just instances of SOXMPPUserCommand itself, and that class has a single execute method which switches based upon regex of the command. Much the same problem -- I know there's a better way, I just am not sure what the best way is:

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

and:

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

I know there's a better way to do this, just not sure what specifically it is. Should the responsibility of creating subclasses of SOXMPPUserCommand fall on SOXMPPUserCommand itself? Should all subclasses register with the parent? Do I need a new class?

What's the best way to instantiate objects of subclasses in such a hierarchal structure?

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

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

发布评论

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

评论(1

巾帼英雄 2024-10-09 14:21:03

解决你的第一个问题。以下是您可能需要考虑的一些想法:

首先,构建子类,以便它们都使用相同的启动参数。另外,您也可以将一些其他启动代码放在那里(例如您的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 

请注意,在我的示例中,子类中仍然会有重复的代码。理想情况下 如果您在创建通用的启动参数列表时遇到问题,则可以通过将其放入适当的父类中来删除重复项,

而不是传入参数列表(事件、房间、服务器),而是将类更改为接受。参数列表作为哈希 {:event => event, :room => room, :server => server, 等等}

。更加动态,消除了对 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_obj = SOChatEvent.create_from_evt( json_event_data,
                                        "some room", 
                                        "some server")

注意:可以对我提供的内容进行进一步的优化,使其更加清晰和简洁,但希望这可以帮助您克服困难。案件陈述。

编辑:我忘了提及用此创建的类实例变量 SOChatEvent.events
类<<自己; attr_accessor:事件;结尾
@events = []

您将事件推送到事件堆栈,但我不清楚您希望该堆栈存在于何处以及它是全局事件列表还是特定于特定类。我所做的是全局性的,因此如果您希望事件堆栈仅限于某些类或实例,请随意更改它。

Addressing your first question. Here's some ideas you might like to consider

First, structure you sub-classes so they all use the same initiation parameters. Also, you could put some of the other initiating code there as well (such as your encoded_body and server accessors. Here's a skeleton of what I mean:

# 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 

Note that in my example you'll still have duplicated code in the sub-classes. Ideally you'd pull out the duplication by putting it in the appropriate parent class.

If you have problems creating a common list of initiation parameters, then rather than pass in a list of arguments (event, room, server), change the classes to accept an argument list as a hash {:event => event, :room => room, :server => server, etc}.

Regardless, once you have a common parameter structure for initializing the classes, you can initialize them a bit more dynamically, eliminating the need for the case statement.

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 contains the mapping between event type and the class to implement that event type. You just assign the appropriate class to a variable and treat it just like the actual class.

Code like the following would create an object of the appropriate class:

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

Note: There are further optimizations that could be done to what I provided to be even cleaner and more concise, but hopefully this helps you get over the hump of the case statement.

Edit: I forgot to mention the Class instance variable SOChatEvent.events created with this:
class << self; attr_accessor :events; end
@events = []

You were pushing events to an event stack, but I wasn't clear where you wanted that stack to exist and whether it was a global events list, or specific to a particular class. The one I did is global, so feel free to change it if you wanted the event stack constrained to certain classes or instances.

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