Antes que nada, separaría el analizador de paquetes del lector de flujo de datos (para poder escribir pruebas sin tener que lidiar con la transmisión). Luego considere una clase base que proporcione un método para leer en un paquete y otro para escribir un paquete.
Además me gustaría construir un diccionario (una sola vez y luego volver a utilizarlo para futuras llamadas) como el siguiente:
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(); }
}
Editar: Algunas explicaciones de lo que está pasando aquí:
primero:
[Accepts(5)]
Esta línea es un atributo de C# (definido por AcceptsAttribute
) Dice el FooMessage
la clase acepta el mensaje de identificación de 5.
Segundo:
Si el diccionario está siendo construida en tiempo de ejecución a través de la reflexión. Solo necesita hacer esto una vez (lo pondría en una clase única que puede poner un caso de prueba que se puede ejecutar para asegurarse de que el diccionario se construye correctamente).
Tercero:
var m = messages[5]();
Esta línea se hace la siguiente expresión lambda compilado rendimiento del diccionario y lo ejecuta:
()=>(Message)new FooMessage();
(El reparto es necesaria en .NET 3.5, pero no en 4.0 debido a los cambios covariantes en cómo funcionan las demoras, en 4.0 se puede asignar un objeto del tipo Func<FooMessage>
a un objeto del tipo Func<Message>
.)
expresión Este lambda es construido por la línea de asignación de valor durante la creación del diccionario:
Value = (Func<Message>)Expression.Lambda(Expression.Convert(Expression.New(t), typeof(Message))).Compile()
(El reparto aquí es necesario convertir la expresión lambda compilado a Func<Message>
.)
lo hice de esta manera porque da la casualidad ya tengo el tipo disponible para mí en ese punto. También es posible usar:
Value =()=>(Message)Activator.CreateInstance(t)
Pero creo que sería más lento (y el reparto aquí es necesario cambiar Func<object>
en Func<Message>
).
Cuarto:
.SelectMany(o => o.Keys.Select(key => new { Key = key, o.Value }))
Esto se hizo porque sentía que podría tener valor en la colocación de la AcceptsAttribute
más de una vez en una clase (a aceptar más de un mensaje de identificación por clase). Esto también tiene el agradable efecto secundario de ignorar las clases de mensajes que no tienen un atributo id de mensaje (de lo contrario, el método Where debería tener la complejidad de determinar si el atributo está presente).
Hola Bill, gracias por esa respuesta. ¡Estoy tratando de entenderlo! ¿Qué significa ... [Se acepta (5)] ... notación significa? ¿Está el diccionario poblado por reflexión en tiempo de ejecución? – Prembo
Gracias por esa explicación detallada. ¡Aprendí mucho! Es una solución muy elegante y escalable. Excelente. I – Prembo