Ich hätte eine Factory-Methode anstelle eines Konstruktors verwenden sollen. Kann ich das ändern und trotzdem abwärtskompatibel sein?

15

Das Problem

Nehmen wir an, ich habe eine Klasse namens, DataSourcedie eine ReadDataMethode (und vielleicht auch andere, aber lassen Sie uns die Dinge einfach halten) zum Lesen von Daten aus einer .mdbDatei bereitstellt :

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

Ein paar Jahre später entscheide ich mich, dass ich .xmlDateien zusätzlich zu .mdbDateien als Datenquellen unterstützen möchte . Die Implementierung zum "Lesen von Daten" ist für .xmlund .mdbDateien ganz anders ; Wenn ich das System von Grund auf neu entwerfen würde, würde ich es folgendermaßen definieren:

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

Großartig, eine perfekte Implementierung des Factory-Methodenmusters. Leider DataSourcebefindet sich in einer Bibliothek ein Refactoring und der Code würde so alle vorhandenen Aufrufe von brechen

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

in den verschiedenen Clients mit der Bibliothek. Weh mir, warum habe ich überhaupt keine Fabrikmethode angewendet?


Lösungen

Das sind die Lösungen, die ich finden könnte:

  1. Lassen Sie den DataSource-Konstruktor einen Subtyp ( MdbDataSourceoder XmlDataSource) zurückgeben. Das würde alle meine Probleme lösen. Leider unterstützt C # das nicht.

  2. Verwenden Sie unterschiedliche Namen:

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

    Das habe ich letztendlich verwendet, da der Code abwärtskompatibel bleibt (dh Aufrufe, die new DataSource("myFile.mdb")noch funktionieren). Nachteil: Die Namen sind nicht so aussagekräftig, wie sie sein sollten.

  3. Erstellen Sie DataSourceeinen "Wrapper" für die eigentliche Implementierung:

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

    Nachteil: Jede Datenquellenmethode (wie z. B. ReadData) muss per Boilerplate-Code geroutet werden. Ich mag keinen Boilerplate-Code. Es ist überflüssig und überfüllt den Code.

Gibt es eine elegante Lösung, die ich verpasst habe?

Heinzi
quelle
5
Können Sie Ihr Problem mit # 3 etwas genauer erläutern? Das kommt mir elegant vor. (Oder so elegant wie möglich, während die Abwärtskompatibilität erhalten bleibt.)
pdr
Ich würde eine Schnittstelle definieren, die die API veröffentlicht, und dann die vorhandene Methode wiederverwenden, indem eine Factory einen Wrapper um Ihren alten Code erstellt und eine neue, indem die Factory entsprechende Instanzen von Klassen erstellt, die die Schnittstelle implementieren.
Thomas
@pdr: 1. Jede Änderung der Methodensignaturen muss an einer weiteren Stelle vorgenommen werden. 2. Ich kann entweder die Impl-Klassen inner und privat machen, was unpraktisch ist, wenn ein Client auf bestimmte Funktionen zugreifen möchte, die z. B. nur in einer XML-Datenquelle verfügbar sind. Oder ich kann sie öffentlich machen, was bedeutet, dass die Kunden jetzt zwei verschiedene Möglichkeiten haben, dasselbe zu tun.
Heinzi
2
@ Heinzi: Ich bevorzuge Option 3. Dies ist das Standardmuster "Fassade". Sie sollten überprüfen, ob Sie wirklich jede Datenquellenmethode an die Implementierung delegieren müssen oder nur einige. Vielleicht gibt es noch generischen Code, der in "DataSource" verbleibt?
Doc Brown
Es ist eine Schande, dass dies newkeine Methode des Klassenobjekts ist (sodass Sie die Klasse selbst - eine als Metaklassen bezeichnete Technik - in Unterklassen unterteilen und steuern können, was newtatsächlich geschieht), aber so funktioniert C # (oder Java oder C ++) nicht.
Donal Fellows

Antworten:

12

Ich würde mich für eine Variante zu Ihrer zweiten Option entscheiden, mit der Sie den alten, zu generischen Namen auslaufen lassen können 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) { ... }
}

Der einzige Nachteil hierbei ist, dass die neue Basisklasse nicht den offensichtlichsten Namen haben kann, da dieser Name bereits für die ursprüngliche Klasse beansprucht wurde und aus Gründen der Abwärtskompatibilität so bleiben muss. Alle anderen Klassen haben ihre beschreibenden Namen.

Bart van Ingen Schenau
quelle
1
+1, genau das kam mir in den Sinn, als ich die Frage las. Obwohl ich Option 3 des OP mehr mag.
Doc Brown
Die Basisklasse könnte den offensichtlichsten Namen haben, wenn der gesamte neue Code in einem neuen Namespace abgelegt wird. Aber ich bin mir nicht sicher, ob das eine gute Idee ist.
Svick
Die Basisklasse sollte das Suffix "Base" haben. Klasse DataSourceBase
Stephen
6

Die beste Lösung ist etwas, das Ihrer Option Nr. 3 nahe kommt. Behalten Sie das DataSourceMeiste wie es jetzt ist und extrahieren Sie nur den Leserteil in seine eigene Klasse.

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

Auf diese Weise vermeiden Sie doppelten Code und sind offen für weitere Erweiterungen.

Nibra
quelle
+1, nette Option. Ich bevorzuge jedoch immer noch Option 2, da ich durch explizites Instanziieren auf die XmlDataSource-spezifische Funktionalität zugreifen kann XmlDataSource.
Heinzi