关于特定主题模型的故事,其中许多有效字段值取决于其他值。
挑战赛
使用特定示例可以更轻松地进行拆卸:需要为传感器配置许多参数,但是这些参数相互依赖。 例如,响应阈值取决于传感器的类型,型号和灵敏度,可能的型号取决于传感器的类型等。
在我们的示例中,我们仅采用传感器的类型及其值(应触发传感器的阈值)。
public class Sensor {
确保电压和温度传感器的值分别只能在-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 });
我们将需要另一个可以直接更改基类值的方法。 该代码类似于一个简单的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);