Berorientasi Aspek Dinamis

Sebuah cerita tentang model subjek tertentu, di mana banyak nilai bidang yang valid bergantung pada nilai orang lain.

Tantangan


Lebih mudah untuk membuat dengan contoh spesifik: perlu mengkonfigurasi sensor dengan banyak parameter, tetapi parameter tergantung satu sama lain. Misalnya, ambang respons tergantung pada jenis sensor, model dan sensitivitas, dan model yang mungkin tergantung pada jenis sensor, dll.

Dalam contoh kami, kami hanya mengambil jenis sensor dan nilainya (ambang batas yang harus dipicu).

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

Pastikan bahwa untuk sensor tegangan dan suhu, nilainya masing-masing hanya berada di kisaran -400..400 dan 200..600. Semua perubahan dapat dilacak dan dicatat.

Solusi "sederhana"


Implementasi termudah untuk menjaga konsistensi data adalah dengan mengatur batasan dan dependensi secara manual dalam setter dan 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; } } } 

Satu ketergantungan menghasilkan sejumlah besar kode yang sulit dibaca. Mengubah kondisi atau menambah dependensi baru itu sulit.

gambar

Dalam proyek nyata, objek seperti itu kami memiliki lebih dari 30 bidang dependen dan lebih dari 200 aturan untuk masing-masing. Solusi yang dijelaskan, meskipun berfungsi, akan membawa sakit kepala yang sangat besar dalam pengembangan dan dukungan sistem semacam itu.

"Ideal" tetapi tidak nyata


Aturan mudah dijelaskan dalam bentuk singkat dan dapat ditempatkan di sebelah bidang yang terkait. Idealnya:

 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 adalah nilai apa yang harus ditetapkan jika kondisinya berubah.

Hanya sintaks C # yang tidak akan mengizinkan penulisan ini dalam atribut, karena daftar bidang yang bergantung pada properti target tidak ditentukan sebelumnya.

Pendekatan kerja


Kami akan menulis aturan sebagai berikut:

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

Tetap membuatnya berfungsi. Kelas semacam itu sama sekali tidak berguna.

Dispatcher


Idenya sederhana - tutup setter dan ubah nilai bidang melalui operator tertentu, yang akan memahami semua aturan, memantau pelaksanaannya, memberi tahu tentang perubahan bidang, dan mencatat semua perubahan.

gambar

Opsi ini berfungsi, tetapi kodenya akan terlihat mengerikan:

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

Tentu saja Anda dapat menjadikan operator sebagai bagian integral dari objek dengan dependensi:

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

Tapi objek saya akan digunakan oleh pengembang lain dan kadang-kadang tidak akan sepenuhnya jelas mengapa mereka perlu menulis. Lagi pula, saya hanya ingin menulis:

 mySensor.Type=SensorType.Voltage; 

Warisan


Khususnya, dalam model kami, kami sendiri mengelola siklus hidup objek dengan dependensi - kami menciptakannya hanya dalam model itu sendiri dan secara lahiriah hanya menyediakan pengeditannya. Oleh karena itu, kita akan membuat semua bidang menjadi virtual, kita akan membiarkan antarmuka eksternal dari model tidak berubah, tetapi itu akan bekerja dengan kelas "pembungkus", yang akan menerapkan logika pemeriksaan.

gambar

Ini adalah pilihan ideal untuk pengguna eksternal, ia akan bekerja dengan objek seperti itu dengan cara biasa

 mySensor.Type=SensorType.Voltage 

Masih belajar bagaimana membuat pembungkus seperti itu

Generasi kelas


Sebenarnya ada dua cara untuk menghasilkan:

  • Hasilkan kode berbasis atribut lengkap
  • Hasilkan kode lengkap yang memanggil pengecekan atribut

Membuat kode berdasarkan atribut sudah pasti keren dan akan bekerja dengan cepat. Tetapi berapa banyak itu akan membutuhkan kekuatan. Dan, yang paling penting, jika Anda perlu menambahkan batasan / aturan baru, berapa banyak perubahan yang diperlukan dan kompleksitas apa?

Kami akan menghasilkan kode standar untuk setiap penyetel, yang akan memanggil metode yang akan menganalisis atribut dan melakukan pemeriksaan.

Kami akan meninggalkan pengambil tidak berubah:

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

Di sini, parameter pertama yang datang ke metode kami didorong ke stack, ini adalah referensi ke objek (ini). Kemudian pengambil kelas dasar dipanggil dan hasilnya dikembalikan, yang diletakkan di atas tumpukan. Yaitu pengambil kami hanya meneruskan panggilan ke kelas dasar.

Setter agak sulit. Untuk analisis, kami akan membuat metode statis, yang akan menghasilkan analisis kira-kira dengan cara berikut:

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

StrongValidate - akan membuang nilai yang tidak dapat dikonversi ke nilai yang sesuai dengan aturan. Sebagai contoh, hanya "y" dan "n" yang diizinkan ditulis dalam kotak teks; ketika Anda mencoba menulis "u", Anda hanya harus menolak perubahan sehingga modelnya tidak hancur.

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

SoftValidate - akan mengonversi nilai dari tidak pantas ke valid. Misalnya, bidang int hanya dapat menerima angka. Saat Anda mencoba menulis 111, Anda dapat mengonversi nilainya ke yang cocok terdekat - "9".

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

<panggil setter basis dengan nilai> - setelah kami mendapatkan nilai yang valid, Anda perlu memanggil setter kelas dasar untuk mengubah nilai field.

ForceValidate - setelah perubahan, kita bisa mendapatkan model yang tidak valid di bidang yang bergantung pada bidang kita. Misalnya, mengubah Jenis menyebabkan perubahan Nilai.

Log hanyalah pemberitahuan dan pencatatan.

Untuk memanggil metode seperti itu, kita perlu objek itu sendiri, nilainya baru dan lama, dan bidang yang berubah. Kode untuk setter akan terlihat seperti ini:

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

Kita akan membutuhkan metode lain yang secara langsung akan mengubah nilai kelas dasar. Kode ini mirip dengan pengambil sederhana, hanya ada dua parameter - ini dan nilai:

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

Semua kode dengan tes kecil dapat ditemukan di sini:
github.com/wolf-off/DinamicAspect

Validasi


Kode validasi sendiri sederhana - mereka hanya mencari atribut aktif saat ini sesuai dengan prinsip kondisi terpanjang dan bertanya kepadanya apakah nilai baru itu valid. Anda hanya perlu mempertimbangkan dua hal saat memilih aturan (menguraikannya dan menghitung yang sesuai):

  • Tembolok hasil GetCustomAttributes. Fungsi yang mengambil atribut dari bidang berfungsi lambat karena membuatnya setiap waktu. Cache hasilnya. Saya menerapkan BaseModel di kelas dasar
  • Saat menghitung aturan yang sesuai, Anda harus berurusan dengan tipe bidang. Jika semua nilai direduksi menjadi string dan dibandingkan, ini akan bekerja lambat. Terutama enum. Diimplementasikan di kelas atribut dasar DependencyAttribute

Kesimpulan


Apa keuntungan dari pendekatan ini?

Dan itu setelah membuat objek:

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

Itu dapat digunakan seperti biasa, tetapi akan berperilaku sesuai dengan atribut:

 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/id472598/


All Articles