译文:伯仲之争--接口与抽象类【节选】

原文地址: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()方法的调用次数,而其实很显然其他方法也肯定有被调用,但没法统计了。如果换做是完全重新实现所有接口,我们就可以避免这种问题,因为每个方法的实现,此刻由我们自己做主。

    所以,当我们自己作为一个组件/类库的开发提供者的时候,我们要尽可能的让组件内容完全正确,并且要允许用户能够重新重写任何他们需要的内容——有的时候你也免不了犯错,抑或他们确实有什么特殊的需求需要特殊处理。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,039评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,223评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,916评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,009评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,030评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,011评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,934评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,754评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,202评论 1 309
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,433评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,590评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,321评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,917评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,568评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,738评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,583评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,482评论 2 352

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 172,056评论 25 707
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,650评论 18 139
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,621评论 18 399
  • 《将夜》 ——作者:陆陈蔚 将夜归来的鸽群 纷纷栖落在对面楼顶 它们的辛劳我听不见 翅膀鼓起的尘灰我呛不...
    陆陈蔚阅读 312评论 0 1
  • 就跟写作文一样,先写正文再写标题吧。是这样,我有一个一直联系的男性朋友,不知道算不算男闺蜜那种。突然说,双...
    是我桃子呀阅读 198评论 0 0