c# - sirve - Patrón de diseño o soluciones aceptadas para evitar el cambio de tipos
patrones de diseño web (2)
- Use una colección estática de ''Convertidores'' (o lo que sea) que implementen una interfaz común. Luego puede iterar sobre esa colección preguntando a cada uno si manejan el tipo. Si lo hacen, entonces pídales que lo hagan. Cada convertidor solo conoce su tipo.
- Use la misma "colección" estática de conversores, pero manténgalas en un diccionario codificado por tipo. Luego solicite el convertidor por tipo del diccionario y solicite que lo convierta por usted.
Estoy tratando de encontrar un patrón de diseño bueno y limpio o una implementación comúnmente aceptada para tratar una enumeración de tipos donde el tipo individual se conoce solo en el tiempo de ejecución.
Sé que preguntas similares se han hecho antes, pero aún no está claro para mí que las implementaciones alternativas tengan ventajas significativas sobre un conmutador, o una serie de situaciones imprevistas.
Primero, voy a demostrar algunas implementaciones, y luego voy a hacer la pregunta: ¿Son estas implementaciones mejores que o preferidas a un simple cambio? Si es así, ¿por qué? ¿Si no, porque no?
En mi aplicación, envío y recibo datos a través de una transmisión. En tiempo de ejecución, recibo una estructura de datos a través de una serialización que describe qué campos están ubicados dentro de mis datos binarios. Esto incluye el tipo de datos en el campo, es decir, Int32, Bool, Double, etc. En el momento del diseño, todo lo que sé es que los datos pueden estar en uno de varios tipos. Necesito leer los campos de la transmisión y tratar los datos de manera apropiada.
Si se permitiera activar Tipos, una solución podría ser la siguiente:
Código no operativo:
object ReadDataField(byte [] buff, ref int position,
Dictionary<int, Type> fields)
{
object value;
int field = buff[position];
position++;
switch(fields[field])
{
case typeof(Int32):
{
value = (Int32)BitConverter.ToInt32(buff, position);
position += sizeof(Int32);
break;
}
case typeof(Int16):
{
value = (Int16)BitConverter.ToInt16(buff, position);
position += sizeof(Int16);
break;
}
// Etc...
}
return value;
}
En mi opinión, este código tiene la ventaja de ser sencillo, fácil de leer y simple de mantener.
Sin embargo, como el cambio de Tipos no está disponible en C #, implementé lo anterior de la siguiente manera:
Código de trabajo:
enum RawDataTypes
{
Int32,
Int16,
Double,
Single,
etc.
}
object ReadDataField(byte [] buff, ref int position,
Dictionary<int, RawDataTypes> fields)
{
object value;
int field = buff[position];
position++;
switch(fields[field])
{
case RawDataTypes.Int32:
{
value = (int)BitConverter.ToInt32(buff, position);
position += sizeof(int);
break;
}
case RawDataTypes.Int16:
{
value = (Int16)BitConverter.ToInt16(buff, position);
position += sizeof(Int16);
break;
}
// Etc.
}
return value;
}
Esto es claramente una solución alternativa, pero también es sencillo y fácil de mantener.
Sin embargo, hay varios artículos que detallan que el cambio de Tipos no está disponible en C #. Y además de la dificultad de tratar con la herencia de manera que produzca un resultado esperado, etc., he visto muchas respuestas que dicen que hay un enfoque mucho "mejor" que está más en línea con el espíritu de la programación orientada a objetos .
Las soluciones comunes propuestas son 1) usar polimorfismo, o 2) utilizar una búsqueda en el diccionario. Pero implementar cualquiera tiene sus propios desafíos.
Con respecto al polimorfismo, el siguiente es un ejemplo del código "¿no sería bueno si funcionó?":
Implementación no funcional del polimorfismo:
object ReadDataField(byte [] buff, int position,
Dictionary<int, Type> fields)
{
int field = buff[position];
position++;
object value = Activator.CreateInstance(fields[field]);
// Here we''re trying to use an extension method on the raw data type.
value.ReadRawData(buff, ref position);
return value;
}
public static Int32 ReadRawData(this Int32 value, byte[] buff, ref int position)
{
value = BitConverter.ToInt32(buff, position);
position += sizeof(Int32);
return value;
}
public static Int16 ReadRawData(this Int16 value, byte[] buff, ref int position)
{
value = BitConverter.ToInt16 (buff, position);
position += sizeof(Int16 );
return value;
}
// Additional methods for each type...
Si intenta compilar el código anterior, obtendrá:
''objeto'' no contiene una definición para ''ReadRawData'' y la mejor sobrecarga del método de extensión ''RawDataFieldExtensions.ReadRawData (short, byte [], ref int)'' tiene algunos argumentos inválidos en blah blah ...
No puede subclasificar los tipos de datos brutos para agregar la funcionalidad, porque están sellados, por lo que los métodos de extensión parecían una opción. Sin embargo, los métodos de extensión no convertirán de ''objeto'' al tipo real, aunque el valor de llamada. GetType () devuelve el tipo subyacente: System.Int32, System.Int16, etc. El uso de la palabra clave ''dynamic'' no lo hace ayuda, tampoco, porque no puede usar métodos de extensión en un tipo dinámico .
Lo anterior se puede hacer funcionar pasando una instancia del objeto en sí mismo como un parámetro para métodos con parámetros polimórficos:
Implementación de trabajo de polimorfismo:
object ReadDataField(byte [] buff, int position,
Dictionary<int, Type> fields)
{
int field = buff[position];
position++;
dynamic value = Activator.CreateInstance(fields[field]);
// Here the object is passed to an overloaded method.
value = ReadRawData(value, buff, ref position);
return value;
}
public static Int32 ReadRawData(Int32 value, byte[] buff, ref int position)
{
value = BitConverter.ToInt32(buff, position);
position += sizeof(Int32);
return value;
}
public static Int16 ReadRawData(Int16 value, byte[] buff, ref int position)
{
value = BitConverter.ToInt16 (buff, position);
position += sizeof(Int16 );
return value;
}
// Additional methods for each type...
El código anterior funciona y sigue siendo sencillo y fácil de mantener, y probablemente más "en el espíritu de la programación orientada a objetos".
Pero, ¿es realmente "mejor"? Yo diría que es más difícil de mantener, ya que requiere más búsqueda para ver qué tipos se han implementado.
Un enfoque alternativo es usar una búsqueda en el diccionario. Tal código podría verse así:
Implementación del diccionario:
delegate object ReadDelegate(byte [] buff, ref int position);
static Dictionary<Type, ReadDelegate> readers = new Dictionary<Type, ReadDelegate>
{
{ typeof(Int32), ReadInt32 },
{ typeof(Int16), ReadInt16 },
// Etc...
};
object ReadDataField(byte [] buff, int position,
Dictionary<int, Type> fields)
{
int field = buff[position];
position++;
object value = readers[fields[field]](buff, ref position);
return value;
}
public static object ReadInt32(byte[] buff, ref int position)
{
Int32 value = BitConverter.ToInt32(buff, position);
position += sizeof(Int32);
return value;
}
public static object ReadInt16(byte[] buff, ref int position)
{
return BitConverter.ToInt16(buff, position);
position += sizeof(Int16);
return value;
}
// Additional methods for each type...
Una ventaja de la implementación del diccionario, en mi opinión, sobre las soluciones polimórficas es que enumera todos los tipos que pueden manejarse en una ubicación fácil de leer. Esto es útil para la mantenibilidad.
Sin embargo, dados estos ejemplos, ¿hay implementaciones mejores, más limpias, más aceptadas, etc. que tengan una ventaja significativa sobre las anteriores? ¿Estas implementaciones que usan polimorfismo o una búsqueda de diccionario son preferibles a usar un interruptor? Realmente no estoy guardando ningún código, y no estoy seguro de haber aumentado la capacidad de mantenimiento del código.
En cualquier caso, aún necesito enumerar cada uno de los tipos con su propio método. El polimorfismo es diferir el condicional del lenguaje en sí mismo, en lugar de ser explícito con un interruptor o un si-entonces. Usar un diccionario depende de los condicionales internos para hacer su propia búsqueda. Al final del día, ¿cuál es la diferencia?
Use el patrón de diseño de Estrategia :
Defina objetos de convertidor separados (con una interfaz común) que encapsulan los diferentes algoritmos de conversión. Los clientes delegan la conversión al objeto convertidor adecuado en tiempo de ejecución.
Esto reduce en gran medida las dependencias de implementación. El código de cliente es independiente de cómo se implementa la conversión.
Estoy de acuerdo con la respuesta de @David Osborne. E incluso si implementa un solo objeto convertidor con una instrucción switch, esta implementación está encapsulada y oculta de los clientes.