Debería haber usado un método de fábrica en lugar de un constructor. ¿Puedo cambiar eso y seguir siendo compatible con versiones anteriores?

15

El problema

Digamos que tengo una clase llamada DataSourceque proporciona un ReadDatamétodo (y tal vez otros, pero mantengamos las cosas simples) para leer datos de un .mdbarchivo:

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

Unos años más tarde, decido que quiero poder admitir .xmlarchivos además de .mdbarchivos como fuentes de datos. La implementación de "lectura de datos" es bastante diferente para .xmly .mdbarchivos; así, si tuviera que diseñar el sistema desde cero, lo definiría así:

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 */ }
}

Genial, una implementación perfecta del patrón del método Factory. Desafortunadamente, DataSourcese encuentra en una biblioteca y refactorizar el código de esta manera rompería todas las llamadas existentes de

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

en los distintos clientes que usan la biblioteca. Ay de mí, ¿por qué no utilicé un método de fábrica en primer lugar?


Soluciones

Estas son las soluciones que podría encontrar:

  1. Hacer que el constructor DataSource devuelva un subtipo ( MdbDataSourceo XmlDataSource). Eso resolvería todos mis problemas. Desafortunadamente, C # no es compatible con eso.

  2. Usa diferentes nombres:

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

    Eso es lo que terminé usando, ya que mantiene el código compatible con versiones anteriores (es decir, llamadas para new DataSource("myFile.mdb")seguir funcionando). Inconveniente: los nombres no son tan descriptivos como deberían ser.

  3. Haga DataSourceun "contenedor" para la implementación 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 { ... }
    }

    Inconveniente: cada método de fuente de datos (como ReadData) debe ser enrutado por código repetitivo. No me gusta el código repetitivo. Es redundante y desordena el código.

¿Hay alguna solución elegante que me haya perdido?

Heinzi
fuente
55
¿Puedes explicar tu problema con el n. ° 3 con un poco más de detalle? Eso me parece elegante. (O tan elegante como puedas mientras mantienes la compatibilidad con versiones anteriores.)
pdr
Definiría una interfaz que publique la API, y luego reutilice el método existente haciendo que una fábrica cree un contenedor alrededor de su código antiguo y los nuevos haciendo que la fábrica cree instancias correspondientes de clases que implementen la interfaz.
Thomas
@pdr: 1. Cada cambio en las firmas de métodos debe hacerse en un lugar más. 2. Puedo hacer que las clases Impl sean internas y privadas, lo cual es inconveniente si un cliente desea acceder a funciones específicas que solo están disponibles, por ejemplo, en una fuente de datos Xml. O puedo hacerlos públicos, lo que significa que los clientes ahora tienen dos formas diferentes de hacer lo mismo.
Heinzi
2
@Heinzi: Prefiero la opción 3. Este es el patrón estándar de "Fachada". Debe verificar si realmente tiene que delegar todos los métodos de origen de datos a la implementación, o solo algunos. ¿Quizás todavía hay algún código genérico que permanece en "DataSource"?
Doc Brown
Es una pena que newno sea un método del objeto de clase (para que pueda subclasificar la clase en sí misma, una técnica conocida como metaclases) y controlar lo que newrealmente hace, pero no es así como funciona C # (o Java, o C ++).
Donal Fellows

Respuestas:

12

Buscaría una variante de su segunda opción que le permita eliminar el antiguo nombre, demasiado 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) { ... }
}

El único inconveniente aquí es que la nueva clase base no puede tener el nombre más obvio, porque ese nombre ya fue reclamado para la clase original y debe permanecer así para la compatibilidad con versiones anteriores. Todas las otras clases tienen sus nombres descriptivos.

Bart van Ingen Schenau
fuente
1
+1, eso es exactamente lo que se me ocurrió cuando leí la pregunta. Aunque me gusta más la opción 3 del OP.
Doc Brown
La clase base podría tener el nombre más obvio si coloca todo el código nuevo en un nuevo espacio de nombres. Pero no estoy seguro de que sea una buena idea.
svick
La clase base debe tener el sufijo "Base". clase DataSourceBase
Stephen
6

La mejor solución será algo cercano a su opción # 3. Mantenga la DataSourcemayoría como está ahora y extraiga solo la parte del lector en su propia clase.

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

De esta manera, evita el código duplicado y está abierto a más extensiones.

nibra
fuente
+1, buena opción. Sin embargo, todavía prefiero la opción 2, ya que me permite acceder a la funcionalidad específica de XmlDataSource creando una instancia explícita XmlDataSource.
Heinzi