Orientation dynamique

Une histoire sur un modèle de sujet spécifique, où de nombreuses valeurs de champ valides dépendent des valeurs des autres.

Défi


Il est plus facile de démonter en utilisant un exemple spécifique: il est nécessaire de configurer des capteurs avec de nombreux paramètres, mais les paramètres dépendent les uns des autres. Par exemple, le seuil de réponse dépend du type de capteur, du modèle et de la sensibilité, et les modèles possibles dépendent du type de capteur, etc.

Dans notre exemple, nous prenons uniquement le type de capteur et sa valeur (le seuil auquel il doit être déclenché).

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

Assurez-vous que pour les capteurs de tension et de température, les valeurs ne peuvent être que dans les plages -400..400 et 200..600, respectivement. Toutes les modifications peuvent être suivies et enregistrées.

La solution «simple»


La mise en œuvre la plus simple pour maintenir la cohérence des données consiste à définir manuellement les restrictions et les dépendances dans les setters et les 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; } } } 

Une dépendance génère une grande quantité de code difficile à lire. Changer les conditions ou ajouter de nouvelles dépendances est difficile.

image

Dans un vrai projet, de tels objets nous avions plus de 30 champs dépendants et plus de 200 règles pour chacun. La solution décrite, bien que fonctionnelle, apporterait un énorme casse-tête dans le développement et le support d'un tel système.

"Idéal" mais irréel


Les règles sont facilement décrites sous forme abrégée et peuvent être placées à côté des champs auxquels elles se rapportent. Idéalement:

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

La force est la valeur à définir si la condition change.

Seule la syntaxe C # ne permettra pas d'écrire cela dans des attributs, car la liste des champs dont dépend la propriété cible n'est pas prédéfinie.

Approche de travail


Nous écrirons les règles comme suit:

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

Il reste à le faire fonctionner. Une telle classe est tout simplement inutile.

Dispatcher


L'idée est simple: fermez les paramètres et modifiez les valeurs de champ via un certain répartiteur, qui comprendra toutes les règles, surveillera leur exécution, notifiera les modifications de champ et consignera toutes les modifications.

image

L'option fonctionne, mais le code aura l'air horrible:

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

Vous pouvez bien sûr faire du répartiteur une partie intégrante des objets avec des dépendances:

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

Mais mes objets seront utilisés par d'autres développeurs et parfois il ne leur sera pas tout à fait clair pourquoi il est si nécessaire d'écrire. Après tout, je veux simplement écrire:

 mySensor.Type=SensorType.Voltage; 

Héritage


Plus précisément, dans notre modèle, nous avons nous-mêmes géré le cycle de vie des objets avec des dépendances - nous ne les avons créés que dans le modèle lui-même et, à l'extérieur, nous n'avons fourni que leur édition. Par conséquent, nous rendrons tous les champs virtuels, nous laisserons l'interface externe du modèle inchangée, mais cela fonctionnera déjà avec des classes de «wrappers», qui implémenteront la logique des vérifications.

image

Ceci est une option idéale pour un utilisateur externe, il travaillera avec un tel objet de la manière habituelle

 mySensor.Type=SensorType.Voltage 

Reste à savoir comment créer de tels wrappers

Génération de classe


Il existe en fait deux façons de générer:

  • Générer un code complet basé sur les attributs
  • Générer un code complet qui appelle la vérification des attributs

La génération de code basé sur des attributs est vraiment cool et fonctionnera rapidement. Mais combien cela demandera de la force. Et, plus important encore, si vous devez ajouter de nouvelles restrictions / règles, combien de changements seront nécessaires et quelle complexité?

Nous générerons du code standard pour chaque setter, qui appellera des méthodes qui analyseront les attributs et effectueront des vérifications.

Nous laisserons getter inchangé:

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

Ici, le premier paramètre qui est venu à notre méthode est poussé sur la pile, c'est une référence à l'objet (this). Ensuite, le getter de la classe de base est appelé et le résultat est renvoyé, qui est placé en haut de la pile. C'est-à-dire notre getter transmet simplement l'appel à la classe de base.

Setter est un peu plus délicat. Pour l'analyse, nous allons créer une méthode statique, qui produira l'analyse approximativement de la manière suivante:

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

StrongValidate - supprimera les valeurs qui ne peuvent pas être converties en celles qui correspondent aux règles. Par exemple, seuls «y» et «n» peuvent être écrits dans la zone de texte; lorsque vous essayez d'écrire "u", il vous suffit de rejeter les modifications afin que le modèle ne soit pas détruit.

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

SoftValidate - convertira les valeurs de inappropriées en valides. Par exemple, un champ int ne peut accepter que des nombres. Lorsque vous essayez d'écrire 111, vous pouvez convertir la valeur en celle appropriée la plus proche - "9".

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

<appeler le setter de base avec value> - après avoir obtenu une valeur valide, vous devez appeler le setter de la classe de base pour changer la valeur du champ.

ForceValidate - après le changement, nous pouvons obtenir un modèle non valide dans les champs qui dépendent de notre champ. Par exemple, la modification du type entraîne une modification de la valeur.

Le journal est simplement une notification et une journalisation.

Pour appeler une telle méthode, nous avons besoin de l'objet lui-même, de sa nouvelle et ancienne valeur, et du champ qui change. Le code d'un tel setter ressemblera à ceci:

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

Nous aurons besoin d'une autre méthode qui changera directement la valeur de la classe de base. Le code est similaire à un simple getter, seulement il y a deux paramètres - this et 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); 

Tout le code avec de petits tests peut être trouvé ici:
github.com/wolf-off/DinamicAspect

Validations


Les codes des validations elles-mêmes sont simples - ils recherchent simplement l'attribut actif actuel selon le principe de la condition la plus longue et lui demandent si la nouvelle valeur est valide. Il suffit de considérer deux choses lors du choix des règles (les analyser et calculer celles qui conviennent):

  • Mettez en cache le résultat de GetCustomAttributes. Une fonction qui prend des attributs de champs fonctionne lentement car elle les crée à chaque fois. Mettez en cache son résultat. J'ai implémenté dans la classe de base BaseModel
  • Lors du calcul des règles appropriées, vous devrez faire face aux types de champs. Si toutes les valeurs sont réduites à des chaînes et comparées, cela fonctionnera lentement. Surtout enum. Implémenté dans la classe d'attributs de base DependencyAttribute

Conclusion


Quel est l'avantage de cette approche?

Et qu'après avoir créé l'objet:

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

Il peut être utilisé comme d'habitude, mais il se comportera selon les attributs:

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


All Articles