Я должен был использовать фабричный метод вместо конструктора. Могу ли я изменить это и при этом быть обратно совместимым?

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) должен маршрутизироваться с помощью стандартного кода. Мне не нравится шаблонный код. Это избыточно и загромождает код.

Есть ли какое-нибудь элегантное решение, которое я пропустил?

Heinzi
источник
5
Можете ли вы объяснить свою проблему с # 3 более подробно? Это кажется элегантным для меня. (Или настолько элегантно, насколько это возможно при сохранении обратной совместимости.)
pdr
Я бы определил интерфейс, публикующий API, а затем повторно использовал бы существующий метод, создав фабрику, создающую оболочку вокруг вашего старого кода, а новую - фабрику, создавшую соответствующие экземпляры классов, реализующих интерфейс.
Томас
@pdr: 1. Каждое изменение в сигнатурах методов должно быть сделано еще в одном месте. 2. Я могу сделать классы Impl внутренними и закрытыми, что неудобно, если клиент хочет получить доступ к конкретным функциям, доступным только, например, в источнике данных Xml. Или я могу сделать их публичными, что означает, что у клиентов теперь есть два разных способа сделать одно и то же.
Хайнци
2
@Heinzi: я предпочитаю вариант 3. Это стандартный шаблон «Фасад». Вы должны проверить, действительно ли вам нужно делегировать каждый метод источника данных реализации или только некоторые. Возможно, есть еще какой-то общий код, который остается в «DataSource»?
Док Браун
Это позор, который newне является методом объекта класса (так что вы можете создать подкласс самого класса - метод, известный как метаклассы - и контролировать то, что на newсамом деле делает), но это не так, как работает C # (или Java, или C ++).
Donal Fellows

Ответы:

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

Таким образом, вы избегаете дублирования кода и открыты для дальнейших расширений.

nibra
источник
+1, хороший вариант. Я все еще предпочитаю вариант 2, поскольку он позволяет мне получить доступ к функциональности, специфичной для XmlDataSource, путем явного создания экземпляра XmlDataSource.
Хайнци