Dynamische Reihenfolge von Methoden-Aufrufen

  • C#
  • .NET (FX) 4.5–4.8

Es gibt 14 Antworten in diesem Thema. Der letzte Beitrag () ist von Schmittmuthelm.

    Dynamische Reihenfolge von Methoden-Aufrufen

    Hey Leute, kurz zur Ausgangssituation:
    ich habe ein Programm bzw. eine Library die ich für mehrere Maschinen nutzen möchte.
    Das Programm soll Initialisierungsmethoden für verschiedene Hardware beinhalten, unabhängig davon ob die Hardware tatsächlich in der Maschine verbaut ist.
    Die eine Maschine hat z.B. ein Wago IO-Modul mit entsprechenden digitalen Ein- bzw. Ausgängen, eine andere hat einen Scanner und ein Multimeter(DMM).
    Im Programm sollen dann, wie gesagt unabhängig von der Hardwareausstattung, die Init-Methoden enthalten sein wie InitWago, InitDI, InitDO, InitScanner und InitDMM.

    Zu meinem Problem:
    Manche Methoden dürfen nur ausgeführt werden, wenn zuvor eine andere ausgeführt wurde. Bsp.: InitWago muss zwingend vor InitDI und InitDO ausgeführt werden.
    Für solche Fälle setze ich aktuell ein Attribute ein.

    Spoiler anzeigen

    C#-Quellcode

    1. using System;
    2. using System.Collections.Generic;
    3. using System.Linq;
    4. using System.Reflection;
    5. namespace DynamischerMethodenAufruf
    6. {
    7. [AttributeUsage(AttributeTargets.Method)]
    8. internal class InitPriorityAttribute : Attribute
    9. {
    10. public InitPriorityAttribute(bool enabled, InitPriority priority)
    11. {
    12. Priority = priority;
    13. Enabled = enabled;
    14. }
    15. public InitPriorityAttribute(bool enabled, InitPriority priority, string performAfterMethod) : this(enabled, priority)
    16. {
    17. PerformAfterMethod = performAfterMethod;
    18. }
    19. public enum InitPriority
    20. {
    21. Prio1 = 0,
    22. Prio2
    23. }
    24. public bool Enabled { get; private set; }
    25. public string PerformAfterMethod { get; private set; } = string.Empty;
    26. public InitPriority Priority { get; private set; }
    27. private static MethodInfo[] ShellSort(MethodInfo[] array)
    28. {
    29. for (int interval = array.Length / 2; interval > 0; interval /= 2)
    30. {
    31. for (int i = interval; i < array.Length; i++)
    32. {
    33. var item = array[i];
    34. var k = i;
    35. while (k >= interval && array[k - interval].Name != item.GetCustomAttribute<InitPriorityAttribute>().PerformAfterMethod)
    36. {
    37. array[k] = array[k - interval];
    38. k -= interval;
    39. }
    40. array[k] = item;
    41. }
    42. }
    43. return array;
    44. }
    45. public static IEnumerable<MethodInfo> GetSortedMethodInfos(Type type, string identifier)
    46. {
    47. var methodInfos = type.GetMethods(BindingFlags.NonPublic | BindingFlags.Static)
    48. .Where(x => x.Name.StartsWith(identifier) && x.GetCustomAttribute<InitPriorityAttribute>().Enabled)
    49. .OrderBy(x => x.GetCustomAttribute<InitPriorityAttribute>().Priority);
    50. return methodInfos.Select(x => x.GetCustomAttribute<InitPriorityAttribute>().Priority).Distinct()
    51. .SelectMany(x => ShellSort(methodInfos.Where(c => c.GetCustomAttribute<InitPriorityAttribute>().Priority == x).ToArray()));
    52. }
    53. }
    54. }


    Verwendung:

    C#-Quellcode

    1. [InitPriority(true, InitPriorityAttribute.InitPriority.Prio1, nameof(InitWago))]
    2. internal static void InitDI()
    3. {
    4. Console.WriteLine(nameof(InitDI));
    5. }


    Wie man sieht kann man die Methode über die Attribute-Property Enabled entweder ausführen lassen oder eben nicht. Über die Property Priority soll festgelegt werden, in welcher Reihenfolge die Methoden ausgeführt werden.
    Allerdings hatte ich hier das Problem, dass auch innerhalb einer "Prioritätsgruppe" eine bestimmte Reihenfolge einzuhalten ist.
    Müssen beispielsweise die DIs und DOs vor dem Scanner initialisiert werden, wären InitWago, InitDI und InitDO Prio1 und InitScanner wäre Prio2. Aber wie gesagt muss InitWago vor InitDI ausgeführt werden.
    Um dieses Problem zu lösen soll die Property PerformAfterMethod dienen.

    Die Initialisierungsmethoden müssen mit einem bestimmten Identifier beginnen, in dem Fall mit "Init". Auf die einzelnen Attribute-Werte greife ich via Reflection zu.
    Für die Sortierung innerhalb einer Prioritätsgruppe verwende ich einen Algorithmus den ich hier gefunden und für meine Zwecke angepasst habe.

    Aufruf der sortierten Methoden:

    C#-Quellcode

    1. private static void ExecuteSortedMethods()
    2. {
    3. var methodInfos = InitPriorityAttribute.GetSortedMethodInfos(typeof(Program), "Init");
    4. foreach (var methodInfo in methodInfos)
    5. {
    6. methodInfo.Invoke(null, null);
    7. }
    8. }


    Im Anhang ist noch ein kleines Beispiel-Projekt.

    Kann man das so machen oder ist das Pfusch?
    Was kann man verbessern/vereinfachen?
    Dateien
    Dumm ist der, der dumm ist. Nicht andersrum!
    @Schmittmuthelm Relativ einfach:
    Deine Prozeduren wissen anhand der Property Priority, in welcher Reihenfolge sie aufgerufen werden.
    Mach eine Schleife über alle Prioritäten in der richtigen Reihenfolge, und die Priorität, die gerade dran ist, sorgt dafür, dass die damit verknüpfte Prozedur aufgerufen wird.
    Also ein entsprechend langes Select Case Priority.
    Falls Du diesen Code kopierst, achte auf die C&P-Bremse.
    Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch :!:
    Ein guter .NET-Snippetkonverter (der ist verfügbar).
    Programmierfragen über PN / Konversation werden ignoriert!

    Schmittmuthelm schrieb:

    Allerdings hatte ich hier das Problem, dass auch innerhalb einer "Prioritätsgruppe" eine bestimmte Reihenfolge einzuhalten ist.


    Das kann ich leider nicht mit einer Select Case lösen, oder ich versteh dich noch nicht richtig.

    Angenommen ich habe folgende Methoden mit entsprechenden Attributen:

    C#-Quellcode

    1. [InitPriority(true, InitPriorityAttribute.InitPriority.Prio1)]
    2. internal static void InitWago()
    3. {
    4. Console.WriteLine(nameof(InitWago));
    5. }
    6. [InitPriority(true, InitPriorityAttribute.InitPriority.Prio1, nameof(InitWago))]
    7. internal static void InitDI()
    8. {
    9. Console.WriteLine(nameof(InitDI));
    10. }


    Dann haben beide Methoden die selbe Priorität, select-case bringt hier also keine Unterscheidung.
    Deshalb die Property PerformAfterMethod, die eine Unterscheidung innerhalb einer Priority ermöglichen soll.
    In dem Fall soll InitDI erst nach InitWago ausgeführt werden.
    Dumm ist der, der dumm ist. Nicht andersrum!
    Ich bin mir nicht sicher, ob ich die Ausgangssituation richtig verstehe. Ich interpretiere es so, dass du selbst eine Library baust, die intern Code für das Ansteuern verschiedener Maschinen bereitstellt. Je nach Maschine müssen die Init Methoden in der richtigen Reihenfolge ausgeführt werden (aber diese Reihenfolge ist dir pro Maschine bekannt).
    Ich *vermute* jetzt, dass du pro Maschine ein eigenes Programm hast, dass diese Library benutzen möchte. Wahrscheinlich wird eine Funktion in der Lib aufgerufen, die dann den Initialisierungscode der zugehörigen Maschine aufrufen soll. Ist das soweit (oder zumindest so ähnlich) korrekt?

    Die Verwendung von Attributen für das Ganze kommt mir persönlich zu kompliziert vor. Spricht etwas dagegen, die Methoden "simply and stupid" der Reihe nach aufzurufen, abhängig vom Maschinentyp? Also, stark vereinfacht, nach diesem Prinzip?

    Quellcode

    1. If typ == MaschineA
    2. InitA()
    3. InitB()
    4. InitC()
    5. Else If typ = MaschineB
    6. InitB()
    7. InitD()
    8. ...



    Je nach Anforderungen könnte man diesen Ansatz noch verändern/verbessern, aber ich will für den Moment nicht zu weit gehen, ohne die Anforderungen genau zu kennen.
    @Schmittmuthelm Dann habe ich Dich nicht verstanden.
    Gib den Prozeduren eine Property Priority2, die eindeutig die Reihenfolge der Abarbeitung definiert.
    Weiter oben im Text.
    Falls Du diesen Code kopierst, achte auf die C&P-Bremse.
    Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch :!:
    Ein guter .NET-Snippetkonverter (der ist verfügbar).
    Programmierfragen über PN / Konversation werden ignoriert!
    @shad fast.
    Sowas wie MaschineA oder MaschineB gibt es nicht. Es gibt eine zentrale Klasse Machine in der sämtliche Init-Methoden aufgerufen werden.
    Das entsprechende Programm läuft dann jeweils auf dem Rechner der aktuellen Maschine.

    Grundsätzlich war es mal ähnlich wie nach deinem Prinzip:

    C#-Quellcode

    1. public static void InitMachine()
    2. {
    3. InitA();
    4. InitB();
    5. ...
    6. InitY();
    7. }


    Das ganze beläuft sich im "Worst-Case" auf aktuell ca. 35 Init-Methoden, was natürlich weiter wächst wenn neue Hardware-Komponenten dazu kommen.
    Da ich keinen Bock hatte bei jeder neuen Maschine diverse Methoden innerhalb von InitMachine auszukommentieren, zu entfernen oder zu verschieben, bin ich eben auf das Konzept mit dem Attribute gekommen.
    Initial wären dann für alle Methoden die Attribute-Property Enabled auf false und ich passe dann einfach nur die Attribute der Methoden an, die ich für die Maschine tatsächlich haben will.


    @RodFromGermany

    RodFromGermany schrieb:

    Gib
    den Prozeduren eine Property Priority2, die eindeutig die Reihenfolge der Abarbeitung definiert.


    Das wäre evtl. eine Alternative, wobei dann bei entsprechender Methodenanzahl auch die Anzahl von Priority2 ziemlich schnell ansteigt oder?
    Das wollte ich eigentlich vermeiden. Desweiteren gibt es Methoden die erst nach einer anderen ausgeführt werden dürfen, aber untereinander ist mir egal wie die ausgeführt werden.
    Beispiel:

    C#-Quellcode

    1. [InitPriority(true, InitPriorityAttribute.InitPriority.Prio1)]
    2. internal static void InitWago()
    3. {
    4. Console.WriteLine(nameof(InitWago));
    5. }
    6. [InitPriority(true, InitPriorityAttribute.InitPriority.Prio1, nameof(InitWago))]
    7. internal static void InitDI()
    8. {
    9. Console.WriteLine(nameof(InitDI));
    10. }
    11. [InitPriority(true, InitPriorityAttribute.InitPriority.Prio1, nameof(InitWago))]
    12. internal static void InitDO()
    13. {
    14. Console.WriteLine(nameof(InitDO));
    15. }


    Hier muss InitWago zwingend vor InitDO und InitDI ausgeführt werden.
    Ob nach InitWago aber zuerst InitDO und dann InitDI oder zuerst InitDI und dann InitDO aufgerufen wird ist mir eigentlich egal.

    Dumm ist der, der dumm ist. Nicht andersrum!

    Dieser Beitrag wurde bereits 3 mal editiert, zuletzt von „Schmittmuthelm“ ()

    Heißt das, dass du den Code dann pro Rechner, auf dem das Programm nachher laufen soll, einmal kopierst, anpasst und dann für den jeweiligen Rechner neu kompilierst? Wenn ich das richtig verstehe, müssten ja pro Rechner auch die Flags der Attribute unterschiedlich sein, und somit manuell geändert werden.
    Als Beispiel "Enabled": Wenn Rechner A eine Methode InitX() ausführen soll, Rechner B aber nicht, dann musst du das Programm ja einmal für Rechner A mit "Enabled = true" und einmal für Rechner B mit "Enabled = false" kompilieren, da Attribute immer konstant sind.
    Wenn das so ist, wäre meine Anschlussfrage, ob das vom Aufwand her am Ende des Tages nicht doch äquivalent zum Pflegen von deinem ursprünglichen Ansatz ohne Attribute wäre. Ohne den Code zu kennen: Ob ich jetzt in einer Codezeile ein Attribut anpassen muss, oder eine Zeile auskommentiere ist vom Aufwand her kein großer Unterschied. Ich persönlich würde die Lösung ohne Attributen/Reflection immer präferieren, da sie einfach sehr simpel ist. Am Ende des Tages sollen die Attribute ja nur die Ausführungsreihenfolge definieren - "roher Code" macht das allerdings auch (und meiner Meinung nach viel besser, weil expliziter/weniger versteckt). Aber vielleicht ist es im echten Projekt ja tatsächlich komplizierter als hier im Forum beschrieben, daher kann ich es auch einfach falsch einschätzen. ^^

    -------------

    Unabhängig davon: Vielleicht würde es reichen, die Prio in einen Integer zu ändern um beliebig viele Prioritätsgruppen zu erlauben. Das PerformAfter könnte dann wegfallen.

    Wenn du diese Anforderungen hast (momentan):

    Quellcode

    1. Prio Gruppe 1:
    2. InitA // In Gruppe 1 muss A vor B,C ausgeführt werden.
    3. InitB, InitC // Momentan gibt es also PerformAfter = "InitA".
    4. Prio Gruppe2:
    5. InitD // InitD streng nach allem in Gruppe 1.



    Dann entspricht das doch auch dieser Umformung, wenn du beliebig viele Gruppen erlauben würdest:

    Quellcode

    1. Prio Gruppe 1: InitA
    2. Prio Gruppe 2: InitB, InitC // PerformAfter gibt es hier nicht mehr, aber B und C haben eine niedrigere Prio als A. -> Werden nach A aufgerufen.
    3. Prio Gruppe 3: InitD


    Auch wenn InitB und InitC nicht enabled wären, bliebe die Initialisierungsreihenfolge gleich. Erst wird Gruppe 1 ausgeführt, dann Gruppe 3 (weil 1 < 3, wenn du sortierst).

    shad schrieb:

    Wenn ich das richtig verstehe, müssten ja pro Rechner auch die Flags der Attribute unterschiedlich sein, und somit manuell geändert werden.

    Nein, das Priority-Enum ändert sich nicht. Die Member wurden schon so gewählt, dass so ziemlich jede Hardware an die momentan zu denken ist in eine bestimmte Kategorie/Prio-Gruppe passt.
    Aus diesem Grund verwende ich auch ein Enum und eben keinen Integer für die Priorisierung (aus den bekannten Vorzügen, dass ich hier einen sprechenden Namen hab: z.B. InitPriority.PeripheryMeasurement für Multimeter)

    Ich verstehe natürlich die Argumente gegen die Attribute-Variante, vorallem, dass ich auch hier einen gewissen Pflegeaufwand für jede Maschine habe. Aber mir gefällt rein optisch diese Variante eben besser.
    Ich lasse mir auch aktuell die generierte Reihenfolge der InitMethoden ausgeben, damit ich überprüfen kann, ob ich die maschinenspezifische Init-Sequenz richtig konfiguriert habe.
    Wie du schon richtig erkannt hast ist das reelle Projekt um einiges umfangreicher, weshalb sich die Notwendigkeit für diese Konzeptänderung aus dem hier genannten Beispiel vielleicht nicht richtig erschließt.


    Um den Thread abschließend noch mal in Richtung der Eingangsfrage zu bringen:
    Ich würde gerne weiter mit der Attribute-Variante fahren, außer es gibt schwerwiegende, begründete Kritik daran.
    Kann man an der Art und Weise wie ich die Methoden sortiere noch was optimieren/anpassen?
    Ich hab das Gefühl, dass da noch was geht, mein Hirn dafür aber noch nicht bereit ist ?(

    Ansonsten schon mal vielen Dank für euren Input.
    Dumm ist der, der dumm ist. Nicht andersrum!
    Ich würde das vermutlich anders angehen.
    Ich habe eine Klasse Maschine
    Und ich habe für jeden Maschinentyp eine Klasse MaschineA, MaschineB, MaschineC..., die jeweils von Maschine erben.
    Alles was die Maschinen gemeinsam haben wird in Maschine durchgeführt.
    InitMachine ist in der Basisklasse Overridable und wird jeweils in den abgeleiteten Klassen passend überschrieben und mit den Aufrufen der notwendigen Init-Prozeduren befüllt.

    Das hat den Vorteil, dass du nicht nur die Init-Reihenfolge sondern ggf. auch noch anderes spezifisches Eigenverhalten der Maschinen überschreiben kannst.
    --
    If Not Program.isWorking Then Code.Debug Else Code.DoNotTouch
    --
    Das ist für mich leider keine Lösung, da jede Maschine sehr umfangreiche Funktionen, spezifische Anforderungen und damit verbunden auch einen erhebliche Anzahl Codezeilen hat.
    Nach deinem Konzept hätte ich ein Projekt, was für alle Maschinen gültig ist. Wenn ich jetzt meine ganzen Maschinen mit dem spezifischen Code in ein Projekt packe blickt leider kein Schwein mehr durch.

    Ich habe für die verschiedenen Maschinen auch Libraries die grundlegende Sachen handhaben. Es ist also für jede Maschine gleich wie z.B. ein Digitaler Ausgang betätigt wird. Was allerdings die DOs machen bzw. wofür sie genutzt werden (Zylinder fahren lassen, Leuchte anschalten, Signalton usw.) ist wieder Maschinenspezifisch und wird individuell in dem Maschinenprojekt festgelegt.
    Also alle Maschinen in ein Projekt zu bringen ist in meinem Fall der falsche Ansatz.
    Dumm ist der, der dumm ist. Nicht andersrum!

    Schmittmuthelm schrieb:

    Das ist für mich leider keine Lösung, da jede Maschine sehr umfangreiche Funktionen, spezifische Anforderungen und damit verbunden auch einen erhebliche Anzahl Codezeilen hat.
    Das klingt mir doch sehr nach Spagetti-Code und nicht OOP geschweige denn OOD.
    Vielleicht reden wir zunächst über die Philosophie der Ansteuerung überhaupt.
    Falls Du diesen Code kopierst, achte auf die C&P-Bremse.
    Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch :!:
    Ein guter .NET-Snippetkonverter (der ist verfügbar).
    Programmierfragen über PN / Konversation werden ignoriert!
    Also mit dem Problem der Reihenfolgen kenne ich mich aus. Sowas wird elegant mit topologischer Sortierung gelöst.
    Vorraussetzung ist allerdings ein Datenmodell, wo ablesbar ist, welcher Schritt welche anderen Schritte vorraussetzt.

    Eine Zusatzbedingung ist "Ring-Freiheit" - also es darf nicht vorkommen, dass ein Schritt einen anderen zur Vorraussetzung hat - auch mittelbar über viele ZwischenSchritte - von dem er selbst Vorraussetzung ist.

    RodFromGermany schrieb:

    klingt mir doch sehr nach Spagetti-Code
    Das würde ich so nicht sagen. Die DO- und DI-Objekte sind mMn sauber OOP.
    Vielleicht noch mal anders ausgeholt. Das jeweilige Maschinen-Projekt beruht auf einer Art Template wenn man so will. Soll heißen: Ein "leeres" Maschinen-Projekt hat eine Klasse static class MachineActions. Diese Klasse hat im Template keine Member und ist für maschinenspezifische Methoden vorgesehen. Demnach hat jedes einzelne Maschinen-Projekt diese Klasse, aber mit meist gänzlich verschiedenen Methoden.
    Maschine A hat z.B. die Methoden CloseDoor(), in welcher verschiedene DIs abgefragt und verschiedene DOs betätigt werden, und die Methode StartCalib(), in welcher wieder (andere) DOs betätigt werden und Daten mit einem DMM ausgetauscht werden.
    Bei Maschine B gibt es keine Türen und auch nichts zu kalibrieren, hier gibt es dafür dann Methoden wie TakePicture(), welche ein Bild mit einer angeschlossenen Kamera macht und eine Methode ValidatePins(), die z.B. bei einem DUT prüft ob alle Pins vorhanden sind.
    Eine Maschine hat oft mehrere Varianten, sprich bei Maschine A gibt es die Variante 1, in der CloseDoor() und StartCalib() in einer bestimmten Reihenfolge ausgeführt werden und eine Variante 2, in der zwar CloseDoor() ausgeführt werden muss, aber anstatt StartCalib() wird hier sowas wie PrintLabel() benötigt.
    Ich habe also für jede Variante unterschiedliche Sequenzen, die auf Methoden aus MachineActions zugreifen. Das ganze kann man natürlich noch für verschiedene Stationen innerhalb einer Maschine und Variante erweitern.
    Das ganze wird also sehr schnell sehr komplex und ist mit kleinen Beispielen hier im Forum nicht so leicht nachzustellen. Ich hoffe du vertraust mir einfach mal, dass ich das nach bestem Wissen und Gewissen im Sinne der OOP mache :saint:


    @ErfinderDesRades das hört sich interessant an, da muss ich mich wie es aussieht aber erstmal belesen.
    Dumm ist der, der dumm ist. Nicht andersrum!

    Schmittmuthelm schrieb:

    muss ich mich wie es aussieht aber erstmal belesen.
    Zu topologischer Sortierung sich belesen ist nicht so wichtig - den Algo kann ich dir leicht hinbasteln, sobald du ein vernünftiges Datenmodell bereitstellst.

    Das Datenmodell also ist das Problem, und dazu sich belesen ist eine Lebensaufgabe.
    Aber kurz: Es gibt 2 Datenmodell-Strategien:
    1. selbstgebastelte Datenklassen
    2. typisiertes Dataset - auch hierbei entstehen Datenklassen, aber nach einem unerhört gut durchdachten und leistungsfähigen System aufgebaut.
      Vorteil ist natürlich, dass das unerhört gut durchdacht und leistungsfähig ist. Und dass alle die Fehler und Architektur-Probleme entfallen, auf die man beim Selberbasteln stossen würde.
      Nachteil ist, man muss das System kennenlernen, um es nutzen zu können.
      Also dazu kannste dich belesen, etwa im Datenbank-Tut-Bereich des Forums.
    Aber die GrundIdee von Datenmodell ist einfach: Schau die Wirklichkeit an, und mach dir Klassen zurecht, die genauso ticken wie die Wirklichkeit.
    Etwa schau auf eine Schule.
    Dann mach die Klassen Schule, Lehrer, Schüler, Schulklasse, Klassenraum, Jahrgang etc.. Ja, nenn die Klassen genauso wie in Wirklichkeit.
    Und die Dinge hängen miteinander zusammen, also eine Schulklasse hat viele Schüler - also wird die Klasse Schulklasse wohl eine List(Of Schüler) haben usw.

    So ähnlich isses auch mit deinen Schritten. Das habich schon herausgefunden: Jeder Schritt hat keine oder mehrere Vorraussetzungen, und diese Vorraussetzungen sind ebenfalls Schritte.
    Wie gesagt: Wenn du das codemässig umgesetzt hast, kann ich dir dazu eine topologische Sortierung hinhauen, also Reihenfolgen bilden (es sind meist mehrere möglich), bei denen kein Schritt getan wird, bevor nicht alle seine Vorraussetzungen abgearbeitet sind.

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „ErfinderDesRades“ ()

    @ErfinderDesRades Vielen Dank für den Input.
    Ich bin jetzt die letzten zwei Tage auf Arbeit, dann erstmal Urlaub 8-)
    Danach muss ich mal sehen wie danach die Lage ist und ob ich genug Luft habe um mich mit der topologischen Sortierung eingehend zu befassen.
    Ich würde mich dann einfach noch mal an dich wenden, ansonsten muss ich das Thema leider zunächst auf unbestimmte Zeit beiseite legen.
    Dumm ist der, der dumm ist. Nicht andersrum!