Frage

Ich arbeite an einem internen Projekt für mein Unternehmen und ein Teil des Projekts besteht darin, verschiedene „Aufgaben“ aus einer XML-Datei in eine Sammlung von Aufgaben zu analysieren, die später ausgeführt werden sollen.

Da jeder Aufgabentyp über eine Vielzahl unterschiedlicher Felder verfügt, habe ich beschlossen, dass es am besten wäre, jeden Aufgabentyp durch eine separate Klasse darzustellen.

Dazu habe ich eine abstrakte Basisklasse erstellt:

public abstract class Task
{
    public enum TaskType
    {
        // Types of Tasks
    }   

    public abstract TaskType Type
    {
        get;
    }   

    public abstract LoadFromXml(XmlElement task);
    public abstract XmlElement CreateXml(XmlDocument currentDoc);
}

Jede Aufgabe erbte von dieser Basisklasse und enthielt den Code, der erforderlich war, um sich selbst aus dem übergebenen XmlElement zu erstellen und sich selbst wieder in ein XmlElement zu serialisieren.

Ein einfaches Beispiel:

public class MergeTask : Task
{

    public override TaskType Type
    {
        get { return TaskType.Merge; }
    }   

    // Lots of Properties / Methods for this Task

    public MergeTask (XmlElement elem)
    {
        this.LoadFromXml(elem);
    }

    public override LoadFromXml(XmlElement task)
    {
        // Populates this Task from the Xml.
    }

    public override XmlElement CreateXml(XmlDocument currentDoc)
    {
        // Serializes this class back to xml.
    }
}

Der Parser würde dann einen ähnlichen Code verwenden, um eine Aufgabensammlung zu erstellen:

XmlNode taskNode = parent.SelectNode("tasks");

TaskFactory tf = new TaskFactory();

foreach (XmlNode task in taskNode.ChildNodes)
{
    // Since XmlComments etc will show up
    if (task is XmlElement)
    {
        tasks.Add(tf.CreateTask(task as XmlElement));
    }
}

All dies funktioniert wunderbar und ermöglicht es mir, Aufgaben mithilfe der Basisklasse weiterzugeben und gleichzeitig die Struktur beizubehalten, die darin besteht, für jede Aufgabe einzelne Klassen zu haben.

Allerdings bin ich mit meinem Code für TaskFactory.CreateTask nicht zufrieden.Diese Methode akzeptiert ein XmlElement und gibt dann eine Instanz der entsprechenden Task-Klasse zurück:

public Task CreateTask(XmlElement elem)
{
    if (elem != null)
    {
        switch(elem.Name)
        {
            case "merge":
                return new MergeTask(elem);
            default:
                throw new ArgumentException("Invalid Task");
        }
    }
}

Da ich das XMLElement analysieren muss, verwende ich einen großen Schalter (10–15 Fälle im echten Code), um auszuwählen, welche untergeordnete Klasse instanziiert werden soll.Ich hoffe, dass ich hier irgendeinen polymorphen Trick anwenden kann, um diese Methode zu bereinigen.

Irgendein Rat?

War es hilfreich?

Lösung

Dazu nutze ich Reflexion.Sie können eine Fabrik erstellen, die grundsätzlich erweitert werden kann, ohne dass Sie zusätzlichen Code hinzufügen müssen.

Stellen Sie sicher, dass Sie „using System.Reflection“ haben, und fügen Sie den folgenden Code in Ihre Instanziierungsmethode ein.

public Task CreateTask(XmlElement elem)
{
    if (elem != null)
    { 
        try
        {
          Assembly a = typeof(Task).Assembly
          string type = string.Format("{0}.{1}Task",typeof(Task).Namespace,elem.Name);

          //this is only here, so that if that type doesn't exist, this method
          //throws an exception
          Type t = a.GetType(type, true, true);

          return a.CreateInstance(type, true) as Task;
        }
        catch(System.Exception)
        {
          throw new ArgumentException("Invalid Task");
        }
    }
}

Eine weitere Beobachtung ist, dass Sie diese Methode statisch machen und an die Task-Klasse hängen können, sodass Sie die TaskFactory nicht neu erstellen müssen und sich außerdem einen beweglichen Teil sparen, den Sie warten müssen.

Andere Tipps

Erstellen Sie eine „Prototyp“-Instanz jeder Klasse und fügen Sie sie in eine Hashtabelle innerhalb der Factory ein, mit der Zeichenfolge, die Sie im XML erwarten, als Schlüssel.

so dass CreateTask nur das richtige Prototyp-Objekt findet, durch Get() aus der Hashtabelle.

Rufen Sie dann LoadFromXML darauf auf.

Sie müssen die Klassen vorab in die Hashtabelle laden.

Wenn Sie es automatisierter wollen...

Sie können die Klassen „selbstregistrierend“ machen, indem Sie eine statische Registermethode in der Factory aufrufen.

Platzieren Sie Aufrufe zur Registrierung (mit Konstruktoren) in den statischen Blöcken der Task-Unterklassen.Dann müssen Sie nur noch die Klassen „erwähnen“, um die statischen Blöcke auszuführen.

Ein statisches Array von Task-Unterklassen würde dann ausreichen, um sie zu „erwähnen“.Oder verwenden Sie Reflexion, um die Klassen zu erwähnen.

Was halten Sie von Dependency Injection?Ich verwende Ninject und die darin enthaltene kontextbezogene Bindungsunterstützung wäre perfekt für diese Situation.Schau dir das an Blogeintrag Erfahren Sie, wie Sie die kontextbezogene Bindung beim Erstellen von Controllern mit der IControllerFactory verwenden können, wenn diese angefordert werden.Dies sollte eine gute Ressource für den Einsatz in Ihrer Situation sein.

@jholland

Ich glaube nicht, dass die Type-Enumeration benötigt wird, da ich immer so etwas tun kann:

Aufzählung?

Ich gebe zu, dass es sich kitschig anfühlt.Das Nachdenken fühlt sich zunächst schmutzig an, aber sobald Sie das Biest gezähmt haben, werden Sie genießen, was es Ihnen ermöglicht.(Denken Sie an die Rekursion, es fühlt sich schmutzig an, ist aber gut)

Der Trick besteht darin, zu erkennen, dass Sie Metadaten, in diesem Fall eine aus XML bereitgestellte Zeichenfolge, analysieren und in Laufzeitverhalten umwandeln.Das ist es, was Reflexion am besten kann.

Übrigens:Der is-Operator ist auch eine Reflexion.

http://en.wikipedia.org/wiki/Reflection_(computer_science)#Uses

@Tim, am Ende habe ich eine vereinfachte Version Ihres Ansatzes und ChanChans verwendet. Hier ist der Code:

public class TaskFactory
    {
        private Dictionary<String, Type> _taskTypes = new Dictionary<String, Type>();

        public TaskFactory()
        {
            // Preload the Task Types into a dictionary so we can look them up later
            foreach (Type type in typeof(TaskFactory).Assembly.GetTypes())
            {
                if (type.IsSubclassOf(typeof(CCTask)))
                {
                    _taskTypes[type.Name.ToLower()] = type;
                }
            }
        }

        public CCTask CreateTask(XmlElement task)
        {
            if (task != null)
            {
                string taskName = task.Name;
                taskName =  taskName.ToLower() + "task";

                // If the Type information is in our Dictionary, instantiate a new instance of that task
                Type taskType;
                if (_taskTypes.TryGetValue(taskName, out taskType))
                {
                    return (CCTask)Activator.CreateInstance(taskType, task);
                }
                else
                {
                    throw new ArgumentException("Unrecognized Task:" + task.Name);
                }                               
            }
            else
            {
                return null;
            }
        }
    }

@ChanChan

Ich mag die Idee der Reflexion, aber gleichzeitig war ich immer schüchtern, sie einzusetzen.Es kam mir immer wie ein „Hack“ vor, etwas zu umgehen, das einfacher sein sollte.Ich habe über diesen Ansatz nachgedacht und bin dann zu dem Schluss gekommen, dass eine Switch-Anweisung bei gleichem Codegeruch schneller wäre.

Du hast mich zum Nachdenken gebracht: Ich glaube nicht, dass die Type-Enumeration benötigt wird, weil ich immer so etwas tun kann:

if (CurrentTask is MergeTask)
{
    // Do Something Specific to MergeTask
}

Vielleicht sollte ich mein GoF Design Patterns-Buch noch einmal aufschlagen, aber ich dachte wirklich, dass es eine Möglichkeit gibt, die richtige Klasse polymorph zu instanziieren.

Aufzählung?

Ich bezog mich auf die Type-Eigenschaft und die Aufzählung in meiner abstrakten Klasse.

Dann ist es Reflexion!Ich werde Ihre Antwort in etwa 30 Minuten als akzeptiert markieren, um allen anderen Zeit zu geben, sich zu äußern.Es ist ein lustiges Thema.

Danke, dass du es offen gelassen hast, ich werde mich nicht beschweren.Es ist ein lustiges Thema, ich wünschte, Sie könnten es polymorph instanziieren.
Sogar Ruby (und seine überlegene Metaprogrammierung) muss dafür seinen Reflexionsmechanismus nutzen.

@Tal

Ich habe nInject nicht genau untersucht, aber aufgrund meines umfassenden Verständnisses der Abhängigkeitsinjektion glaube ich, dass es das Gleiche bewirken würde wie ChanChans Vorschlag, nur mit mehr Ebenen von Cruft (ähm: Abstraktion).

In einer einmaligen Situation, in der ich es nur hier brauche, denke ich, dass die Verwendung von handgerolltem Reflexionscode ein besserer Ansatz ist, als eine zusätzliche Bibliothek zum Verknüpfen zu haben und sie nur an einer Stelle aufzurufen ...

Aber vielleicht verstehe ich nicht, welchen Vorteil mir nInject hier bieten würde.

Einige Frameworks verlassen sich bei Bedarf möglicherweise auf Reflektion, aber meistens verwenden Sie, wenn Sie so wollen, einen Bootstrapper, um festzulegen, was zu tun ist, wenn eine Instanz eines Objekts benötigt wird.Dies wird normalerweise in einem generischen Wörterbuch gespeichert.Bis vor kurzem habe ich mein eigenes verwendet, als ich anfing, Ninject zu verwenden.

Was mir an Ninject vor allem gefallen hat, ist, dass Reflektion nicht erforderlich ist, wenn sie tatsächlich verwendet werden muss.Stattdessen nutzt es die Codegenerierungsfunktionen von .NET, die es unglaublich schnell machen.Wenn Sie der Meinung sind, dass die Reflexion in dem von Ihnen verwendeten Kontext schneller wäre, können Sie sie auch so einrichten.

Ich weiß, dass das für das, was Sie im Moment brauchen, vielleicht übertrieben ist, aber ich wollte nur auf die Abhängigkeitsinjektion hinweisen und Ihnen einige Denkanstöße für die Zukunft geben.Besuche den Dojo für eine Lektion.

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top