最佳表/枚举驱动方法调用系统

考虑我正在与将以某种格式发送消息(数据库表,消息队列,Web服务)的外部系统连接。 在“消息头”中有“MessageType”,它是1到20之间的数字.MessageType定义了如何处理消息的其余部分。 有新的,修改的,删除的,取消的……

我的第一个倾向是设置枚举并定义所有类型。 然后将数字解析为枚举类型。 将它作为枚举,我将设置典型的开关案例系统,并为每种消息类型调用特定方法。

一个重要的问题是维护。
开关/箱体系统笨重而且很笨重,但它非常简单。
各种其他表/配置系统可能很难让其他人知道并添加新消息或调整现有消息。

对于12个左右的MessageTypes,交换机/案例系统似乎很合理。 切换到表驱动系统的合理截止点是什么?

哪种系统最适合处理这些类型的问题?

我在这里为C#和Java设置了一个标签,因为它绝对是一个常见的问题。 还有许多其他语言具有相同的问题。

在Java中,你可以使它成为枚举并为不同的值赋予行为(尽管有100个值,我希望每种类型的行为都是短暂的,调用“正确的”类)。

在C#中,您可以拥有从值到某个适当委托类型的映射 – 然后在静态构造映射时,您可以根据需要使用lambda表达式或方法组转换。

话虽如此,设置地图将像switch语句一样丑陋。 如果每个switch语句只是一个方法调用,您可能想尝试这种格式:

switch (messageType) { case 0: HandleLogin(message); break; case 50: SaveCurrentDocument(message); break; case 100: HandleLogout(message); break; } 

(等等)。 我知道这违反了正常惯例,但对于像这样奇怪的特殊情况,它可能非常简洁。 如果你只需要一个地方的数字,那么引入常数几乎没有意义 – 基本上包含有效数字的行常量定义!

如何使用Dictionary按消息类型存储这些方法? 在类的初始化期间,注册此字典中的所有方法。 然后调用适当的方法。 以下是伪代码:

  delegate void ProcessMessageDelegate(Message message) public class MyMessageProcessor { Dictionary methods; public void Register( int messageType, ProcessMessageDelegate processMessage) { methods[messageType] = processMessage; } public void ProcessMessage(int messageType, Message message) { if(methods.ContainsKey(messageType)) { methods[messageType](message); } } } 

注册方法:

  myProcessor.Register(0, ProcessMessageOfType0); myProcessor.Register(1, ProcessMessageOfType1); myProcessor.Register(2, ProcessMessageOfType2); ... 

编辑 :我意识到乔恩已经建议有一张地图,现在让我的答案多余。 但我不明白为什么静态构造的地图比切换案例更丑陋?

有这样的处理程序界面:

 interface MessageHandler { void processMessage(Message msg) throws Exception; int[] queryInterestingMessageIds(); int queryPriority(int messageId); // this one is optional } 

找到,实例化并注册您的处理程序。 您可能希望使用一些基于reflection的机制,如ServiceLoader ,Spring(显式配置或类路径扫描和可能自动assembly)或普通属性文件

注册应该将每个处理程序传递给WhateverManager类,该类将在内部保存处理程序集合的地图(或由消息ID索引的arry)。 如果您希望有多个处理程序,则可以使用queryPriority(int)方法来解决处理顺序(否则您可以将其视为错误并在配置时抛出exception)。 不使用静态地图进行注册是一种很好的做法。

如果您决定为消息支持多个处理程序,则可能需要菊花链它们。 在这种情况下,一种方法是更改​​签名,如下所示:

  Message processMessage(Message msg, Message original) throws Exception; 

这就是我在C#中做到这一点的方式。

我认为这种方法实际上并没有那么难看,随着消息类型数量的增加,它变得不那么难看:要实现一个新的消息类型,你只需要为你的Enum添加一个值并用新的消息处理程序类来标记一个属性。

在某些情况下,能够在运行时从程序集加载消息处理程序是一个非常强大的function; 根据安装的消息处理程序程序集,您可以拥有一个行为不同的可执行文件。

首先为消息处理程序创建一个接口(我们将其称为IMessageHandler ),并为消息类型创建一个Enum(我们将其称为MessageType )。

接下来,创建一个名为MessageHandlerAttribute的类:

 public class MessageHandlerAttribute : System.Attribute { public MessageType MessageType { get; set; } } 

现在将每个消息处理程序实现为一个单独的类,并使用其消息类型属性标记每个类。 如果消息处理程序可以处理多种类型的消息,则可以在其上放置多个属性:

 [MessageHandler(MessageType=MessageType.Login)] public class LoginMessageHandler : IMessageHandler { ... } 

这些消息处理程序都具有无参数构造函数,这一点很重要。 我想不出一个很好的理由,你想要一个消息处理程序的构造函数来获取参数,但如果有的话,下面的代码无法处理它。

将所有消息处理程序构建到同一个程序集中,并确保您有一种方法可以在运行时知道它的路径。 (这是这种方法失败的重点。)

现在我们可以使用Reflection在运行时构建消息处理程序的映射:

 using System.Reflection; ... Assembly mhAssembly = Assembly.LoadFrom(mhAssemblyPath); Dictionary mhMap = new Dictionary(); foreach (Type t in mhAssembly.GetExportedTypes()) { if (t.GetInterface("IMessageHandler") != null) { MessageHandlerAttribute list = (MessageHandlerAttribute[])t.GetCustomAttributes( typeof(MessageHandlerAttribute), false); foreach (MessageHandlerAttribute att in list) { MessageType mt = att.MessageType; Debug.Assert(!mhMap.ContainsKey(mt)); IMessageHandler mh = mhAssembly.CreateInstance( t.FullName, true, BindingFlags.CreateInstance, null, new object[] { }, null, null); mhMap.Add(mt, mh); } } // depending on your application, you might want to check mhMap now to make // sure that every MessageType value is in it. } return mhMap; 

现在,当您收到消息时,您可以像这样处理它:

 Debug.Assert(MhMap.ContainsKey(Message.MessageType)); IMessageHandler mh = MhMap[Message.MessageType]; mh.HandleMessage(Message); 

这段代码全部基于我现在在生产系统中的代码; 我稍微改了一下(以便消息处理程序实现一个接口,而不是从一个抽象类派生,并且它处理多个消息处理程序属性),这可能会引入错误。