Eu deveria ter usado um método de fábrica em vez de um construtor. Posso mudar isso e ainda ser compatível com versões anteriores?

15

O problema

Digamos que eu tenha uma classe chamada DataSource que forneça um método ReadData (e talvez outros, mas vamos manter as coisas simples) para ler dados de um arquivo .mdb :

var source = new DataSource("myFile.mdb");
var data = source.ReadData();

Alguns anos depois, decido que gostaria de poder suportar .xml arquivos além de .mdb arquivos como fontes de dados. A implementação para "leitura de dados" é bem diferente para .xml e .mdb files; assim, se eu fosse projetar o sistema do zero, eu o definiria assim:

abstract class DataSource {
    abstract Data ReadData();
    static DataSource OpenDataSource(string fileName) {
        // return MdbDataSource or XmlDataSource, as appropriate
    }
}

class MdbDataSource : DataSource {
    override Data ReadData() { /* implementation 1 */ }
}

class XmlDataSource : DataSource {
    override Data ReadData() { /* implementation 2 */ }
}

Ótimo, uma implementação perfeita do padrão do método Factory. Infelizmente, DataSource está localizado em uma biblioteca e a refatoração do código desse modo quebra todas as chamadas existentes de

var source = new DataSource("myFile.mdb");

nos vários clientes que usam a biblioteca. Ai de mim, por que eu não usei um método de fábrica em primeiro lugar?

Soluções

Estas são as soluções que eu poderia apresentar:

  1. Faça o construtor DataSource retornar um subtipo ( MdbDataSource ou XmlDataSource ). Isso resolveria todos os meus problemas. Infelizmente, o C # não suporta isso.

  2. Use nomes diferentes:

    abstract class DataSourceBase { ... }    // corresponds to DataSource in the example above
    
    class DataSource : DataSourceBase {      // corresponds to MdbDataSource in the example above
        [Obsolete("New code should use DataSourceBase.OpenDataSource instead")]
        DataSource(string fileName) { ... }
        ...
    }
    
    class XmlDataSource : DataSourceBase { ... }
    

    Foi o que acabei usando, pois mantém o código compatível com versões anteriores (ou seja, as chamadas para new DataSource("myFile.mdb") ainda funcionam). Inconveniente: os nomes não são tão descritivos quanto deveriam ser.

  3. Crie DataSource a "wrapper" para a implementação real:

    class DataSource {
        private DataSourceImpl impl;
    
        DataSource(string fileName) {
            impl = ... ? new MdbDataSourceImpl(fileName) : new XmlDataSourceImpl(fileName);
        }
    
        Data ReadData() {
            return impl.ReadData();
        }
    
        abstract private class DataSourceImpl { ... }
        private class MdbDataSourceImpl : DataSourceImpl { ... }
        private class XmlDataSourceImpl : DataSourceImpl { ... }
    }
    

    Drawback: Todo método da fonte de dados (como ReadData ) deve ser roteado pelo código clichê. Eu não gosto de código clichê. É redundante e confunde o código.

Existe alguma solução elegante que eu tenha perdido?

    
por Heinzi 29.04.2013 / 16:29
fonte

2 respostas

12

Eu escolheria uma variante para sua segunda opção que permite descartar o nome antigo, muito genérico, DataSource :

abstract class AbstractDataSource { ... } // corresponds to the abstract DataSource in the ideal solution

class XmlDataSource : AbstractDataSource { ... }
class MdbDataSource : AbstractDataSource { ... } // contains all the code of the existing DataSource class

[Obsolete("New code should use AbstractDataSource instead")]
class DataSource : MdbDataSource { // an 'empty shell' to keep old code working.
    DataSource(string fileName) { ... }
}

A única desvantagem aqui é que a nova classe base não pode ter o nome mais óbvio, porque esse nome já foi reivindicado para a classe original e precisa permanecer assim para compatibilidade com versões anteriores. Todas as outras classes têm seus nomes descritivos.

    
por 29.04.2013 / 16:52
fonte
6

A melhor solução será algo próximo da sua opção # 3. Mantenha o DataSource na maior parte do tempo e extraia apenas a parte do leitor para sua própria classe.

class DataSource {
    private Reader reader;

    DataSource(string fileName) {
        reader = ... ? new MdbReader(fileName) : new XmlReader(fileName);
    }

    Data ReadData() {
        return reader.next();
    }

    abstract private class Reader { ... }
    private class MdbReader : Reader { ... }
    private class XmlReader : Reader { ... }
}

Dessa forma, você evita códigos duplicados e está aberto a novas extensões.

    
por 30.04.2013 / 12:11
fonte