Prima di tutto separerei il parser di pacchetti dal lettore di flussi di dati (in modo che potessi scrivere test senza occuparmi dello stream). Quindi considera una classe base che fornisce un metodo per leggere in un pacchetto e uno per scrivere un pacchetto.
Inoltre vorrei costruire un dizionario (una volta solo allora riutilizzarlo per le chiamate future) come la seguente:
class Program {
static void Main(string[] args) {
var assembly = Assembly.GetExecutingAssembly();
IDictionary<byte, Func<Message>> messages = assembly
.GetTypes()
.Where(t => typeof(Message).IsAssignableFrom(t) && !t.IsAbstract)
.Select(t => new {
Keys = t.GetCustomAttributes(typeof(AcceptsAttribute), true)
.Cast<AcceptsAttribute>().Select(attr => attr.MessageId),
Value = (Func<Message>)Expression.Lambda(
Expression.Convert(Expression.New(t), typeof(Message)))
.Compile()
})
.SelectMany(o => o.Keys.Select(key => new { Key = key, o.Value }))
.ToDictionary(o => o.Key, v => v.Value);
//will give you a runtime error when created if more
//than one class accepts the same message id, <= useful test case?
var m = messages[5](); // consider a TryGetValue here instead
m.Accept(new Packet());
Console.ReadKey();
}
}
[Accepts(5)]
public class FooMessage : Message {
public override void Accept(Packet packet) {
Console.WriteLine("here");
}
}
//turned off for the moment by not accepting any message ids
public class BarMessage : Message {
public override void Accept(Packet packet) {
Console.WriteLine("here2");
}
}
public class Packet {}
public class AcceptsAttribute : Attribute {
public AcceptsAttribute(byte messageId) { MessageId = messageId; }
public byte MessageId { get; private set; }
}
public abstract class Message {
public abstract void Accept(Packet packet);
public virtual Packet Create() { return new Packet(); }
}
Edit: Alcune spiegazioni di ciò che sta succedendo qui:
primo:
[Accepts(5)]
questa linea è un attributo di C# (definito da AcceptsAttribute
) Dice il classe FooMessage
accetta il messaggio id 5.
Secondo:
Sì, il dizionario è in costruzione in fase di esecuzione attraverso la riflessione. Devi solo farlo una volta (lo metterei in una classe singleton che puoi mettere su un caso di test che può essere eseguito per assicurare che il dizionario sia compilato correttamente).
Terzo:
var m = messages[5]();
Questa linea ottiene la seguente espressione lambda compilata dal dizionario e lo esegue:
()=>(Message)new FooMessage();
(Il cast è necessario .NET 3.5, ma non in 4.0 causa alle modifiche covarianti nel modo in cui funzionano i delagati, in 4.0 un oggetto di tipo Func<FooMessage>
può essere assegnato a un oggetto del tipo Func<Message>
.)
espressione Questo lambda è costruito dalla linea Assegnazione valore durante la creazione del dizionario:
Value = (Func<Message>)Expression.Lambda(Expression.Convert(Expression.New(t), typeof(Message))).Compile()
(Il cast qui è necessario per lanciare l'espressione lambda compilato Func<Message>
.)
ho fatto in questo modo perché mi capita per avere già il tipo disponibile per me in quel punto. Si potrebbe anche usare:
Value =()=>(Message)Activator.CreateInstance(t)
ma credo che sarebbe stato più lento (e il cast qui è necessario cambiare Func<object>
in Func<Message>
).
Quarto:
.SelectMany(o => o.Keys.Select(key => new { Key = key, o.Value }))
Ciò è stato fatto perché ho sentito che si potrebbe avere un valore nel porre più AcceptsAttribute
di una volta su una classe (ad accettare più di un ID messaggio per classe). Questo ha anche il piacevole effetto collaterale di ignorare le classi di messaggi che non hanno un attributo id del messaggio (altrimenti il metodo Where dovrebbe avere la complessità di determinare se l'attributo è presente).
Ciao Bill, grazie per la risposta. Sto provando a capirlo! Cosa fa ... [Accetta (5)] ... notazione significa? Il dizionario viene popolato da reflection in fase di esecuzione? – Prembo
Grazie per la spiegazione dettagliata. Ho imparato molto! È una soluzione molto elegante e scalabile. Eccellente. I – Prembo