BT

如何利用碎片时间提升技术认知与能力? 点击获取答案

讨论:所有的成员都应该是virtual的吗?

| 作者 赵劼 关注 4 他的粉丝 发布于 2009年9月2日. 估计阅读时间: 11 分钟 | 都知道硅谷人工智能做的好,你知道 硅谷的运维技术 也值得参考吗?QCon上海带你探索其中的奥义

在Java语言中,所有的方法在默认情况下都可以被子类override,而C#与之相反,必须显式地将成员标记为virtual。那么哪种做法更合理一些呢?最近,这个话题再次引发了一场讨论。

此次讨论由Ward Bell引起,他在翻阅了Roy Osherove的新书《The Art of Unit Testing》之后认为,他不同意Roy给出的建议“将所有的成员默认加上virtual修饰”。为此,他还独立开篇阐述了他的观点。在文章的一开始,他说明了virtual成员的优势:

(将所有成员标记为virtual)是Roy在“为可测试性(testability)设计”附录中提出的第一条建议。这的确非常方便,这可以说是在测试中为一个类创建stub或mock最简单的方式了……如果每个方法都是virtual的,那么任何成员都可以在测试环境中替换实现……它也为各种mock框架提供了便利条件,mock一个封闭的具体类要困难得多……一个包含virtual方法的类也容易为它创建动态代理,就像Ayende展示的为POCO对象注入INotifyPropertyChanged接口一样。

不过,Ward认为将所有的方法标记为virtual违反了“里氏替换原则(Liskov Substitution Principle,LSP)”,它是Bob Martin提出的SOLID设计原则之一,它要求“子类中的方法不能违反父类中方法制定的基础保证,它不能强化方法的前置条件,也不能削弱方法的后置条件”。Bob Martion认为,违反了LSP,也有极大可能违反了SOLID中的另一个原则“开闭原则(Open Close Principle,OCP)”,它要求系统“对扩展开放,对修改关闭”。Ward谈到:

把每个成员标记为virtual相当于盲目地公开了一个类,此时没有什么是“对修改关闭”了。virutal关键字的意义是对外部声称“这是我的扩展点”,每个方法都是virtual时,每个方法都可以被改变了。

Martin(即Bob Martin)认为:“接受妥协而不是坚持完美是工程上的权衡,但是LSP无论如何不应该有丝毫违反”,“LSP保证了子类可以在任何父类工作的环境中使用,这控制了系统的复杂程度,如果它被破坏,那么我们必须单独考虑每个子类”。

当我们将每个成员设为virtual之后,就像外部开启了一扇麻烦之门,因为我们放弃了这一保证。

同时Ward举了一个电梯控制程序作为例子:

我的Elevator类有一个Up方法,假设它是virtual的,那么一个需要增加新功能的程序员开发了一个BetterElevator类型,并override了Up方法。

我知道Up方法是让电梯向上的,但是我无法阻止别人重新实现Up方法让电梯下降。可能base.Up()会让电梯门关闭,但是子类的开发人员可能调用base.Up方法的时机太迟了,在门关闭之前电梯就启动了。开发人员可能会替换我的base.Up掩盖了关门的问题,并插入自定义的行为,但是电梯向上的行为又违反了其他一些保证。

由于缺乏细致的思考,我的开发负担提升了。由于这种不假思索的设计,我的代码错误的“开放”导致“关闭”失败。此时我就必须一一检查,手动关闭原本语言自动标为virtual的成员。

Ward指出,在.NET框架设计规范一书中,Krzysztof Cwalina和Brad Abram认为:

virtual成员……会给设计,测试和维护带来成本,因为任何对virtual成员的调用可以被override成为无法预测行为的任意代码……此时就需要更清晰地描述virtual成员的协议,这样设计的编写文档的成本便增加了。

最后Ward谈到:

把自动属性或类似的逻辑较少的方法标记为virtual会更安全一些,使用模板方法设计模式可以控制可扩展性,也对可测试性有帮助。基于接口的设计也不错。

在文章的评论中,Craig Cavalier发表了不同的看法

我希望我的“扩展点”不仅仅是以virtual方法的形式暴露出去的。正如应用程序开发人员希望可以看到一条扩展的“缝”,我更倾向于实现一个接口,而不是继承类,然后override它的virtual方法。接口比virtual方法更容易发现。

最后,我对OCP有不同的看法。我认为“对修改关闭”是指对类的代码关闭修改能力,而不是避免对类的行为作出修改。不过我也同意,把所有方法标记为virtual会让LSP原则变得脆弱。

你的论点似乎是希望保护自己,我认为有更安全的方法可以做到这一点,同时保留所有方法默认为virtual所带来的灵活性。

Mark Nijof则回复道

我认为这样(代替程序员来保护他们自己)的做法是错误的,我们应该教会他们理解并正确使用合适的工具……为什么不定义一些API并注明这些是可以向后兼容,然后声明其他部分并不考虑兼容性呢?你应该在框架中提供清晰的扩展点,不要把会保持的行为,和可能改变的行为混合在一起。可能更好的方法是将其拆分,把不会改变的部分暴露在公开的API中……你也可以提供单元测试,当开发人员想要扩展你的系统时,他们可以通过单元测试来验证重要的行为并没有被打破。

Ward回应道

我并没有想要代替程序员保护他们自己,这可能很“高尚”,但是阻碍一个专家程序员的成本不能被接受。

我限制类的公开面,是为了发布高质量的代码,是为了精心设计出你可以依赖的产品。我是个框架提供者,你基于我的类型进行开发。如果你信任我的话,那么就让我来做。

这也是种封装,我关闭了部分代码,是因为它是一个纯粹的实现。通过限制访问级别,我可以尽可能地划分出我支持的扩展点,以及我自己的,以后可能会修改的实现。

你会公开一个field吗?当然不会,这就是“封装”。封装让你可以自由的改变,而不会影响现有的代码。

关于“默认”virtual……我关闭(seal)一个方法是因为我想保证它会按照我的说法去做事,把它设为virtual会破坏这种保证,让我耗费更多精力去对待它,这本可以避免。我可能会选择将其设为virtual,但这是设计上的考虑,而不是由上层强塞给我的。

这个问题在国内社区也引发了讨论,如微软MVP老赵在博客中认写道

对于一个可“全面扩展”的类型来说,意味着开发人员有更多的自由,进而意味着选择(即使是做同一件事情)。但是选择多,则同样意味着我们需要了解的多,一个不慎可能就会发现没有得到预期的效果。

例如,在继承了ASP.NET的Control类之后,您要改变它输出的内容,您会选择覆盖哪一个方法?看上去Render方法和RenderChildren都可以,如果随便选一个,你的类型看上去没有问题,但是如果别人希望进一步继承你写的类,补充一些实现,那么你的“选择”就会影响到他的结果了。

在.NET中,最容易扩展扩展的抽象元素是什么呢?应该是“接口”。接口中的所有成员都是由实现方提供的,除了成员的签名之外,接口并没有作任何限制。如根据IList接口的隐藏协议,Add方法调用之后,Count必须加一。但是这个协议并无法加诸于实现之上。如果要提供这方面的约束,我们只能公开一部分的扩展点,而不是把所有的职责交给实现方。

最后,为了方便起见,我们常常会对类型中的方法给出重载,其中大部分的重载最终都委托给一个唯一的核心方法。此时那个核心方法应该成为唯一的扩展点,否则的话,用户就需要在三个方法中进行选择性的override,并且要平衡三者的行为。在单元测试需要构造Mock或Stub时,有限制地提供扩展点则意味着“别挑了,就是这个”。 “可测试性”也是设计出来,不是语言或平台自动赋予的。

除了“设计”方面的考量之外,qiaojie还提出virtual之于性能的看法

性能都是从小地方体现出来的。在某些情况下不能忽视virtual带来的额外开销,比方说C#中大量使用的property,本来因为内联的关系property的开销跟直接变量访问是一样的,在关键代码段里大量用property也没问题,但是如果是virtual的话,其性能要比直接变量访问差好多。

在.net的设计原则中,性能肯定不是排第一的,但是.net的设计又是非常实用主义的,所以我们会看到很多设计是在尽可能的兼顾到性能,比方说为了在栈上分配对象而引入struct,甚至不惜牺牲安全性而引入指针。

我们承认virtual和非virtual成员函数都有存在的价值,所以默认是不是virtual只是一个无足轻重的问题,就好比讨论C++的成员函数要不要默认是const的呢?那么这个时候如果要兼顾性能的话,显然应该默认为非virtual的。而java的设计哲学里性能是从来不被做为重要因素来考量的,所以默认为virtual也是可以理解的。

除了上述讨论之外,Oren Eini也为这个话题开辟了新的战场。其中许多人发表了自己的看法,并且其中为数不少独立开篇发表了自己的看法。此外,有人也提到C#语言设计这Anders Hejlsberg在一次访谈中阐述他为什么不希望让所有成为都变成virtual。

您的看法呢?

评价本文

专业度
风格

您好,朋友!

您需要 注册一个InfoQ账号 或者 才能进行评论。在您完成注册后还需要进行一些设置。

获得来自InfoQ的更多体验。

告诉我们您的想法

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

总结的很好 by zou bigqiang

写这样一篇文字需要相当大的阅读量

支持所有的成员可以被重载 by Liu Min

API的设计者无需为API的扩展者担心他的扩展是否可以正常工作,那是别人的工作的内容。
很讨厌微软的这套封闭的思路,欣赏Java设计的这种开放性思维,你永远无法预知别人拿到你的API或者类库以后会干嘛?如果功能不满足别人的需要,别人难道不 可以简单的扩展一下满足自己的需求吗?当然,微软的东西都是收费的,如果没有我的允许你就不可能扩展我的东西,这个就是微软的想法。对比看看Java领域有多少是第三方思考出来的好东西吧,微软扼杀了整个产业圈的创新的积极性。

. by D. Animax

C#上还有new关键字.

Re: 支持所有的成员可以被重载 by Zhu Tony

微软的确在开源等方面做得不是非常好,但个人认为与此问题无关,也不是很赞同这种缺省都是virtual的方式。再说C#中有new关键字。

Re: 支持所有的成员可以被重载 by Jeffrey Zhao

我觉得谈到微软开源不开源上就扯远了。

Re: 支持所有的成员可以被重载 by Liu Min

微软的确在开源等方面做得不是非常好,但个人认为与此问题无关,也不是很赞同这种缺省都是virtual的方式。再说C#中有new关键字。

我的评论那个地方说要微软开源了?我说的只是微软封闭。封闭是指的c#默认不能override,而java默认是可以override。

另外,这个文章讨论的本来就是语言的设计风格,和是否可以做到无关吧。
既然父类已经不允许override了,你使用new有什么意义?这个才是真正的违背API的设计者的初衷!假设Java的API的设计者在某个方法或者成员上声明了final,有谁会没事去想着override那些方法或者成员吗?

Re: 支持所有的成员可以被重载 by Shichao Liu

做功课不仔细...文章讨论的不是非虚方法是否有价值, 以及非虚方法是否有背后的险恶用心. 讨论的是默认那种更合适, 其实是个编码量以及避免出纰漏的问题. 技术问题就事论事,红卫兵的思路不会带来任何好处... 无非虚方法是不可能的, 于性能虚方法没法内联, 于设计虚方法没有确定的行为.

Re: 支持所有的成员可以被重载 by Zhu Tony

懒得说了,一提到微软,就跟上了弦似的。洗洗睡吧。

又见老赵,呵呵 by 没 剑

又见老赵,呵呵

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

9 讨论

登陆InfoQ,与你最关心的话题互动。


找回密码....

Follow

关注你最喜爱的话题和作者

快速浏览网站内你所感兴趣话题的精选内容。

Like

内容自由定制

选择想要阅读的主题和喜爱的作者定制自己的新闻源。

Notifications

获取更新

设置通知机制以获取内容更新对您而言是否重要

BT