wake-up-neo.com

Entwurfsmuster für die Verarbeitung mehrerer Nachrichtentypen

Ich habe die GOF hier auf meinem Schreibtisch sitzen und ich weiß, dass es ein Designmuster geben muss, das mein Problem löst, aber ich kann es nicht verstehen.

Der Einfachheit halber habe ich den Namen einiger der von mir verwendeten Schnittstellen geändert.

Hier also das Problem: Auf einer Seite des Drahtes habe ich mehrere Server, die verschiedene Arten von Nachrichten versenden. Auf der anderen Seite der Leitung habe ich einen Client, der in der Lage sein muss, alle verschiedenen Arten von Nachrichten zu verarbeiten. 

Alle Nachrichten implementieren dieselbe gemeinsame Schnittstelle wie iMessage. Mein Problem ist, wenn der Client eine neue iMessage erhält, woher weiß er, welche Art von iMessage er erhalten hat?

Ich nahm an, ich könnte so etwas wie das Folgende tun, aber das fühlt sich einfach schrecklich an.

TradeMessage tMessage = newMessage as TradeMessage;
if (tMessage != null)
{
    ProcessTradeMessage(tMessage);
}

OrderMessage oMessage = newMessage as OrderMessage;
if (oMessage != null)
{
    ProcessOrderMessage(oMessage);
}

Der zweite Gedanke besteht darin, eine Eigenschaft mit dem Namen MessageTypeID zu iMessage hinzuzufügen, aber dazu müsste ich etwas wie das Folgende schreiben, das sich auch schrecklich anfühlt.

TradeMessage tMessage = new TradeMessage();
if (newMessage.MessageTypeID == tMessage.MessageTypeID)
{
    tMessage = newMessage as TradeMessage;
    ProcessTradeMessage(tMessage); 
}

OrderMessage oMessage = new OrderMessage();
if (newMessage.MessageTypeID == oMessage.MessageTypeID)
{
    oMessage = newMessage as OrderMessage;
    ProcessOrderMessage(oMessage);
}

Ich weiß, dass dieses allgemeine Problem bereits eine Million Mal angegangen wurde. Es muss also ein besserer Weg gefunden werden, um das Problem zu lösen, dass eine Methode eine Schnittstelle als Parameter verwendet, die jedoch eine andere Flusssteuerung benötigt, je nachdem, welche Klasse diese Schnittstelle implementiert hat. 

29

Sie können für jeden Nachrichtentyp separate Meldungshandler erstellen und die Nachricht an jeden verfügbaren Handler übergeben, bis Sie einen finden, der sie verarbeiten kann. Ähnlich wie bei der Verantwortungskette:

public interface IMessageHandler {
    bool HandleMessage( iMessage msg );
}

public class OrderMessageHandler {
    bool HandleMessage( iMessage msg ) {
       if ( !(msg is OrderMessage)) return false;

       // Handle the message and return true to indicate it was handled
       return true; 
    }
}

public class SomeOtherMessageHandler {
    bool HandleMessage( iMessage msg ) {
       if ( !(msg is SomeOtherMessage) ) return false;

       // Handle the message and return true to indicate it was handled
       return true;
    }
}

... etc ...

public class MessageProcessor {
    private List<IMessageHandler> handlers;

    public MessageProcessor() {
       handlers = new List<IMessageHandler>();
       handlers.add(new SomeOtherMessageHandler());
       handlers.add(new OrderMessageHandler());
    }

    public void ProcessMessage( iMessage msg ) {
       bool messageWasHandled
       foreach( IMessageHandler handler in handlers ) {
           if ( handler.HandleMessage(msg) ) {
               messageWasHandled = true;
               break;
           }
       }

       if ( !messageWasHandled ) {
          // Do some default processing, throw error, whatever.
       }
    }
}

Sie können dies auch als Map implementieren, mit dem Nachrichtenklassennamen oder der Nachrichtentyp-ID als Schlüssel und der entsprechenden Handler-Instanz als Wert.

Andere haben vorgeschlagen, das Nachrichtenobjekt selbst "handhaben" zu lassen, aber das fühlt sich für mich einfach nicht richtig an. Es scheint, als wäre es am besten, die Handhabung der Nachricht von der Nachricht selbst zu trennen.

Einige andere Dinge, die ich daran mag:

  1. Sie können die Message-Handler über spring oder what-have-you eingeben, anstatt sie im Konstruktor zu erstellen, was dies sehr überprüfbar macht.

  2. Sie können ein themenähnliches Verhalten einführen, bei dem Sie mehrere Handler für eine einzelne Nachricht haben, indem Sie einfach den "break" aus der ProcessMessage-Schleife entfernen.

  3. Durch das Trennen der Nachricht vom Handler können Sie verschiedene Handler für dieselbe Nachricht an verschiedenen Zielen verwenden (z. B. mehrere MessageProcessor-Klassen, die dieselben Nachrichten unterschiedlich behandeln).

20
Eric Petroelje

Hierfür gibt es einige Lösungen, erstens beste Lösung, letzte ist am wenigsten beste. Alle Beispiele sind Pseudocode:

1. und beste Lösung

Vincent Ramdhanie führte das eigentlich richtige Muster ein, um dieses Problem zu lösen, das als Strategiemuster bezeichnet wird. 

Dieses Muster erstellt einen separaten "Prozessor", um die Meldungen in diesem Fall entsprechend zu verarbeiten.

Aber ich bin mir ziemlich sicher, dass GOF eine gute Erklärung in Ihrem Buch gibt :)

2.

Wenn die Nachricht kommentiert wird, kann sie möglicherweise nicht selbst verarbeitet werden. Es ist dennoch nützlich, eine Schnittstelle für die Nachricht oder eine Basisklasse zu erstellen, sodass Sie eine allgemeine Verarbeitungsfunktion für eine Nachricht erstellen und diese für spezifischere überladen können. 

Überladen ist auf jeden Fall besser als für jede Art von Nachricht eine andere Methode zu erstellen ...

public class Message {}
public class TradeMessage extends Message {}

public class MessageProcessor {
    public function process(Message msg) {
        //logic
    }

    public function process(TradeMessage msg) {
        //logic
    }
}

3.

Wenn Ihre Nachricht selbst verarbeitet werden könnte, könnten Sie eine Schnittstelle schreiben, da Ihre Prozessmethode davon abhängt, welche Nachricht Sie erhalten haben. Es scheint einfacher, sie in die Nachrichtenklasse einzufügen.

public interface iMessage
{
    public function process(){}
}

sie implementieren dies dann in allen Ihren Nachrichtenklassen und verarbeiten diese:

list = List<iMessage>();
foreach (iMessage message in list) {
    message.process();
}

in Ihrer Liste können Sie jede Klasse speichern, die diese Schnittstelle implementiert ...

15
NDM

Nach meiner Erfahrung mit der Nachrichtenverarbeitung ist es in der Regel der Fall, dass verschiedene Benutzer von Nachrichten die Handhabung verschiedener Nachrichtentypen erfordern. Ich habe das Double Dispatch - Muster gefunden, um dieses Problem zu lösen. Die Grundidee besteht darin, eine Gruppe von Handlern zu registrieren, die die empfangenen Nachrichten an den Handler senden, um sie basierend auf dem jeweiligen Typ (mit Überladen von Funktionen) zu verarbeiten. Die Verbraucher registrieren sich nur für die spezifischen Typen, die sie erhalten möchten. Unten ist ein Klassendiagramm.

Double Dispatch UML Class Diagram

Der Code sieht so aus:

IHandler

public interface IHandler
{
}

IMessageHandler

public interface IMessageHandler<MessageType> : IHandler
{
   void ProcessMessage(MessageType message);
}

iMessage

public interface iMessage
{
   void Dispatch(IHandler handler);
}

MessageBase

public class MessageBase<MessageType> : iMessage
   where MessageType : class, iMessage
{
   public void Dispatch(IHandler handler)
   {
      MessageType msg_as_msg_type = this as MessageType;
      if (msg_as_msg_type != null)
      {
         DynamicDispatch(handler, msg_as_msg_type);
      }
   }

   protected void DynamicDispatch(IHandler handler, MessageType self)
   {
      IMessageHandler<MessageType> handlerTarget = 
         handler as IMessageHandler<MessageType>;
      if (handlerTarget != null)
      {
         handlerTarget.ProcessMessage(self);
      }
   }
}

DerivedMessageHandlerOne

// Consumer of DerivedMessageOne and DerivedMessageTwo 
// (some task or process that wants to receive messages)
public class DerivedMessageHandlerOne : 
   IMessageHandler<DerivedMessageOne>, 
   IMessageHandler<DerivedMessageTwo>
   // Just add handlers here to process incoming messages
{     
   public DerivedMessageHandlerOne() { }

   #region IMessageHandler<MessaegType> Members

   // ************ handle both messages *************** //
   public void ProcessMessage(DerivedMessageOne message)
   {
     // Received Message one, do something with it
   }

   public void ProcessMessage(DerivedMessageTwo message)
   {
      // Received Message two, do something with it   
   }

   #endregion
}

DerivedMessageOne

public class DerivedMessageOne : MessageBase<DerivedMessageOne>
{
   public int MessageOneField;

   public DerivedMessageOne() { }
}

Dann haben Sie nur einen Container, der die Handler verwaltet, und Sie sind fertig. Eine einfache for-Schleife durch die Liste der Handler, wenn eine Nachricht empfangen wurde, und die Handler empfangen die Nachrichten dort, wo sie sie möchten

// Receive some message and dispatch it to listeners
iMessage message_received = ...
foreach(IHandler handler in mListOfRegisteredHandlers)
{
   message_received.Dispatch(handler);
}

Dieses Design kam aus einer Frage, die ich vor einiger Zeit nach Polymorphic Event Handling

8
SwDevMan81

Eine Möglichkeit besteht darin, dass die Nachrichten mit ihren eigenen Handlern kommen. Erstellen Sie also eine Schnittstelle namens IMessageProcessor, die eine Methode processMessage (iMessage) angibt. Definieren Sie als Nächstes eine konkrete Klasse, die IMessageProcessor für jeden Nachrichtentyp implementiert.

Jede iMessage-Klasse definiert dann einen eigenen Prozessor.

Wenn Sie ein Nachrichtenobjekt erhalten, machen Sie Folgendes:

message.processor.processMessage();
5

Für mein kleines Messaging-Framework in der Silverlight-App verwende ich das Mediator-Muster. Hierbei handelt es sich um eine Art Messaging-Bus/Broker, für den Objekte bestimmte Arten von Nachrichten abonnieren. Dann entscheidet dieses Mediator-Objekt (Broker/Bus), wer welche Nachrichten erhalten soll.
Etwas wie: 

SubscribeFor<ChatMessage>().If(x=>x.SomeProp==true).Deliver(MyMethod);

Beispielmethoden, die aufgerufen werden:

void MyMethod(ChatMessage msg) , or
void MyMethod(BaseBessage msg)

oder Veröffentlichung (Ausstrahlung) von Nachrichten:

Publish(new ChatMessage());

BaseMessage ist eine abstrakte Klasse, die alle meine Nachrichten erbt und nur auf den Absender und eine bestimmte Guid verweist. 

Ich habe den Ausgangspunkt für das Erstellen meines Messaging-Frameworks aus MVVM Light Toolkit genommen. Sie können sich den Quellcode ansehen, es ist nicht kompliziert!

Wenn Sie wünschen, kann ich irgendwo C # -Code dafür eingeben?

3
Hrvoje Hudo

Ein Versandmuster kann gut funktionieren.

public static class MessageDispatcher
{
  private static readonly IMessageHandler s_DefaultHandler =
      new DefaultMessageHandler();
  private static readonly Dictionary<Type, IMessageHandler> s_Handlers =
      new Dictionary<Type, IMessageHandler>();

  static MessageDispatcher()
  {
    // Register a bunch of handlers.
    s_Handlers.Add(typeof(OrderMessage), new OrderMessageHandler());
    s_Handlers.Add(typeof(TradeMessage), new TradeMessageHandler());
  }

  public void Dispatch(iMessage msg)
  {
    Type key = msg.GetType();
    if (s_Handlers.ContainsKey(key))
    {
      // We found a specific handler! :)
      s_Handlers[key].Process(msg);
    }
    else
    {
      // We will have to resort to the default handler. :(
      s_DefaultHandler.Process(msg);
    }
  }
}

public interface IMessageHandler
{
  void Process(iMessage msg);
}

public class OrderMessageHandler : IMessageHandler
{
}

public class TradeMessageHandler : IMessageHandler
{
}

Es gibt alle Arten von Variationen zu diesem Thema. Sie verfügen alle über ein Dispatcher-Objekt, das viele verschiedene Handler enthält. Sie sollten einen Standardhandler in Betracht ziehen, falls der Dispatcher keinen bestimmten Handler finden kann. Es gibt viele Freiheiten bei der Auswahl der Nachrichten an die entsprechenden Handler. Ich versende einfach nur nach Typ, aber Sie könnten es beliebig komplexer machen. Möglicherweise könnte der Dispatcher den Inhalt der Nachricht überprüfen, um den besten Handler zu ermitteln. Möglicherweise enthält die Nachricht einen Schlüssel, der einen bevorzugten Handler identifiziert. Ich weiß es nicht. Hier gibt es viele Möglichkeiten.

2
Brian Gideon

Vielleicht möchten Sie einen Blick auf Enterprise Integration Patterns von Gregor Hohpe und Bobby Woolf werfen. Es verfügt über einen guten Musterkatalog für die Nachrichtenverarbeitung.

2
Mike Two

Fügen Sie der iMessage-Schnittstelle eine ProcessMessage () - Methode hinzu, und lassen Sie die konkrete Nachricht polymorphisch den richtigen Weg für die Verarbeitung selbst bestimmen.

Ihr Code wird dann

newMessage.ProcessMessage();

Hier ist ein guter Artikel über mit Polymorphismus anstelle von Bedingungen .

1
Ryan Michela

In einem ähnlichen Szenario habe ich einen Server, der viele verschiedene Nachrichten von mehreren Clients empfängt.

Alle Nachrichten werden serialisiert gesendet und beginnen mit einer Kennung des Nachrichtentyps. Ich habe dann eine switch-Anweisung, die den Bezeichner betrachtet. Die Nachrichten werden dann deserialisiert (zu sehr unterschiedlichen Objekten) und entsprechend verarbeitet.

Ähnliches könnte durch Übergeben von Objekten geschehen, die eine Schnittstelle implementieren, die eine Art der Anzeige des Nachrichtentyps enthält.

public void ProcessMessage(iMessage msg)
{
    switch(msg.GetMsgType())  // GetMsgType() is defined in iMessage
    {
        case MessageTypes.Order:
            ProcessOrder(msg as OrderMessage);  // Or some other processing of order message
            break;
        case MessageTypes.Trade:
            ProcessTrade(msg as TradeMessage); //  Or some other processing of trade message
        break;
        ...
    }
}
1
Matt Lacey

Ich weiß, dass dies ein älterer Thread ist, mit mehreren sehr guten Antworten über die Jahre.

Im Jahr 2018 würde ich jedoch ein Paket wie Jimmy Bogards MediatR verwenden ( https://github.com/jbogard/MediatR ).

Es ermöglicht die Entkopplung von Nachrichtenversand und -verarbeitung mit Mustern wie Request/Response, Command/Query, One-Way, Pub/Sub, Async, polymorphes Dispatching usw.

0
Thiago Silva