TL; DR Le tue applicazioni dovrebbero dipendere da interfacce che descrivono modelli di messaggi e non dovrebbero dipendere dai dettagli di implementazione relativi a RabbitMQ oa qualsiasi altro tecnologia di trasporto dei messaggi.
In generale, i diversi tipi di middleware tendono a risolvere gli stessi problemi utilizzando schemi di messaggistica comuni, ad esempio:
- Messaggi diretti punto-punto
- Messaggio Broadcast / Multicast
- RPC (richiesta di risposta)
- Consegna dei messaggi garantita affidabile
- Separazione del traffico / segregazione
- ecc.
Ho usato RabbitMQ in due grandi progetti C # finora - in entrambi i casi ho scritto il mio wrapper per incapsulare (nascondere) tutta la "roba" di RabbitMQ, ma modelli come request-response o pub / sub sono agnostici per qualsiasi particolare tecnologia di messaggistica.
Ecco alcuni esempi dei tipi di interfacce che uso nelle applicazioni che usano RabbitMQ - l'aspetto più importante di loro è che nessuna delle applicazioni sa qualcosa di RabbitMQ in particolare, sanno solo su modelli di messaggi e dati grezzi sulla topologia generale del sistema (ad esempio identificatori per endpoint di invio / ricezione, informazioni di routing di messaggi, identificatori di messaggi, ecc.).
In primo luogo, le interfacce usano strutture generiche che includono informazioni sul modello del messaggio insieme ai payload effettivi dei messaggi - nulla qui che necessariamente deve essere legato a RabbitMQ:
public class Message<T>
{
// Message data
T Payload { get; set; }
// Identifiying "Network Name" of the target/recipient
// This might identify an application, a physical host address, etc.
// In RabbitMQ terms, it's a Routing Key.
// If null, assume broadcast/fanout.
string TargetName { get; set; }
// Unique identifier for RPC (Request-Reply) pattern
// - Perhaps a GUID would be appropriate for this
string CorrelationTerm { get; set; }
// Should the message layer include a provision to guarantee delivery?
bool IsGuaranteedDelivery { get; set; }
// TODO - anything else? stuff like durability and persistence adds complexity
// but it can be abstracted out here.
}
L'interfaccia con il livello messaggio stesso non "perde" i nomi delle strutture specifiche di RabbitMQ, tutti i tipi di argomento e i tipi di ritorno fanno parte della libreria wrapper:
public interface IMessageLayer : IDisposable
{
// Fire-n-forget publishing
void Publish<T>(Message<T> message);
// General-purpose method for consumption of received messages
ISubscriber<T> Subscribe<T>(SubscriptionParameters<T> parameters);
// Request-Reply pattern for RPC
Task<Message<TReply>> Request<TReq, TReply>(Message<T> request);
}
Un esempio per un'interfaccia sottoscrittore abbastanza minimale - un evento di callback per ricevere un messaggio, un paio di semplici metodi per iniziare e interrompere la sottoscrizione.
public interface ISubscriber<T> : IDisposable
{
event EventArgs<MessageReceivedEventArgs<T>> MessageReceived;
void StartConsuming();
void StopConsuming();
}
Un breve / breve esempio di come le applicazioni potrebbero usare questo:
Connessione broker
var connectionParams = new MessageLayerConfig { /* Broker details */ }
using (var myMessageEndpoint = MyMessageLayerFactory.Connect(connectionParams))
{
// TODO - Use messaging patterns here
}
Pattern di sottoscrizione di messaggi ad alto livello in un'applicazione:
var mySubscriptionParams = new SubscriptionParams<T> { /* Consumer args */ }
using (var mySubscription = myMessageEndpoint.CreateSubscriber(mySubscriptionParams))
{
var tcs = new TaskCompletionSource<FooMessage>();
mySubscription.MessageReceived += (s, e) =>
{
// TODO: Handle message
tcs.TrySetResult( /* receivedFooMessage */ );
}
mySubscription.StartConsuming();
await tcs.Task;
}
Messaggio di alto livello fire'n'forget publishing in un'applicazione:
var message = new Message<FooMessage>
{
Payload = myLittleFooMessage,
TargetName = "SomeHostName.SomeAppName",
IsGuaranteedDelivery = true,
};
myMessageEndpoint.Publish(message);
Modello di richiesta di risposta ad alto livello in un'applicazione:
var message = new Message<MyRequest>
{
Payload = myRequestData,
TargetName = "SomeRpcServer.SomeRpcAppName",
IsGuaranteedDelivery = false,
};
var task = myMessageEndpoint.Request(message);
if (await Task.WhenAny(task, Task.Delay(5000)) != task)
{
throw new TimeoutException("Request wait time exceeded 5sec");
}
var response = await task;
In generale, la chiave della flessibilità consiste nel mantenere le cose semplici e cercare il più possibile solo affidarsi ai più noti schemi di messaggistica piuttosto che cercare di trovare modi per fare un sacco di cose "intelligenti" con RabbitMQ.
vale a dire. attenersi ai tipi di modelli di messaggi che sono facili da replicare in altre tecnologie (o altre implementazioni AMQP o qualcosa come ZeroMQ), in questo modo la sostituzione del framework sottostante dovrebbe essere abbastanza semplice.
Se decidi di seguire il coniglio giù nel buco con tutti i tipi di pattern di messaggi avanzati come Last Value Cache, Shovels, Federated Exchanges, ecc. allora il lavoro coinvolto in sostituire quei pattern in il futuro sarà maggiore, perché avrai più schemi da replicare.
Infine, un approccio roll-your-own con RabbitMQ non è l'unica opzione; la gente è già andata meglio di questa con framework come MassTransit: link che funziona a un livello molto più alto di RabbitMq.Client
- le sue interfacce sono molto più vicine ai tipi di modelli di messaggi a cui le vostre applicazioni dovrebbero essere interessate.
Anche se si usa anche MassTransit, c'è ancora un vantaggio nella scrittura delle proprie interfacce per il wrapper; la capacità di estrarre qualsiasi framework sottostante e sostituirlo con qualcos'altro con modifiche minimamente intrusive alle applicazioni che dipendono dai pattern dei messaggi.