问题
As I was looking through SO I came across a question about handling multiple message types. My concern is - how do I load such a message in a neat way? I decided to have a separate class with a method which loads one message each time it's invoked. This method should create a new instance of a concrete message type (say AlphaMessage, BetaMessage, GammaMessage, etc.) and return it as a Message.
class MessageLoader
{
public Message Load()
{
// ...
}
}
The code inside the method is something which looks really awful to me and I would very much like to refactor it/get rid of it:
Message msg = Message.Load(...); // load yourself from whatever source
if (msg.Type == MessageType.Alpha) return new AlphaMessage(msg);
if (msg.Type == MessageType.Beta) return new BetaMessage(msg);
// ...
In fact, if the whole design looks just too messy and you guys have a better solution, I'm ready to restructure the whole thing.
If my description is too chaotic, please let me know what it's missing and I shall edit the question. Thank you all.
Edit: What I don't like about this code is that I have to create an instance of a Message (cause it knows how to load itself) and then have to decorate it with a concrete message type (cause decorators know how to interpret msg's Data property). Maybe this will make the question slightly more clear.
回答1:
The next level of abstraction is to make Message discovery and instantiation dynamic. This is often accomplished by associating a string name with each Message or by using the name of the class as an identifier. You can use Reflection to discover available Message types, store them in a Dictionary and provide instantiation by name. This can be further extended to bring in Messages from dynamically loaded 'plugin' assemblies, with appropriate meta-data and interfaces to allow for loosely coupled composition between different Messages and Message Consumers. Once you get to that level, I recommend looking into frameworks like MEF which automate the discovery and instance injection process.
For your simple application, I think your approach is already quite clean. A series of if statements or a switch works just fine and is easy to understand/maintain, as long as you have a relatively small and stable set of cases.
Summarizing the further discussion in the comments:
The main design concern creating uneasiness was the fact that the different specific messages inherited from Message and yet a base Message had to be instantiated before the more specific messages could perform further analysis. This muddied up whether the Message is intended to contain raw information or to act as a base type for interpreted messages. A better design is to separate the RawMessage functionality into its own class, clearly separating concerns and resolving the feeling of 'instantiating twice'.
As for refactoring with DTOs and a mapper class:
I actually prefer your approach for an app-specific message encoding/decoding. If I want to track down why FactoryTakenOverByRobotsMessage contains invalid data, it makes sense to me that the parser method for the message is contained with the decoded data for the message. Where things get more dicey if when you want to support different encodings, as now you start wanting to specify the DTO declaratively (such as with attributes) and allow your different transport layers to decide how to serialize/deserialize. In most cases where I'm using your pattern, however, it's for a very app-specific case, with often somewhat inconsistent message formats and various proprietary encodings that don't map well in any automatic way. I can always still use the declarative encoding in parallel with the proprietary, in-class encoding and do things like serialize my messages to XML for debugging purposes.
回答2:
I agree with CkH in that Factory pattern will solve it. I wrote a silly example as a proof of concept. Not meant to show good class design, just that a simple Factory pattern works. Even if you are using multiple message types and handlers, you should only need to modify this pattern slightly.
class Class12
{
public static void Main()
{
Message m = new Message(1, "Hello world");
IMessageHandler msgHandler = Factory.GetMessageHandler(m.MessageType);
msgHandler.HandleMessage(m);
Message m2 = new Message(2, "Adios world");
IMessageHandler msgHandler2 = Factory.GetMessageHandler(m2.MessageType);
msgHandler2.HandleMessage(m2);
}
}
public class Factory
{
public static IMessageHandler GetMessageHandler(int msgType)
{
IMessageHandler msgHandler = null;
switch(msgType)
{
case 1:
msgHandler = new MessageHandler1();
break;
case 2:
msgHandler = new MessageHandler2();
break;
default:
msgHandler = new MessageHandler1();
break;
}
return msgHandler;
}
}
public class Message
{
public int MessageType { get; set; }
public string AMessage { get; set; }
public Message(int messageType, string message)
{
this.MessageType = messageType;
this.AMessage = message;
}
}
public interface IMessageHandler
{
void HandleMessage(Message m);
}
class MessageHandler1 : IMessageHandler
{
#region IMessageHandler Members
public void HandleMessage(Message m)
{
string message = m.AMessage;
Console.WriteLine(message);
}
#endregion
}
class MessageHandler2 : IMessageHandler
{
#region IMessageHandler Members
public void HandleMessage(Message m)
{
string message = m.AMessage;
Console.WriteLine("Hey there " + message);
}
#endregion
}
回答3:
With C# you'll probably need something like what you've written because C# is a strongly-typed language. Basically, you have to get the concrete classes somewhere in your code.
回答4:
What you have looks fine. It's unambiguous. If your AlphaMessage and BetaMessage objects are children of Message then instead of creating a new object, just return the object casted.
if (msg.Type == MessageType.Alpha) return msg as AlphaMessage;
else if (msg.Type == MessageType.Beta) return msg as BetaMessage;
Consumer code will have to handle the case where the cast fails (as returns null).
来源:https://stackoverflow.com/questions/3056447/design-pattern-for-loading-multiple-message-types