Powinienem był zastosować metodę fabryczną zamiast konstruktora. Czy mogę to zmienić i nadal być kompatybilny wstecz?

15

Problem

Powiedzmy, że mam klasę o nazwie, DataSourcektóra zapewnia ReadDatametodę (i może inne, ale bądźmy prostymi) do odczytu danych z .mdbpliku:

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

Kilka lat później postanawiam, że chcę być w stanie obsługiwać .xmlpliki oprócz .mdbplików jako źródeł danych. Implementacja „odczytu danych” jest zupełnie inna dla plików .xmli .mdb; dlatego gdybym zaprojektował system od zera, zdefiniowałbym go w następujący sposób:

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

Świetna, doskonała implementacja wzorca metody Factory. Niestety, DataSourceznajduje się w bibliotece i przefaktoryzowanie kodu w ten sposób zepsułoby wszystkie istniejące wywołania

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

w różnych klientach korzystających z biblioteki. Biada mi, dlaczego nie użyłem metody fabrycznej?


Rozwiązania

Oto rozwiązania, które mógłbym wymyślić:

  1. Spraw, aby konstruktor DataSource zwrócił podtyp ( MdbDataSourcelub XmlDataSource). To rozwiązałoby wszystkie moje problemy. Niestety C # nie obsługuje tego.

  2. Użyj różnych nazw:

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

    Właśnie tego użyłem, ponieważ utrzymuje on kompatybilność wsteczną (tj. Wywołania do new DataSource("myFile.mdb")pracy). Wada: nazwy nie są tak opisowe, jak powinny.

  3. Zrób DataSource„opakowanie” dla prawdziwej implementacji:

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

    Wada: każda metoda źródła danych (taka jak ReadData) musi być kierowana za pomocą kodu typu „kocioł”. Nie podoba mi się kod płyty. Jest zbędny i zaśmieca kod.

Czy jest jakieś eleganckie rozwiązanie, za którym tęskniłem?

Heinzi
źródło
5
Czy możesz wyjaśnić swój problem z numerem 3 bardziej szczegółowo? Wydaje mi się to eleganckie. (Lub tak elegancki, jak to tylko możliwe, przy jednoczesnym zachowaniu kompatybilności wstecznej).
pdr
Zdefiniowałbym interfejs publikujący interfejs API, a następnie ponownie wykorzystałbym istniejącą metodę, zakładając, że fabryka tworzy opakowanie wokół starego kodu i nowy, poprzez fabrykę tworzącą odpowiednie wystąpienia klas implementujących interfejs.
Thomas
@pdr: 1. Każda zmiana w podpisach metod musi być dokonana w jeszcze jednym miejscu. 2. Mogę uczynić klasy Impl wewnętrznymi i prywatnymi, co jest niewygodne, jeśli klient chce uzyskać dostęp do określonych funkcji dostępnych tylko np. W źródle danych Xml. Albo mogę je upublicznić, co oznacza, że ​​klienci mają teraz dwa różne sposoby robienia tego samego.
Heinzi
2
@Heinzi: Wolę opcję 3. Jest to standardowy wzór „Fasada”. Powinieneś sprawdzić, czy naprawdę musisz przekazać każdą metodę źródła danych implementacji, czy tylko niektóre. Być może nadal istnieje jakiś ogólny kod, który pozostaje w „DataSource”?
Doc Brown
Szkoda, że newnie jest to metoda obiektu klasy (abyś mógł podklasować samą klasę - technikę znaną jako metaklasy - i kontrolować, co newfaktycznie robi), ale nie tak działa C # (lub Java, lub C ++).
Donal Fellows

Odpowiedzi:

12

Wybrałbym wariant drugiej opcji, który pozwala wycofać starą, zbyt ogólną nazwę 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) { ... }
}

Jedyną wadą jest to, że nowa klasa podstawowa nie może mieć najbardziej oczywistej nazwy, ponieważ nazwa ta została już zgłoszona dla oryginalnej klasy i musi pozostać taka dla kompatybilności wstecznej. Wszystkie pozostałe klasy mają swoje opisowe nazwy.

Bart van Ingen Schenau
źródło
1
+1, dokładnie to przychodzi mi na myśl, gdy czytam pytanie. Chociaż bardziej podoba mi się opcja 3 OP.
Doc Brown
Klasa podstawowa może mieć najbardziej oczywistą nazwę, jeśli umieści cały nowy kod w nowej przestrzeni nazw. Ale nie jestem pewien, czy to dobry pomysł.
svick
Klasa podstawowa powinna mieć przyrostek „Base”. klasa DataSourceBase
Stephen
6

Najlepszym rozwiązaniem będzie coś bliskiego Twojej opcji # 3. Zachowaj DataSourceprzeważnie taki, jaki jest teraz i wyodrębnij tylko część czytelnika do własnej klasy.

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

W ten sposób unikniesz powielania kodu i będziesz otwarty na dalsze rozszerzenia.

nibra
źródło
+1, fajna opcja. Nadal wolę opcję 2, ponieważ pozwala mi uzyskać dostęp do funkcji specyficznych dla XmlDataSource poprzez jawne tworzenie instancji XmlDataSource.
Heinzi