面向动态方面

关于特定主题模型的故事,其中许多有效字段值取决于其他值。

挑战赛


使用特定示例可以更轻松地进行拆卸:需要为传感器配置许多参数,但是这些参数相互依赖。 例如,响应阈值取决于传感器的类型,型号和灵敏度,可能的型号取决于传感器的类型等。

在我们的示例中,我们仅采用传感器的类型及其值(应触发传感器的阈值)。

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

确保电压和温度传感器的值分别只能在-400..400和200..600范围内。 可以跟踪和记录所有更改。

“简单”的解决方案


维护数据一致性最简单的实现是在setter和getter中手动设置限制和依赖项:

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

一种依赖关系会生成大量难以阅读的代码。 更改条件或添加新的依赖关系很困难。

图片

在一个实际的项目中,这样的对象有30多个相关字段,每个字段都有200多个规则。 所描述的解决方案尽管有效,但将在这种系统的开发和支持方面带来巨大的麻烦。

“理想”但虚幻


规则很容易以简短的形式描述,并且可以放置在它们所涉及的字段旁边。 理想情况下:

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

如果条件发生变化,力是要设置的值。

仅C#语法不允许将其写入属性,因为目标属性所依赖的字段列表尚未预定义。

工作方法


我们将编写以下规则:

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

仍然需要使它起作用。 这样的类根本没用。

调度员


这个想法很简单-关闭设置器并通过某个调度程序来更改字段值,该调度程序将了解所有规则,监视其执行,通知字段更改并记录所有更改。

图片

该选项有效,但是代码看起来很糟糕:

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

您当然可以使调度程序成为具有依赖项的对象的组成部分:

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

但是我的对象将被其他开发人员使用,有时对他们来说并不清楚为什么需要这样做。 毕竟,我只想简单地写:

 mySensor.Type=SensorType.Voltage; 

传承


具体来说,在我们的模型中,我们自己管理具有依赖关系的对象的生命周期-我们仅在模型本身中创建它们,并仅向外提供它们的编辑。 因此,我们将所有字段设为虚拟,我们将使模型的外部接口保持不变,但是它已经可以与“包装器”类一起使用,这将实现检查的逻辑。

图片

对于外部用户来说,这是一个理想的选择,他将以通常的方式使用此类对象

 mySensor.Type=SensorType.Voltage 

有待学习如何创建此类包装器

类生成


实际上有两种生成方式:

  • 生成完整的基于属性的代码
  • 生成调用属性检查的完整代码

基于属性生成代码绝对很酷,并且可以快速工作。 但是需要多少力量。 而且,最重要的是,如果您需要添加新的限制/规则,那么将需要进行多少次更改以及复杂性如何?

我们将为每个设置器生成标准代码,这些代码将调用将分析属性并执行检查的方法。

我们将使getter保持不变:

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

在这里,我们方法的第一个参数被压入堆栈,这是对对象(this)的引用。 然后,调用基类的getter并返回结果,该结果放在堆栈的顶部。 即 我们的getter只是将调用转发给基类。

塞特犬有点棘手。 为了进行分析,我们将创建一个静态方法,该方法将大致以以下方式进行分析:

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

StrongValidate-将丢弃无法转换为符合规则的值。 例如,仅允许在文本框中写入“ y”和“ n”; 当您尝试编写“ u”时,只需拒绝所做的更改,这样就不会破坏模型。

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

SoftValidate-将值从不适当转换为有效。 例如,一个int字段只能接受数字。 当您尝试写入111时,可以将值转换为最接近的合适值-“ 9”。

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

<使用值调用基设置器>-获得有效值后,您需要调用基类的设置器来更改字段的值。

ForceValidate-更改后,我们可以在依赖于我们字段的那些字段中获得无效模型。 例如,更改类型会导致值发生变化。

日志只是通知和日志记录。

要调用这种方法,我们需要对象本身,其新旧值以及正在变化的字段。 这样的setter的代码如下所示:

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

我们将需要另一个可以直接更改基类值的方法。 该代码类似于一个简单的getter,只有两个参数-this和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); 

所有带有小测试的代码都可以在这里找到:
github.com/wolf-off/DinamicAspect

验证方式


验证代码本身很简单-它们只是根据最长条件的原理寻找当前的活动属性,并询问他新值是否有效。 选择规则时,您只需要考虑两件事(分析它们并计算适当的规则):

  • 缓存GetCustomAttributes的结果。 从字段获取属性的函数工作缓慢,因为它每次都会创建属性。 缓存其结果。 我在基类BaseModel中实现
  • 在计算适当的规则时,您将不得不处理字段类型。 如果将所有值都简化为字符串并进行比较,它将运行缓慢。 特别是枚举。 在DependencyAttribute基本属性类中实现

结论


这种方法的优势是什么?

在创建对象之后:

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

它可以照常使用,但是会根据以下属性进行操作:

 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/zh-CN472598/


All Articles