原文地址:http://www.codeproject.com/Articles/814367/Interfaces-and-Abstract-Classes
【译者注】
网上有很多关于这两弟兄掐架比高低的文章,但个人认为这篇文章解析非常具体,而且提到了很多人没有分析到的点,特别是从接口的“使用者”,“开发者”不同的角度来分析接口与抽象类的差异,实在精彩!原文很长,就只能节选一部分了。
前提介绍
我经常看见有人问关于接口与抽象类的区别,而且大部分的回答都只是在关注他们各自的外在特点,并没有对如何使用进行进一步解释。
比如,大部分的解释像这样:
1. 抽象类有具体实现,而接口却没有;
2. 在.NET中我们没办法进行多继承,但可以同时实现多个接口;
3. 接口就是一个协议,而抽象类的内容则远远不止于此(这句话说对我来说简直毫无意义,但是却是非常常见的解释)
除了这些外,其实还有很多答案都和这些大同小异。换句话说,不管这些答案各自到底正确与否,总之是没有完全答到点上(当然有时候问题也问得不够明确)。
那到底我们该什么时候用接口,什么时候用抽象类?
只在乎传参调用 -- 接口是个好东西
假设你正在开发一个系统,而且这个系统目前需要有日志记录功能。至于这个日志记录是否与整体业务逻辑有关,这个我不会去在意,我准备和大家探讨的,是这个日志的不同记录方式:有的写成文件,有的写到数据库里,有的则有其他更多的记录方式等等。
这样一来,你可以很容易想到它大概应该是这个样子:
logger.Log("An error happened in module X.");
logger.ConcatLog("An error happened in module ", moduleName, ".");
logger.FormatLog("An error happened in module {0}.", moduleName);
如果各位觉得还不够清楚,我进一步说明一下:第一个Log语句只接受了一个String参数,第二个Log接收的是一个Object数组,第三个Log接受一个String类型的分隔符(即{0})以及一个带有分隔符的Object数组。
那么要写出这些方法对应的接口就容易了:
interface ILogger
{
void Log(string message);
void ConcatLog(params object[] messageParts);
void FormatLog(string format, params object[] parameters);
}
请注意,这个时候你不用在意他们到底是怎么实现的,你需要的只是一个能够这样完成方法调用的接口而已。如果你觉得还需要其他的Log执行方法,很简单,加到这个接口上就行了。这就是完全体现了“我只管调用方法,我无需在乎具体实现”。
抽象类--我能给你提供一个基础的实现方案
在我看来,抽象类是在如下情形下才有价值:当你发现一个接口中带有一个“几乎不变”而且会被之后的实现类“反复使用”的方法的时候,你便有必要把这个方法给具体实现出来,而这样的一个接口,也就进而转变成了一个抽象类。
继续以前面的例子为例,如果将Log日志记录到数据库,或是文本或者甚至通过TCP/IP发送一封信件,那么我们可以假设ConcatLog()与FormatLog()方法会实现成这样:
public void ConcatLog(params object[] messageParts)
{
string message = string.Concat(messageParts);
Log(message);
}
public void FormatLog(string format, params object[] parameters)
{
string message = string.Format(format, parameters);
Log(message);
}
所以,可以建立一个抽象类来实现上面两个方法,但继续保持Log()方法为抽象方法。这样一来,这个抽象类已经实现了3个方法中的2个,当进一步开发FileLogger,DatabaseLogger 和 TcpIpLogger 的时候,开发人员可以继承这个抽象类进行开发,避免了重复的代码。
干嘛不一开始就使用抽象类?
好了,看到我的这个例子,我想很多人会问:“为什么不一开始就使用抽象类,免得还采用这么一个无用的接口?”
少年请淡定,谁敢保证说这个接口是“无用的”?
再打个比方,假定我之前的那些方法实现并不完全正确——它并没有校验传入参数的合法性,一旦ConcatLog()方法调用的参数是NULL,将会出现一个ArgumentNullException,同样的问题也会出现在theFormatLog()方法上。如果我们是这个代码的原开发者,我们有权利来修正代码,那自然好办;但如果这个存在问题的抽象类是来自于一个已经编译好的库,而你自己只是一个可怜巴巴的使用者,这时候你怎么办??(全文最警醒之言 ——译者注)
如果一个类库的开发者更多的使用接口,将抽象类分别单独存在,这样我们便可以方便的重新实现我们需要的方法,比如可以加入我们想要的错误返回码等等。
另外一个例子——NullLogger会如何呢?NullLogger,其实就是一个不会做任何事情的Log工具。
你可以通过抽象类来进一步实现这个NullLogger,但是三个方法中有两个方法是在完全浪费时间:调用方法会免不了执行格式化、拼接参数等操作,但是这些内容是完全不会有任何显示的——因为他是NullLogger,本来就是空数据。所以NullLogger即使实现了所有的方法,其实都是毫无作为,浪费时间。像这样一种“无为”的做法非常普遍,主要用来避免对NULL的检查,甚至目前都已经产生了一种设计模式:Null Object Pattern (空对象模式)。
最后,依然假定我们是这个代码的开发者,我们提供给其他程序员使用,那么我们还很难知道其他程序员对于这些方法的统计需求到底是怎样的。假如有人觉得这些Logger不能仅仅用来输出日志,还要同时统计每个log方法的调用次数。如果我们一开始使用的是抽象类的设计方案,那么可以看到具体的实现中总是导向Log()方法,我们在进一步的扩展里,只能去写代码统计Log()方法的调用次数,而其实很显然其他方法也肯定有被调用,但没法统计了。如果换做是完全重新实现所有接口,我们就可以避免这种问题,因为每个方法的实现,此刻由我们自己做主。
所以,当我们自己作为一个组件/类库的开发提供者的时候,我们要尽可能的让组件内容完全正确,并且要允许用户能够重新重写任何他们需要的内容——有的时候你也免不了犯错,抑或他们确实有什么特殊的需求需要特殊处理。
网友评论
冲突的是 自己对业务的不合理抽象
不过确实欣赏这篇文章的更多还是让我们不要太从一个有“上帝权限”的开发者来看,而是你作为一个开源开发者,或者API提供这来看,这样一来,接口这种“合同”与抽象类这种“协助办事”区别就明显了。
在更抽象的层次上,接口可以看作了是定义了一种行为范式——你做什么,我做什么,你应该做什么,我应该做什么。而类除了定位了行为范式,还在一定程度上给出了具体的行为——你具体怎么做的,我具体怎么做的。
这就好比两个公司谈合同,接口是谈定了双方的责任和义务,而抽象类是告诉对方我具体帮你做什么具体内容。