Aspecto Dinámico Orientado

Una historia sobre un modelo de tema específico, donde muchos valores de campo válidos dependen de los valores de otros.

Desafío


Es más fácil desmontar usando un ejemplo específico: es necesario configurar sensores con muchos parámetros, pero los parámetros dependen unos de otros. Por ejemplo, el umbral de respuesta depende del tipo de sensor, modelo y sensibilidad, y los posibles modelos dependen del tipo de sensor, etc.

En nuestro ejemplo, tomamos solo el tipo de sensor y su valor (el umbral en el que debe activarse).

public class Sensor { // Voltage, Temperature public SensorType Type { get; internal set; } //-400..400 for Voltage, 200..600 for Temperature public decimal Value { get; internal set; } } 

Asegúrese de que para los sensores de voltaje y temperatura los valores solo pueden estar en los rangos -400..400 y 200..600, respectivamente. Todos los cambios pueden ser rastreados y registrados.

La solución "simple"


La implementación más fácil para mantener la consistencia de los datos es establecer manualmente restricciones y dependencias en los establecedores y captadores:

 public class Sensor { private SensorType _type; private decimal _value; public SensorType Type { get { return _type; } set { _type = value; if (value == SensorType.Temperature) Value = 273; if (value == SensorType.Voltage) Value = 0; } } public decimal Value { get { return _value; } set { if (Type == SensorType.Temperature && value >= 200 && value <= 600 || Type == SensorType.Voltage && value >= -400 && value <= 400) _value = value; } } } 

Una dependencia genera una gran cantidad de código que es difícil de leer. Cambiar las condiciones o agregar nuevas dependencias es difícil.

imagen

En un proyecto real, dichos objetos tenían más de 30 campos dependientes y más de 200 reglas para cada uno. La solución descrita, aunque funciona, traería un gran dolor de cabeza en el desarrollo y soporte de dicho sistema.

"Ideal" pero irreal


Las reglas se describen fácilmente en formas cortas y se pueden colocar al lado de los campos con los que se relacionan. Idealmente:

 public class Sensor { public SensorType Type { get; set; } [Number(Type = SensorType.Temperature, Min = 200, Max = 600, Force = 273)] [Number(Type = SensorType.Voltage, Min = -400, Max = 400, Force = 0)] public decimal Value { get; set; } } 

La fuerza es qué valor establecer si la condición cambia.

Solo la sintaxis de C # no permitirá escribir esto en los atributos, ya que la lista de campos de los que depende la propiedad de destino no está predefinida.

Enfoque de trabajo


Escribiremos las reglas de la siguiente manera:

 public class Sensor { public SensorType Type { get; set; } [Number("Type=Temperature", "200..600", Force = "273")] [Number("Type=Voltage", "-400..400", Force = "0")] public decimal Value { get; set; } } 

Queda por hacer que funcione. Tal clase es simplemente inútil.

Despachador


La idea es simple: cierre los establecedores y cambie los valores de campo a través de un despachador determinado, que comprenderá todas las reglas, supervisará su ejecución, notificará sobre los cambios de campo y registrará todos los cambios.

imagen

La opción está funcionando, pero el código se verá horrible:

 someDispatcher.Set(mySensor, "Type", SensorType.Voltage); 

Por supuesto, puede hacer que el despachador sea una parte integral de los objetos con dependencias:

 mySensor.Set("Type", SensorType.Voltage) 

Pero mis objetos serán utilizados por otros desarrolladores y, a veces, no les quedará completamente claro por qué es tan necesario escribir. Después de todo, quiero escribir simplemente:

 mySensor.Type=SensorType.Voltage; 

Herencia


Específicamente, en nuestro modelo, nosotros mismos gestionamos el ciclo de vida de los objetos con dependencias: los creamos solo en el modelo mismo y externamente solo proporcionamos su edición. Por lo tanto, haremos que todos los campos sean virtuales, dejaremos la interfaz externa del modelo sin cambios, pero ya funcionará con clases de "envoltorios", que implementarán la lógica de las comprobaciones.

imagen

Esta es una opción ideal para un usuario externo, trabajará con dicho objeto de la manera habitual

 mySensor.Type=SensorType.Voltage 

Queda por aprender cómo crear tales envoltorios

Generación de clase


En realidad, hay dos formas de generar:

  • Generar código completo basado en atributos
  • Genere código completo que llame a la verificación de atributos

Generar código basado en atributos es definitivamente genial y funcionará rápidamente. Pero cuánto requerirá fuerza. Y, lo más importante, si necesita agregar nuevas restricciones / reglas, ¿cuántos cambios serán necesarios y qué complejidad?

Generaremos código estándar para cada setter, que llamará a métodos que analizarán los atributos y realizarán verificaciones.

Vamos a dejar getter sin cambios:

 MethodBuilder getPropMthdBldr = typeBuilder.DefineMethod("get_" + property.Name, MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig | MethodAttributes.Virtual, property.PropertyType, Type.EmptyTypes); ILGenerator getIl = getPropMthdBldr.GetILGenerator(); getIl.Emit(OpCodes.Ldarg_0); getIl.Emit(OpCodes.Call, property.GetMethod); getIl.Emit(OpCodes.Ret); 

Aquí, el primer parámetro que llegó a nuestro método se inserta en la pila, esto es una referencia al objeto (esto). Luego se llama al captador de la clase base y se devuelve el resultado, que se coloca en la parte superior de la pila. Es decir nuestro captador simplemente reenvía la llamada a la clase base.

Setter es un poco más complicado. Para el análisis, crearemos un método estático, que producirá el análisis aproximadamente de la siguiente manera:

 if (StrongValidate(this, property, value)) { value = SoftValidate(this, property, value); if (oldValue != value) { <    value>; ForceValidate(baseModel, property); Log(baseModel, property, value, oldValue); } } 

StrongValidate: descartará valores que no se puedan convertir a aquellos que se ajusten a las reglas. Por ejemplo, solo se permite escribir "y" y "n" en el cuadro de texto; cuando intenta escribir "u", solo tiene que rechazar los cambios para que el modelo no se destruya.

  [String("", "y, n")] 

SoftValidate: convertirá valores de inapropiados a válidos. Por ejemplo, un campo int solo puede aceptar números. Cuando intente escribir 111, puede convertir el valor al más adecuado: "9".

  [Number("", "0..9")] 

<llamar al establecedor base con valor>: después de obtener un valor válido, debe llamar al establecedor de la clase base para cambiar el valor del campo.

ForceValidate: después del cambio, podemos obtener un modelo no válido en los campos que dependen de nuestro campo. Por ejemplo, cambiar el tipo provoca un cambio en el valor.

El registro es solo notificación y registro.

Para llamar a tal método, necesitamos el objeto en sí, su valor nuevo y antiguo, y el campo que está cambiando. El código para tal setter se verá así:

 MethodBuilder setPropMthdBldr = typeBuilder.DefineMethod("set_" + property.Name, MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig | MethodAttributes.Virtual, null, new[] { property.PropertyType }); //  ,    var setter = typeof(DinamicWrapper).GetMethod("Setter", BindingFlags.Static | BindingFlags.Public); ILGenerator setIl = setPropMthdBldr.GetILGenerator(); setIl.Emit(OpCodes.Ldarg_0);//     - this setIl.Emit(OpCodes.Ldarg_1);//     -  setter    (value) if (property.PropertyType.IsValueType) //  Boxing,    Value,         object { setIl.Emit(OpCodes.Box, property.PropertyType); } setIl.Emit(OpCodes.Ldstr, property.Name); //     setIl.Emit(OpCodes.Call, setter); setIl.Emit(OpCodes.Ret); 

Necesitaremos otro método que cambie directamente el valor de la clase base. El código es similar a un getter simple, solo hay dos parámetros: esto y valor:

 MethodBuilder setPureMthdBldr = typeBuilder.DefineMethod("set_Pure_" + property.Name, MethodAttributes.Public, CallingConventions.Standard, null, new[] { property.PropertyType }); ILGenerator setPureIl = setPureMthdBldr.GetILGenerator(); setPureIl.Emit(OpCodes.Ldarg_0); setPureIl.Emit(OpCodes.Ldarg_1); setPureIl.Emit(OpCodes.Call, property.GetSetMethod()); setPureIl.Emit(OpCodes.Ret); 

Todo el código con pequeñas pruebas se puede encontrar aquí:
github.com/wolf-off/DinamicAspect

Validaciones


Los códigos de las validaciones en sí son simples: solo buscan el atributo activo actual de acuerdo con el principio de la condición más larga y le preguntan si el nuevo valor es válido. Solo debe tener en cuenta dos cosas al elegir las reglas (analizarlas y calcular las apropiadas):

  • Caché el resultado de GetCustomAttributes. Una función que toma atributos de los campos funciona lentamente porque los crea cada vez. Caché su resultado. Implementé en la clase base BaseModel
  • Al calcular las reglas apropiadas, tendrá que lidiar con los tipos de campo. Si todos los valores se reducen a cadenas y se comparan, funcionará lentamente. Especialmente enum. Implementado en la clase de atributo base DependencyAttribute

Conclusión


¿Cuál es la ventaja de este enfoque?

Y eso después de crear el objeto:

 var target = DinamicWrapper.Create<Sensor>(); 

Se puede usar como de costumbre, pero se comportará de acuerdo con los atributos:

 target.Type = SensorType.Temperature; target.Value=300; Assert.AreEqual(target.Value, 300); // true target.Value=3; Assert.AreEqual(target.Value, 200); // true - minimum target.Value=3000; Assert.AreEqual(target.Value, 600); // true - maximum target.Type = SensorType.Voltage; Assert.AreEqual(target.Value, 0); // true - minimum target.Value= 3000; Assert.AreEqual(target.Value, 400); // true - maximum 

Source: https://habr.com/ru/post/472598/


All Articles