Dynamisch aspektorientiert

Eine Geschichte über ein bestimmtes Themenmodell, bei der viele gültige Feldwerte von den Werten anderer abhängen.

Herausforderung


Die Demontage ist anhand eines bestimmten Beispiels einfacher: Es ist erforderlich, Sensoren mit vielen Parametern zu konfigurieren, die Parameter hängen jedoch voneinander ab. Beispielsweise hängt die Ansprechschwelle von der Art des Sensors, dem Modell und der Empfindlichkeit ab, und mögliche Modelle hängen von der Art des Sensors usw. ab.

In unserem Beispiel nehmen wir nur den Sensortyp und seinen Wert (den Schwellenwert, bei dem er ausgelöst werden soll).

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

Stellen Sie sicher, dass die Werte für Spannungs- und Temperatursensoren nur in den Bereichen -400..400 bzw. 200..600 liegen können. Alle Änderungen können verfolgt und protokolliert werden.

Die "einfache" Lösung


Die einfachste Implementierung zur Aufrechterhaltung der Datenkonsistenz besteht darin, Einschränkungen und Abhängigkeiten in Setzern und Gettern manuell festzulegen:

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

Eine Abhängigkeit generiert eine große Menge an Code, der schwer zu lesen ist. Das Ändern von Bedingungen oder das Hinzufügen neuer Abhängigkeiten ist schwierig.

Bild

In einem realen Projekt hatten wir für solche Objekte mehr als 30 abhängige Felder und jeweils mehr als 200 Regeln. Die beschriebene Lösung würde, obwohl sie funktioniert, große Kopfschmerzen bei der Entwicklung und Unterstützung eines solchen Systems verursachen.

"Ideal" aber unwirklich


Regeln lassen sich leicht in Kurzformen beschreiben und können neben den Feldern platziert werden, auf die sie sich beziehen. Idealerweise:

 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 ist der Wert, der festgelegt werden soll, wenn sich die Bedingung ändert.

Nur die C # -Syntax erlaubt das Schreiben in Attribute nicht, da die Liste der Felder, von denen die Zieleigenschaft abhängt, nicht vordefiniert ist.

Arbeitsansatz


Wir werden die Regeln wie folgt schreiben:

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

Es bleibt, damit es funktioniert. Eine solche Klasse ist einfach nutzlos.

Dispatcher


Die Idee ist einfach: Schließen Sie die Setter und ändern Sie die Feldwerte über einen bestimmten Dispatcher, der alle Regeln versteht, deren Ausführung überwacht, über Feldänderungen benachrichtigt und alle Änderungen protokolliert.

Bild

Die Option funktioniert, aber der Code wird schrecklich aussehen:

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

Sie können den Dispatcher natürlich zu einem integralen Bestandteil von Objekten mit Abhängigkeiten machen:

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

Aber meine Objekte werden von anderen Entwicklern verwendet und manchmal ist ihnen nicht ganz klar , warum das Schreiben so notwendig ist. Immerhin möchte ich einfach schreiben:

 mySensor.Type=SensorType.Voltage; 

Vererbung


Insbesondere haben wir in unserem Modell den Lebenszyklus von Objekten mit Abhängigkeiten selbst verwaltet - wir haben sie nur im Modell selbst erstellt und äußerlich nur ihre Bearbeitung bereitgestellt. Daher werden wir alle Felder virtuell machen, die externe Schnittstelle des Modells unverändert lassen, aber es wird bereits mit Klassen von "Wrappern" funktionieren, die die Logik der Prüfungen implementieren.

Bild

Dies ist eine ideale Option für einen externen Benutzer, der mit einem solchen Objekt auf die übliche Weise arbeitet

 mySensor.Type=SensorType.Voltage 

Es bleibt zu lernen, wie solche Wrapper erstellt werden

Klassengenerierung


Es gibt tatsächlich zwei Möglichkeiten zu generieren:

  • Generieren Sie vollständigen Attribut-basierten Code
  • Generieren Sie vollständigen Code, der die Attributprüfung aufruft

Das Generieren von Code basierend auf Attributen ist definitiv cool und funktioniert schnell. Aber wie viel wird es Kraft erfordern. Und vor allem, wenn Sie neue Einschränkungen / Regeln hinzufügen müssen, wie viele Änderungen sind erforderlich und wie komplex?

Wir generieren Standardcode für jeden Setter, der Methoden aufruft, die die Attribute analysieren und Überprüfungen durchführen.

Wir werden Getter unverändert lassen:

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

Hier wird der erste Parameter, der zu unserer Methode kam, auf den Stapel verschoben. Dies ist eine Referenz auf das Objekt (this). Dann wird der Getter der Basisklasse aufgerufen und das Ergebnis zurückgegeben, das oben auf dem Stapel abgelegt wird. Das heißt, Unser Getter leitet den Anruf einfach an die Basisklasse weiter.

Setter ist etwas kniffliger. Für die Analyse erstellen wir eine statische Methode, mit der die Analyse auf ungefähr folgende Weise erstellt wird:

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

StrongValidate - verwirft Werte, die nicht in Werte konvertiert werden können, die den Regeln entsprechen. Beispielsweise dürfen nur "y" und "n" in das Textfeld geschrieben werden. Wenn Sie versuchen, "u" zu schreiben, müssen Sie nur die Änderungen ablehnen, damit das Modell nicht zerstört wird.

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

SoftValidate - konvertiert Werte von unangemessen in gültig. Beispielsweise kann ein int-Feld nur Zahlen akzeptieren. Wenn Sie versuchen, 111 zu schreiben, können Sie den Wert in den nächstgelegenen geeigneten Wert umwandeln - "9".

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

<Basis-Setter mit Wert aufrufen> - Nachdem wir einen gültigen Wert erhalten haben, müssen Sie den Setter der Basisklasse aufrufen, um den Wert des Felds zu ändern.

ForceValidate - Nach der Änderung können wir in den Feldern, die von unserem Feld abhängen, ein ungültiges Modell erhalten. Wenn Sie beispielsweise den Typ ändern, ändert sich der Wert.

Protokoll ist nur Benachrichtigung und Protokollierung.

Um eine solche Methode aufzurufen, benötigen wir das Objekt selbst, seinen neuen und alten Wert und das Feld, das sich ändert. Der Code für einen solchen Setter sieht folgendermaßen aus:

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

Wir benötigen eine andere Methode, die den Wert der Basisklasse direkt ändert. Der Code ähnelt einem einfachen Getter, nur gibt es zwei Parameter - diesen und den Wert:

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

Alle Codes mit kleinen Tests finden Sie hier:
github.com/wolf-off/DinamicAspect

Validierungen


Die Codes der Validierungen selbst sind einfach - sie suchen nur nach dem aktuell aktiven Attribut nach dem Prinzip der längsten Bedingung und fragen ihn, ob der neue Wert gültig ist. Sie müssen bei der Auswahl von Regeln nur zwei Dinge berücksichtigen (sie analysieren und die entsprechenden berechnen):

  • Zwischenspeichern Sie das Ergebnis von GetCustomAttributes. Eine Funktion, die Attribute aus Feldern entnimmt, arbeitet langsam, da sie jedes Mal erstellt werden. Zwischenspeichern Sie das Ergebnis. Ich habe in der Basisklasse BaseModel implementiert
  • Bei der Berechnung der entsprechenden Regeln müssen Sie sich mit Feldtypen befassen. Wenn alle Werte auf Zeichenfolgen reduziert und verglichen werden, funktioniert dies langsam. Besonders enum. Implementiert in der DependencyAttribute-Basisattributklasse

Fazit


Was ist der Vorteil dieses Ansatzes?

Und das nach dem Erstellen des Objekts:

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

Es kann wie gewohnt verwendet werden, verhält sich jedoch wie folgt:

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


All Articles