Orientado a aspectos dinâmicos

Uma história sobre um modelo de assunto específico, em que muitos valores de campo válidos dependem dos valores de outros.

Desafio


É mais fácil desmontar usando um exemplo específico: é necessário configurar sensores com muitos parâmetros, mas os parâmetros dependem um do outro. Por exemplo, o limite de resposta depende do tipo de sensor, modelo e sensibilidade, e os possíveis modelos dependem do tipo de sensor, etc.

No nosso exemplo, usamos apenas o tipo de sensor e seu valor (o limite no qual ele deve ser acionado).

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; } } 

Certifique-se de que, para sensores de tensão e temperatura, os valores possam estar apenas nas faixas -400..400 e 200..600, respectivamente. Todas as alterações podem ser rastreadas e registradas.

A solução "simples"


A implementação mais fácil para manter a consistência dos dados é definir manualmente restrições e dependências em setters e getters:

 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; } } } 

Uma dependência gera uma grande quantidade de código que é difícil de ler. Mudar condições ou adicionar novas dependências é difícil.

imagem

Em um projeto real, esses objetos tinham mais de 30 campos dependentes e mais de 200 regras para cada um. A solução descrita, embora funcione, traria uma enorme dor de cabeça no desenvolvimento e suporte de um sistema desse tipo.

"Ideal", mas irreal


As regras são facilmente descritas em formas curtas e podem ser colocadas ao lado dos campos aos quais se relacionam. 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; } } 

Force é qual valor definir se a condição mudar.

Somente a sintaxe C # não permitirá gravar isso em atributos, pois a lista de campos dos quais a propriedade de destino depende não é predefinida.

Abordagem de trabalho


Escreveremos as regras da seguinte maneira:

 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; } } 

Resta fazê-lo funcionar. Tal classe é simplesmente inútil.

Despachante


A idéia é simples - feche os levantadores e altere os valores do campo através de um determinado despachante, que entenderá todas as regras, monitorará sua execução, notificará sobre as alterações no campo e registrará todas as alterações.

imagem

A opção está funcionando, mas o código ficará horrível:

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

Obviamente, você pode tornar o expedidor parte integrante de objetos com dependências:

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

Mas meus objetos serão usados ​​por outros desenvolvedores e, às vezes, não ficará totalmente claro para eles por que é tão necessário escrever. Afinal, eu quero escrever simplesmente:

 mySensor.Type=SensorType.Voltage; 

Herança


Especificamente, em nosso modelo, nós próprios gerenciamos o ciclo de vida de objetos com dependências - nós os criamos apenas no próprio modelo e fornecemos externamente apenas sua edição. Portanto, tornaremos todos os campos virtuais, deixaremos a interface externa do modelo inalterada, mas ele já funcionará com classes de "wrappers", que implementarão a lógica das verificações.

imagem

Esta é uma opção ideal para um usuário externo, ele trabalhará com esse objeto da maneira usual

 mySensor.Type=SensorType.Voltage 

Resta aprender a criar esses invólucros

Geração de classe


Na verdade, existem duas maneiras de gerar:

  • Gere código completo baseado em atributo
  • Gere código completo que chama verificação de atributo

Gerar código com base em atributos é definitivamente legal e funcionará rapidamente. Mas quanto isso exigirá força. E, o mais importante, se você precisar adicionar novas restrições / regras, quantas alterações serão necessárias e qual a complexidade?

Geraremos código padrão para cada setter, que chamará métodos que analisarão os atributos e realizarão verificações.

Vamos deixar o getter inalterado:

 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); 

Aqui, o primeiro parâmetro que veio ao nosso método é colocado na pilha, esta é uma referência ao objeto (this). Em seguida, o getter da classe base é chamado e o resultado é retornado, que é colocado no topo da pilha. I.e. nosso getter apenas encaminha a chamada para a classe base.

Setter é um pouco mais complicado. Para análise, criaremos um método estático, que produzirá a análise aproximadamente da seguinte maneira:

 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 não podem ser convertidos para aqueles que se encaixam nas regras. Por exemplo, apenas "y" e "n" podem ser escritos na caixa de texto; ao tentar escrever "u", basta rejeitar as alterações para que o modelo não seja destruído.

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

SoftValidate - converterá valores de inapropriado para válido. Por exemplo, um campo int pode aceitar apenas números. Ao tentar escrever 111, você pode converter o valor para o valor mais próximo adequado - "9".

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

<chame o setter de base com valor> - depois de obtermos um valor válido, você precisará chamar o setter da classe base para alterar o valor do campo.

ForceValidate - após a alteração, podemos obter um modelo inválido nos campos que dependem do nosso campo. Por exemplo, alterar Tipo causa uma alteração no Valor.

Log é apenas notificação e log.

Para chamar esse método, precisamos do próprio objeto, de seu novo e antigo valor e do campo que está mudando. O código para esse setter ficará assim:

 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); 

Vamos precisar de outro método que altere diretamente o valor da classe base. O código é semelhante a um getter simples, apenas existem dois parâmetros - this e value:

 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 o código com pequenos testes pode ser encontrado aqui:
github.com/wolf-off/DinamicAspect

Validações


Os códigos das validações são simples - eles apenas procuram o atributo ativo atual de acordo com o princípio da condição mais longa e perguntam se o novo valor é válido. Você só precisa considerar duas coisas ao escolher regras (analisando-as e calculando as apropriadas):

  • Coloque em cache o resultado de GetCustomAttributes. Uma função que recebe atributos dos campos funciona lentamente porque os cria sempre. Coloque em cache seu resultado. Eu implementei na classe base BaseModel
  • Ao calcular as regras apropriadas, você terá que lidar com os tipos de campo. Se todos os valores forem reduzidos a cadeias e comparados, funcionará lentamente. Especialmente enum. Implementado na classe de atributo base DependencyAttribute

Conclusão


Qual é a vantagem dessa abordagem?

E isso depois de criar o objeto:

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

Pode ser usado como de costume, mas se comportará de acordo com os 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/pt472598/


All Articles