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 tenho uma classe chamada DataSourceque fornece um ReadDatamétodo (e talvez outros, mas vamos simplificar) para ler dados de um .mdbarquivo:

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

Alguns anos depois, decido que quero poder suportar .xmlarquivos além de .mdbarquivos como fontes de dados. A implementação para "ler dados" é bem diferente para os arquivos .xmle .mdb; portanto, se eu fosse projetar o sistema a partir 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 de método Factory. Infelizmente, DataSourceestá localizado em uma biblioteca e refatorar o código dessa forma interromperia todas as chamadas existentes de

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

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


Soluções

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

  1. Faça o construtor DataSource retornar um subtipo ( MdbDataSourceou 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")continuar funcionando). Desvantagem: os nomes não são tão descritivos quanto deveriam ser.

  3. Faça DataSourceum "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 { ... }
    }

    Desvantagem: todo método de fonte de dados (como ReadData) deve ser roteado pelo código padrão. Eu não gosto de código padrão. É redundante e desorganiza o código.

Existe alguma solução elegante que eu perdi?

Heinzi
fonte
5
Você pode explicar seu problema com o número 3 com mais detalhes? Isso me parece elegante. (Ou tão elegante como você começa, mantendo compatibilidade com versões anteriores.)
PDR
Eu definiria uma interface que publicaria a API e, em seguida, reutilizaria o método existente, fazendo uma fábrica criar um wrapper em torno do código antigo e dos novos, fazendo a fábrica criar instâncias correspondentes de classes implementando a interface.
Thomas
@pdr: 1. Todas as alterações nas assinaturas de métodos devem ser feitas em mais um local. 2. Posso tornar as classes Impl internas e privadas, o que é inconveniente se um cliente quiser acessar funcionalidades específicas disponíveis apenas em, por exemplo, uma fonte de dados Xml. Ou posso torná-los públicos, o que significa que os clientes agora têm duas maneiras diferentes de fazer a mesma coisa.
Heinzi 29/04
2
@ Heinzi: Prefiro a opção 3. Esse é o padrão "Fachada". Você deve verificar se realmente precisa delegar todos os métodos de fonte de dados para a implementação, ou apenas alguns. Talvez ainda exista algum código genérico que permaneça no "DataSource"?
Doc Brown)
É uma pena que newnão seja um método do objeto de classe (para que você possa subclassificar a própria classe - uma técnica conhecida como metaclasses - e controlar o que newrealmente faz), mas não é assim que funciona o C # (ou Java ou C ++).
Donal Fellows

Respostas:

12

Eu usaria uma variante para sua segunda opção que permite eliminar gradualmente o nome antigo e genérico demais 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.

Bart van Ingen Schenau
fonte
1
+1, é exatamente isso que me veio à mente quando li a pergunta. Embora eu goste mais da opção 3 do OP.
Doc Brown
A classe base pode ter o nome mais óbvio se colocar todo o novo código em um novo espaço para nome. Mas não tenho certeza se é uma boa ideia.
svick
A classe base deve ter o sufixo "Base". classe DataSourceBase
Stephen
6

A melhor solução será algo próximo à sua opção nº 3. Mantenha a DataSourcemaioria como está agora e extraia apenas a parte do leitor em 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 outras extensões.

nibra
fonte
+1, boa opção. No entanto, ainda prefiro a opção 2, pois ela permite acessar funcionalidades específicas do XmlDataSource, instanciando explicitamente XmlDataSource.
Heinzi