Pergunta

Estou trabalhando em um projeto interno para minha empresa e parte do projeto é poder analisar várias "Tarefas" de um arquivo XML em uma coleção de tarefas a serem executadas posteriormente.

Como cada tipo de tarefa tem vários campos associados diferentes, decidi que seria melhor representar cada tipo de tarefa com uma classe separada.

Para fazer isso, construí uma classe base abstrata:

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

Cada tarefa herdou dessa classe base e incluiu o código necessário para criar a si mesma a partir do passado em XmlElement, bem como serializar-se novamente em um XmlElement.

Um exemplo básico:

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

O analisador usaria então um código semelhante a este para criar uma coleção de tarefas:

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

Tudo isso funciona maravilhosamente bem e me permite distribuir tarefas usando a classe base, mantendo a estrutura de classes individuais para cada tarefa.

No entanto, não estou satisfeito com meu código para TaskFactory.CreateTask.Este método aceita um XmlElement e retorna uma instância da classe Task apropriada:

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

Como preciso analisar o XMLElement, estou usando uma opção enorme (10 a 15 casos no código real) para escolher qual classe filha instanciar.Espero que haja algum tipo de truque polimórfico que eu possa fazer aqui para limpar esse método.

Algum conselho?

Foi útil?

Solução

Eu uso a reflexão para fazer isso.Você pode criar uma fábrica que basicamente se expande sem precisar adicionar nenhum código extra.

certifique-se de ter "usando System.Reflection", coloque o seguinte código em seu método de instanciação.

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

Outra observação é que você pode tornar esse método estático e pendurá-lo na classe Task, para não precisar atualizar o TaskFactory e também economizar uma peça móvel para manter.

Outras dicas

Crie uma instância "Protótipo" de cada classe e coloque-as em uma hashtable dentro da fábrica, com a string que você espera no XML como chave.

Então, o createTask encontra o objeto de protótipo certo, obtendo () ing a partir da hashtable.

em seguida, chame LoadFromXML nele.

você tem que pré-carregar as classes na tabela hash,

Se você quiser mais automático...

Você pode fazer com que as classes sejam "auto-registradas" chamando um método de registro estático na fábrica.

Coloque chamadas para registrar (com construtores) nos blocos estáticos nas subclasses Task.Então tudo que você precisa fazer é "mencionar" as classes para executar os blocos estáticos.

Uma matriz estática de subclasses Task seria suficiente para "mencioná-las".Ou use a reflexão para mencionar as aulas.

O que você acha da injeção de dependência?Eu uso o Ninject e o suporte de vinculação contextual seria perfeito para esta situação.Veja isso postagem no blog sobre como você pode usar a ligação contextual na criação de controladores com o IControllerFactory quando eles são solicitados.Este deve ser um bom recurso sobre como usá-lo em sua situação.

@jholland

Não acho que o Type enum seja necessário, porque sempre posso fazer algo assim:

Enum?

Eu admito que parece hacky.A reflexão parece suja no início, mas depois de domesticar a fera, você desfrutará do que ela lhe permite fazer.(Lembre-se da recursão, parece sujo, mas é bom)

O truque é perceber que você está analisando metadados, neste caso uma string fornecida pelo xml, e transformando-os em comportamento de tempo de execução.É nisso que a reflexão é melhor.

POR FALAR NISSO:o operador is também é reflexão.

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

@Tim, acabei usando uma versão simplificada da sua abordagem e ChanChans, aqui está o código:

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

Gosto da ideia de reflexão, mas ao mesmo tempo sempre tive vergonha de usar a reflexão.Sempre me pareceu um "hack" contornar algo que deveria ser mais fácil.Eu considerei essa abordagem e então imaginei que uma instrução switch seria mais rápida para a mesma quantidade de cheiro de código.

Você me fez pensar, não acho que o Type enum seja necessário, porque sempre posso fazer algo assim:

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

Talvez eu devesse abrir meu livro GoF Design Patterns novamente, mas realmente pensei que havia uma maneira de instanciar polimorficamente a classe certa.

Enum?

Eu estava me referindo à propriedade Type e enum na minha classe abstrata.

Reflexão é então!Marcarei sua resposta como aceita em cerca de 30 minutos, apenas para dar tempo para que alguém possa avaliar.É um tema divertido.

Obrigado por deixar aberto, não vou reclamar.É um tópico divertido, gostaria que você pudesse instanciar polimorficamente.
Até o Ruby (e sua metaprogramação superior) precisa usar seu mecanismo de reflexão para isso.

@Dale

Eu não inspecionei o nInject de perto, mas pelo meu alto nível de conhecimento sobre injeção de dependência, acredito que ele realizaria a mesma coisa que a sugestão do ChanChan, apenas com mais camadas de lixo (ou seja, abstração).

Em uma situação única em que eu só preciso disso aqui, acho que usar algum código de reflexão manual é uma abordagem melhor do que ter uma biblioteca adicional para vincular e chamá-la apenas em um lugar...

Mas talvez eu não entenda a vantagem que o nInject me daria aqui.

Alguns frameworks podem contar com reflexão quando necessário, mas na maioria das vezes você usa um bootstrapper, se quiser, para configurar o que fazer quando uma instância de um objeto for necessária.Isso geralmente é armazenado em um dicionário genérico.Usei o meu até recentemente, quando comecei a usar o Ninject.

Com o Ninject, o que mais gostei nele foi que quando ele precisa usar reflexão, isso não acontece.Em vez disso, aproveita os recursos de geração de código do .NET, que o tornam incrivelmente rápido.Se você acha que a reflexão seria mais rápida no contexto que você está usando, isso também permite configurá-la dessa forma.

Eu sei que isso pode ser um exagero para o que você precisa no momento, mas eu só queria apontar a injeção de dependência e dar-lhe um pouco de reflexão para o futuro.Visite a dojo para uma aula.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top