قصة عن نموذج موضوع معين ، حيث تعتمد العديد من قيم الحقول الصحيحة على قيم الآخرين.
مهمة
من الأسهل التفكيك باستخدام مثال محدد: من الضروري تكوين أجهزة استشعار مع العديد من المعلمات ، ولكن المعلمات تعتمد على بعضها البعض. على سبيل المثال ، تعتمد حد الاستجابة على نوع المستشعر والطراز والحساسية ، والنماذج المحتملة تعتمد على نوع المستشعر ، إلخ.
في مثالنا ، نأخذ فقط نوع المستشعر وقيمته (العتبة التي يجب تشغيله بها).
public class Sensor {
تأكد من أنه بالنسبة لمستشعرات الجهد ودرجة الحرارة ، يمكن أن تكون القيم فقط في النطاقين -400 .00 و 200..600 ، على التوالي. جميع التغييرات يمكن تتبعها وتسجيلها.
الحل "البسيط"
أسهل تطبيق للحفاظ على تناسق البيانات هو تعيين القيود والتبعيات يدويًا في أدوات التثبيت والحروف:
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);
هنا ، يتم دفع المعلمة الأولى التي جاءت إلى طريقتنا على المكدس ، وهذا مرجع كائن (هذا). ثم يتم استدعاء 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")]
<استدعاء الضابط الأساسي مع القيمة> - بعد أن نحصل على قيمة صالحة ، تحتاج إلى استدعاء setter من الفئة الأساسية لتغيير قيمة الحقل.
ForceValidate - بعد التغيير ، يمكننا الحصول على نموذج غير صالح في تلك الحقول التي تعتمد على مجالنا. على سبيل المثال ، يؤدي تغيير النوع إلى تغيير في القيمة.
السجل هو مجرد إشعار وتسجيل.
للاتصال بهذه الطريقة ، نحتاج إلى الكائن نفسه ، وقيمته الجديدة والقديمة ، والحقل الذي يتغير. سيبدو رمز هذا المضبط كالتالي:
MethodBuilder setPropMthdBldr = typeBuilder.DefineMethod("set_" + property.Name, MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig | MethodAttributes.Virtual, null, new[] { property.PropertyType });
سنحتاج إلى طريقة أخرى من شأنها أن تغير قيمة الفئة الأساسية مباشرةً. يشبه الكود حاصل بسيط ، هناك فقط معلمتان - هذا وقيمة:
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);