Seharusnya saya menggunakan metode pabrik daripada konstruktor. Bisakah saya mengubahnya dan masih kompatibel ke belakang?

15

Masalah

Katakanlah saya memiliki kelas bernama DataSourceyang menyediakan ReadDatametode (dan mungkin yang lain, tapi mari kita tetap sederhana) untuk membaca data dari .mdbfile:

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

Beberapa tahun kemudian, saya memutuskan bahwa saya ingin dapat mendukung .xmlfile selain .mdbfile sebagai sumber data. Implementasi untuk "membaca data" sangat berbeda untuk .xmldan .mdbfile; jadi, jika saya mendesain sistem dari awal, saya akan mendefinisikannya seperti ini:

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

Hebat, implementasi sempurna dari pola metode Pabrik. Sayangnya, DataSourceterletak di perpustakaan dan refactoring kode seperti ini akan memecah semua panggilan yang ada

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

di berbagai klien menggunakan perpustakaan. Celakalah saya, mengapa saya tidak menggunakan metode pabrik di tempat pertama?


Solusi

Ini adalah solusi yang bisa saya buat:

  1. Buat konstruktor DataSource mengembalikan subtipe ( MdbDataSourceatau XmlDataSource). Itu akan menyelesaikan semua masalah saya. Sayangnya, C # tidak mendukung itu.

  2. Gunakan nama yang berbeda:

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

    Itulah yang akhirnya saya gunakan karena menjaga kode yang kompatibel ke belakang (yaitu panggilan untuk new DataSource("myFile.mdb")tetap bekerja). Kekurangan: Nama tidak deskriptif seperti yang seharusnya.

  3. Buat DataSource"pembungkus" untuk implementasi nyata:

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

    Kelemahan: Setiap metode sumber data (seperti ReadData) harus dirutekan oleh kode boilerplate. Saya tidak suka kode boilerplate. Itu berlebihan dan mengacaukan kode.

Apakah ada solusi elegan yang saya lewatkan?

Heinzi
sumber
5
Bisakah Anda menjelaskan masalah Anda dengan # 3 sedikit lebih detail? Bagi saya itu tampak elegan. (Atau sebagai elegan Anda mendapatkan tetap menjaga kompatibilitas mundur.)
pdr
Saya akan mendefinisikan antarmuka yang menerbitkan API, dan kemudian menggunakan kembali metode yang ada dengan meminta pabrik membuat wrapper di sekitar kode lama Anda dan yang baru dengan meminta pabrik membuat instance kelas yang sesuai untuk mengimplementasikan antarmuka.
Thomas
@ pdr: 1. Setiap perubahan pada tanda tangan metode harus dilakukan di satu tempat lagi. 2. Saya dapat membuat kelas Impl dalam dan privat, yang tidak nyaman jika klien ingin mengakses fungsionalitas khusus yang hanya tersedia di, misalnya, sumber data Xml. Atau saya dapat mempublikasikannya, yang berarti bahwa klien sekarang memiliki dua cara berbeda untuk melakukan hal yang sama.
Heinzi
2
@Heinzi: Saya lebih suka opsi 3. Ini adalah pola standar "Fasad". Anda harus memeriksa apakah Anda benar-benar harus mendelegasikan setiap metode sumber data ke implementasi, atau hanya beberapa. Mungkin masih ada beberapa kode generik yang tetap di "DataSource"?
Doc Brown
Ini memalukan yang newbukan metode objek kelas (sehingga Anda bisa subkelas kelas itu sendiri - suatu teknik yang dikenal sebagai metaclasses - dan mengontrol apa yang newsebenarnya) tetapi itu bukan cara kerja C # (atau Java, atau C ++).
Donal Fellows

Jawaban:

12

Saya akan memilih varian untuk opsi kedua Anda yang memungkinkan Anda menghapus nama lama, terlalu umum 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) { ... }
}

Satu-satunya kelemahan di sini adalah bahwa kelas dasar baru tidak dapat memiliki nama yang paling jelas, karena nama itu sudah diklaim untuk kelas asli dan perlu tetap seperti itu untuk kompatibilitas ke belakang. Semua kelas lain memiliki nama deskriptif.

Bart van Ingen Schenau
sumber
1
+1, itulah yang muncul di benak saya ketika saya membaca pertanyaan. Meskipun saya lebih suka opsi 3 dari OP.
Doc Brown
Kelas dasar bisa memiliki nama yang paling jelas jika memasukkan semua kode baru ke ruang nama baru. Tapi saya tidak yakin itu ide yang bagus.
svick
Kelas dasar harus memiliki akhiran "Basis". class DataSourceBase
Stephen
6

Solusi terbaik akan menjadi sesuatu yang dekat dengan pilihan Anda # 3. Simpan DataSourcesebagian besar seperti sekarang, dan ekstrak hanya bagian pembaca ke dalam kelasnya sendiri.

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

Dengan cara ini, Anda menghindari kode duplikat, dan terbuka untuk ekstensi lebih lanjut.

nibra
sumber
+1, opsi yang bagus. Saya masih lebih suka opsi 2, karena itu memungkinkan saya untuk mengakses fungsionalitas spesifik XmlDataSource dengan secara eksplisit instantiating XmlDataSource.
Heinzi