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 {
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.

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.

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.

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 });
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/DinamicAspectValidaçõ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);