Avrei dovuto usare un metodo di fabbrica anziché un costruttore. Posso cambiarlo ed essere ancora compatibile con le versioni precedenti?


15

Il problema

Diciamo che ho una classe chiamata DataSourceche fornisce un ReadDatametodo (e forse altri, ma manteniamo le cose semplici) per leggere i dati da un .mdbfile:

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

Qualche anno dopo, decido di voler supportare i .xmlfile oltre ai .mdbfile come origini dati. L'implementazione per "leggere i dati" è abbastanza diversa per .xmle .mdbfile; quindi, se dovessi progettare il sistema da zero, lo definirei così:

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

Fantastico, una perfetta implementazione del modello del metodo Factory. Sfortunatamente, DataSourcesi trova in una libreria e il refactoring del codice in questo modo interromperebbe tutte le chiamate esistenti

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

nei vari client usando la libreria. Guai a me, perché non ho usato un metodo di fabbrica in primo luogo?


soluzioni

Queste sono le soluzioni che potrei trovare:

  1. Fare in modo che il costruttore DataSource restituisca un sottotipo ( MdbDataSourceo XmlDataSource). Ciò risolverebbe tutti i miei problemi. Sfortunatamente, C # non lo supporta.

  2. Usa nomi diversi:

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

    Questo è quello che ho finito per usare poiché mantiene il codice compatibile con le versioni precedenti (ovvero le chiamate new DataSource("myFile.mdb")funzionano ancora). Svantaggio: i nomi non sono così descrittivi come dovrebbero essere.

  3. Crea DataSourceun "wrapper" per la vera implementazione:

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

    Svantaggio: ogni metodo di origine dei dati (come ReadData) deve essere instradato dal codice di boilerplate. Non mi piace il codice boilerplate. È ridondante e ingombra il codice.

C'è qualche soluzione elegante che mi è sfuggita?


5
Puoi spiegare il tuo problema con # 3 in modo un po 'più dettagliato? Mi sembra elegante. (O elegante quanto ottieni, pur mantenendo la retrocompatibilità.)
pdr

Definirei un'interfaccia che pubblica l'API e quindi riutilizzerei il metodo esistente facendo in modo che una factory crei un wrapper attorno al tuo vecchio codice e a quelli nuovi facendo sì che la factory crei le corrispondenti istanze di classi che implementano l'interfaccia.
Thomas,

@pdr: 1. Ogni modifica alle firme del metodo deve essere effettuata in un altro posto. 2. Posso rendere le classi Impl interne e private, il che è scomodo se un cliente desidera accedere a funzionalità specifiche disponibili solo in, ad esempio, un'origine dati Xml. Oppure posso renderli pubblici, il che significa che i clienti ora hanno due modi diversi di fare la stessa cosa.
Heinzi,

2
@Heinzi: preferisco l'opzione 3. Questo è il modello "Facciata" standard. È necessario verificare se è necessario delegare tutti i metodi dell'origine dati all'implementazione o solo alcuni. Forse c'è ancora del codice generico che rimane in "DataSource"?
Doc Brown,

È un peccato che newnon sia un metodo dell'oggetto class (in modo da poter sottoclassare la classe stessa - una tecnica nota come metaclassi - e controllare ciò che neweffettivamente fa) ma non è così che funziona C # (o Java o C ++).
Donal Fellows,

Risposte:


12

Vorrei fare una variante alla tua seconda opzione che ti permetta di eliminare gradualmente il vecchio, troppo generico, nome 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) { ... }
}

L'unico inconveniente qui è che la nuova classe base non può avere il nome più ovvio, perché quel nome era già stato rivendicato per la classe originale e deve rimanere tale per la compatibilità con le versioni precedenti. Tutte le altre classi hanno i loro nomi descrittivi.


1
+1, è esattamente quello che mi è venuto in mente quando ho letto la domanda. Anche se mi piace di più l'opzione 3 dell'OP.
Doc Brown,

La classe base potrebbe avere il nome più ovvio se si inserisse tutto il nuovo codice in un nuovo spazio dei nomi. Ma non sono sicuro che sia una buona idea.
svick,

La classe base dovrebbe avere il suffisso "Base". class DataSourceBase
Stephen

6

La soluzione migliore sarà qualcosa vicino alla tua opzione # 3. Mantieni il DataSourcepiù com'è adesso ed estrai solo la parte del lettore nella sua 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 { ... }
}

In questo modo, eviti il ​​codice duplicato e sei aperto a ulteriori estensioni.


+1, bella opzione. Preferisco comunque l'opzione 2, poiché mi consente di accedere alla funzionalità specifica di XmlDataSource istanziando esplicitamente XmlDataSource.
Heinzi,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.