J'aurais dû utiliser une méthode d'usine au lieu d'un constructeur. Puis-je changer cela et être toujours rétrocompatible?

15

Le problème

Disons que j'ai une classe appelée DataSourcequi fournit une ReadDataméthode (et peut-être d'autres, mais gardons les choses simples) pour lire les données d'un .mdbfichier:

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

Quelques années plus tard, je décide que je veux pouvoir prendre en charge des .xmlfichiers en plus des .mdbfichiers en tant que sources de données. L'implémentation de la "lecture des données" est très différente pour les fichiers .xmlet .mdb; ainsi, si je devais concevoir le système à partir de zéro, je le définirais comme ceci:

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

Génial, une implémentation parfaite du modèle de méthode Factory. Malheureusement, DataSourceest situé dans une bibliothèque et refactoriser le code comme celui-ci romprait tous les appels existants de

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

dans les différents clients utilisant la bibliothèque. Malheur à moi, pourquoi n'ai-je pas utilisé une méthode d'usine en premier lieu?


Solutions

Voici les solutions que je pourrais trouver:

  1. Faites en sorte que le constructeur DataSource renvoie un sous-type ( MdbDataSourceou XmlDataSource). Cela résoudrait tous mes problèmes. Malheureusement, C # ne prend pas en charge cela.

  2. Utilisez des noms différents:

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

    C'est ce que j'ai fini par utiliser car il maintient le code rétrocompatible (c'est-à-dire que les appels new DataSource("myFile.mdb")fonctionnent toujours). Inconvénient: les noms ne sont pas aussi descriptifs qu'ils devraient l'être.

  3. Faites DataSourceun "wrapper" pour la vraie implémentation:

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

    Inconvénient: chaque méthode de source de données (telle que ReadData) doit être acheminée par du code passe-partout. Je n'aime pas le code passe-partout. C'est redondant et encombre le code.

Y a-t-il une solution élégante que j'ai ratée?

Heinzi
la source
5
Pouvez-vous expliquer votre problème avec le n ° 3 plus en détail? Cela me semble élégant. (Ou aussi élégant que possible tout en conservant la compatibilité descendante.)
pdr
Je définirais une interface publiant l'API, puis je réutiliserais la méthode existante en ayant une fabrique créant un wrapper autour de votre ancien code et de nouvelles en demandant à la fabrique de créer des instances correspondantes de classes implémentant l'interface.
Thomas
@pdr: 1. Chaque modification des signatures de méthode doit être effectuée à un autre endroit. 2. Je peux soit rendre les classes Impl internes et privées, ce qui n'est pas pratique si un client souhaite accéder à des fonctionnalités spécifiques uniquement disponibles, par exemple, dans une source de données Xml. Ou je peux les rendre publics, ce qui signifie que les clients ont maintenant deux façons différentes de faire la même chose.
Heinzi
2
@Heinzi: Je préfère l'option 3. Il s'agit du motif standard "Façade". Vous devez vérifier si vous devez vraiment déléguer chaque méthode de source de données à l'implémentation, ou seulement certaines. Peut-être y a-t-il encore du code générique qui reste dans "DataSource"?
Doc Brown
C'est dommage que ce newne soit pas une méthode de l'objet classe (afin que vous puissiez sous-classer la classe elle-même - une technique connue sous le nom de métaclasses - et contrôler ce newqui fonctionne réellement) mais ce n'est pas ainsi que C # (ou Java, ou C ++) fonctionne.
Donal Fellows

Réponses:

12

J'opterais pour une variante de votre deuxième option qui vous permet de supprimer progressivement l'ancien nom, trop générique 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) { ... }
}

Le seul inconvénient ici est que la nouvelle classe de base ne peut pas avoir le nom le plus évident, car ce nom a déjà été revendiqué pour la classe d'origine et doit rester comme ça pour une compatibilité descendante. Toutes les autres classes ont leurs noms descriptifs.

Bart van Ingen Schenau
la source
1
+1, c'est exactement ce qui m'est venu à l'esprit lorsque j'ai lu la question. Bien que j'aime plus l'option 3 de l'OP.
Doc Brown
La classe de base pourrait avoir le nom le plus évident si elle mettait tout le nouveau code dans un nouvel espace de noms. Mais je ne suis pas sûr que ce soit une bonne idée.
svick
La classe de base doit avoir le suffixe "Base". class DataSourceBase
Stephen
6

La meilleure solution sera quelque chose de proche de votre option n ° 3. Conservez le DataSourceprincipalement tel qu'il est maintenant et extrayez uniquement la partie lecteur dans sa propre 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 { ... }
}

De cette façon, vous évitez le code en double et êtes ouvert à d'autres extensions.

nibra
la source
+1, belle option. Je préfère toujours l'option 2, car elle me permet d'accéder aux fonctionnalités spécifiques à XmlDataSource en instanciant explicitement XmlDataSource.
Heinzi