Я повинен був використовувати заводський метод замість конструктора. Чи можу я це змінити і все ще бути сумісним назад?


15

Проблема

Скажімо, у мене є клас під назвою, DataSourceякий забезпечує ReadDataметод (а може бути й інші, але давайте будемо робити прості речі) для читання даних з .mdbфайлу:

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

Через кілька років я вирішую, що хочу підтримувати .xmlфайли на додаток до .mdbфайлів як джерел даних. Реалізація для «читання даних» дуже різна для .xmlі .mdbфайлів; таким чином, якби я проектував систему з нуля, я б визначив її так:

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

Чудово, досконала реалізація схеми фабричного методу. На жаль, DataSourceрозташований у бібліотеці, і рефакторинг коду таким чином порушив би всі існуючі виклики

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

у різних клієнтів, що користуються бібліотекою. Горе мені, чому я в першу чергу не застосував заводський метод?


Рішення

Це рішення, які я міг би придумати:

  1. Змусити конструктор DataSource повернути підтип ( MdbDataSourceабо XmlDataSource). Це вирішило б усі мої проблеми. На жаль, C # не підтримує цього.

  2. Використовуйте різні назви:

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

    Це те, що я в кінцевому підсумку використав, оскільки він підтримує код, сумісний із зворотним боком (тобто дзвінки new DataSource("myFile.mdb")все ще працюють). Недолік: Назви не такі описові, як повинні бути.

  3. Зробіть DataSource"обгортку" для реальної реалізації:

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

    Недолік: Кожен метод джерела даних (наприклад ReadData) повинен бути направлений кодовою табличкою. Мені не подобається код котла. Це зайве і захаращує код.

Чи є якесь елегантне рішення, яке я пропустив?


5
Чи можете ви пояснити свою проблему з №3 трохи детальніше? Мені це здається елегантним. (Або настільки елегантний, як ви отримуєте, зберігаючи зворотну сумісність.)
pdr,

Я б визначив інтерфейс, що публікує API, а потім повторно використати існуючий метод, створивши фабрику, яка створить обгортку навколо вашого старого коду та нового, створивши на заводі відповідні екземпляри класів, що реалізують інтерфейс.
Томас

@pdr: 1. Кожна зміна підписів методу повинна бути здійснена ще в одному місці. 2. Я можу зробити класи Impl внутрішніми та приватними, що незручно, якщо клієнт хоче отримати доступ до певних функцій, доступних лише у, наприклад, у джерелі даних Xml. Або я можу оприлюднити їх, а це означає, що клієнти зараз мають два різні способи робити те саме.
Хайнці

2
@Heinzi: Я віддаю перевагу варіант 3. Це стандартний шаблон "Фасад". Ви повинні перевірити, чи дійсно потрібно делегувати кожен метод джерела даних до реалізації, або лише деякі. Можливо, є ще якийсь загальний код, який залишається в "DataSource"?
Док Браун

Прикро, що newце не метод об’єкта класу (щоб ви могли підкласирувати сам клас - техніку, відому як метакласи - і контролювати, що newнасправді робить), але це не так, як працює C # (або Java, або C ++).
Дональні стипендіати

Відповіді:


12

Я б запропонував варіант до вашого другого варіанту, який дозволяє поступово припинити стару, занадто загальну назву 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) { ... }
}

Єдиним недоліком тут є те, що новий базовий клас не може мати найочевиднішу назву, оскільки це ім'я вже було заявлено для початкового класу і воно повинне залишатися таким для зворотної сумісності. Усі інші класи мають свої описові назви.


1
+1, саме це мені спадало на думку, коли я читав питання. Хоча мені більше подобається варіант 3 ОП.
Док Браун

Базовий клас міг би мати найочевиднішу назву, якби помістити весь новий код у нову область імен. Але я не впевнений, що це гарна ідея.
svick

Базовий клас повинен мати суфікс "Base". клас DataSourceBase
Стівен

6

Найкращим рішенням стане щось наближене до вашого варіанту №3. Зберігайте DataSourceздебільшого так, як зараз, і витягайте лише читацьку частину у свій клас.

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

Таким чином, ви уникаєте дублювання коду і відкриті для подальшого розширення.


+1, хороший варіант. Я все ж віддаю перевагу варіант 2, оскільки він дозволяє мені отримати доступ до функцій, характерних для XmlDataSource, шляхом явної інстанції XmlDataSource.
Хайнці
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.