代码改变世界

再谈抽象类和接口

2009-08-19 12:15 Jeffrey Zhao 阅读(...) 评论(...) 编辑 收藏

昨天我质疑了为什么定义RouteBase抽象类,而不是IRoute接口,我谈到对于一个“没有任何实现”的抽象类来说,开发人员应该使用接口。不过在后面的评论中,有朋友给了我启发,让我忽然想到更多的事情。晚上又再次翻了翻《Framework Design Guidelines》之后,打算再谈一些东西,把这个问题讨论地更加清楚一些。

上次谈了接口的好处,那么这次就主要谈一下接口的缺点。

API是会“升级”的。在一个类库的新版本中,往往会对旧有的API进行修改。作为一个公开的的API,应该做到“无痛升级”,也就是说API的演变不应该破坏现有的应用。而类(class)相对于接口(interface)的优势之一,便是在于类适合进行API的演变。

试想,在类库的1.0版本中定义了一个公开的接口,就比如IRoute吧,它其中有两个成员。于是乎开发人员在使用1.0版本的时候,就会实现这个IRoute接口:

public class MyRoute : IRoute
{
    public RouteData GetRouteData(HttpContextBase httpContext) { ... }

    public VirtualPathData GetVirtualPath(
        RequestContext requestContext, RouteValueDictionary values) { ... }
}

于是到了2.0版本中,开发人员发现,IRoute接口不够用,需要加入新的成员——但是不行,因为IRoute在1.0中已经公开了,这意味着如果向IRoute中添加新的成员,就可能会破坏外部已经使用IRoute接口的实现(如上面的MyRoute)。因此,一旦公开接口发布之后,它就不能被修改了。进而,这一点又可以引出了接口的一个设计准则:接口的职责应该尤其单一。例如.NET框架中的IComparable,IEnumerable等接口。因为如果您设计了一个接口,其中包含了许多成员,那么在新版本中这个接口则更有可能需要补充新的功能……但是接口又不可以修改,这该怎么办呢?

的确是进退两难的情况。在《Framework Design Guildlines》里提出了一些没办法时的办法,但书中也不得坦诚道,这些做法都不是好办法。事实上真没有什么好办法。

如果是“类”的话,问题就相对好办多了,因为我们可以向类中添加新的成员——只要这个新成员不是abstract的,就不会破坏外部已经出现的依赖。不过加上之后,API设计是否合理,语义是否清晰,就是另一回事情了。API设计不仅仅是技术活,每个举动都是需要推敲的。因为一旦发布,就没法删除了。

因此在这里再次感谢那位匿名朋友在评论中的提醒,他指出RouteBase可能是为了“预留”而实现成抽象类的。

说起语义和协议,可能就会涉及到接口的另一个特点,或者说也是个“缺陷”,那就是协议并不明确。接口定义了成员,也就是限制了实现这个接口的“外观”,但是对于“内在”是做不了任何限制的。例如,我们可以让一个方法抛出NotImplementedException,这便是“外强中干”的典型。还有一种情况,就比如IList<T>接口:

public interface IList<T>
{
    void Add(T item);
    int Count { get; }

    ...
}

IList<T>接口只是限制了接口的外部表现:一个接受T类型的Add方法,还有表示元素数量的Count。但是在协议层面上,我们是无法限制这两个成员的关系的。例如,Add方法调用过后,Count肯定会增加1吗?此外还有,Add方法是不是线程安全的?仅通过接口都是不得而知的。

如果是抽象类,我们可以实现的东西就多了。例如,我们可以实现这样的一个抽象类:

public abstract class ThreadSafeListBase<T>
{
    private ReaderWriterLockSlim m_rwLock = new ReaderWriterLockSlim();

    public int Count { get; private set; }

    public void Add(T item)
    {
        this.m_rwLock.EnterWriteLock();
        try
        {
            this.Count++;
            this.AddCore(item);
        }
        finally
        {
            this.m_rwLock.ExitWriteLock();
        }
    }

    protected abstract void AddCore(T item);

    ...
}

这么做,就在一定程度上对实现内容进行了约束。但是很明显约束是不可能彻底的(如AddCore也可能抛出NotImplementedException),最彻底的约束便是一个sealed class——但这个很明显,就已经不是抽象了。

接口的确有优势(可以让类来实现多个接口,且struct实现接口不能继承一个类),但是在这篇文章中我们也看到接口也是有一定缺陷的。设计需要平衡,没有什么东西是最好的,也没有什么东西总是最合适的。

不过比较奇怪的是,有(不止一个)朋友回复说,使用抽象类是为了使用扩展方法。我从来没有看到过某个资料说接口和扩展方法有任何冲突,事实上我们也可以为接口定义扩展方法。由于一个类可以实现多个接口,而接口又可以实现扩展方法,这似乎也又有了“多继承”的意味。