我应该使用工厂方法而不是构造函数。我可以更改它并且仍然向后兼容吗?


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构造函数返回一个子类型(MdbDataSourceXmlDataSource)。那会解决我所有的问题。不幸的是,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数据源中可用的特定功能,则这会带来不便。或者我可以将它们公开,这意味着客户现在有两种不同的方式来做同一件事。
Heinzi 2013年

2
@Heinzi:我更喜欢选项3。这是标准的“外观”模式。您应该检查是否真的必须将每个数据源方法委托给实现,还是仅委托给某些实现。也许还有一些通用代码保留在“ DataSource”中?
Doc Brown

遗憾的是,new它不是类对象的方法(因此您可以将类本身子类化(一种称为元类的技术,并控制new实际执行的操作)),但这不是C#(或Java或C ++)的工作方式。
Donal Fellows

Answers:


12

我会为您的第二个选项提供一个变体,该选项允许您逐步淘汰旧的太通用的name 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,这正是我阅读问题时想到的。虽然我更喜欢OP的选项3。
Doc Brown

如果将所有新代码放入新的名称空间,则基类可能具有最明显的名称。但是我不确定这是个好主意。
svick

基类应具有后缀“ Base”。DataSourceBase类
Stephen

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
海因兹
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.