冒号和他的学生们
27.接口服务
律己宜严,待人宜宽 ——《洪应明·菜根谭》
叹号幡然反省:“以前我们做OOP编程时,总是专注于如何利用其他类来解决问题,而较少考虑自己设计的类对其他类的影响。”
引号翻开以前的笔记:“前面提过,OOP的世界是民主制的,所有对象都是独立而平等的公民,有权利寻求服务,也有义务提供服务。看来我们是光惦着权利而忘了义务了。”
冒号继而提出:“作为服务的提供者,最重要的是讲诚信。首先,服务要有可靠性,不能阳奉阴违——即接口必须履行它的承诺;其次,服务要有稳定性,不能朝令夕改——即接口一经公开,不得随意变更。”
句号迅即领悟:“从抽象的角度看,服务的可靠性保证了规范抽象,服务的稳定性保证了数据抽象。”
“孺子可教也!”冒号喜赞道,“相比而言,前者更为重要,但遗憾的是,只有后者才有法律保障——如果接口被废弃或其签名发生变化,固然会牵连客户,至少还可通过编译器来发现和修改;而规范只是语义上的契约,没有语法上的约束,不在编译器的监管范围之内。”
引号插言:“编译器管不着,那只有靠单元测试了。”
“这正是单元测试的主要目的。”冒号很认同,“此外,高质量的服务还要有纯粹性和完备性。Unix有一个哲学:‘一个程序只做一件事,但要做好’。用在OOP上,则是:‘一个类只提供一套服务,但要完善’。譬如,同为手机,老式的大哥大提供的服务是纯粹的,现代的智能手机则不是——除了打电话,还能摄像、听音乐、打游戏、上网等等,完全是手机与掌上电脑的结合体。又如,同为通讯工具,手机提供的服务是完备的,而BP机提供的服务是不完备的——只能接收信息,不能发送信息。”
叹号摇头晃脑:“提供的服务过多则不纯粹,过少则不完备。如此设计出的类是不是要达到‘增一分则肥,减一分则瘦’的美人标准啊?”
“编程毕竟是门实践活儿,完美无缺的设计如梦中佳人,可以追求却难以企及。”冒号笑了笑,“其实关键不在于服务数量的多寡,而在于服务的一致性和关联性。连贯一致的服务是良好的抽象与封装的结果,同时也是‘高聚合、低耦合’的保证。”
“作一个服务的提供者真不容易啊。”问号叹道,“那么,作为服务的享受者有什么讲究吗?”
逗号鼻腔里发出共鸣声:“哈,享受服务还需要讲究吗?”
“当然有。”冒号断然道,“作为服务的享受者,最重要的是讲规矩。享受人家的服务,自然得按人家的规则,否则服务将得不到保障。比如,你可以在超市的货架上任意选取商品,但不能偷偷溜进货舱去取。”
逗号辩道:“可货舱想进也进不去啊。正如合适的封装,是禁止客人进入私有接口的。”
冒号作一引喻:“我们不妨这么假设,货舱的正门挂着‘非工作人员莫入’的牌子。但是你偶然发现,通往洗手间的过道尽头正好是货舱的后门,既没有上锁,也没有挂牌。请问你会不会大摇大摆地走进去?”
逗号哑口无言。
冒号循循善诱:“超市的工作人员也许不该为图方便而开放后门,但那是管理者的事,作为客户显然不应乘虚而入。商品从货舱到货架之前可能会有装箱、拆箱、条码打印、条码扫描等操作,客户直接从货舱拿货无疑将破坏这种程序,于人于己皆无益处。同样地,作为他人代码的客户,就应按他人所设计的方式去重用,如此才能保证预期的效果。至于他人代码是否有效杜绝了一切可能的漏洞,那是监管软件质量的负责人的职责。”
引号表示理解:“这就好比客户购买了一款产品,却不按使用说明书进行操作,由此而引起的一切后果,厂家概不负责。”
“就是这个理儿。”冒号轻锤桌面,“当然事物是一分为二的。生活中有一个司空见惯的现象:许多行人跨越护栏、横穿马路。一方面,行人应该遵守交通规则,不应破坏道路的‘封装性’。另一方面,有些交通设计者没有‘以人为本’的客户意识,为行人提供的斑马线、天桥或隧道之间相距过远。从客观上说,不够完备的服务是导致行人违规的一大诱因。”
此言显见深得人心——几乎人人都当过道路封装的破坏分子。
冒号接着问:“提个问题:当你们在使用一个类或其中的某个方法时,对其用法存疑,即使阅读注释文档也无济于事,怎么办?”
叹号顺嘴说道:“看源代码呗。”
“看源代码是一种很好的学习和借鉴他人的方式,但不宜作为用法参考。”冒号否定道,“且不说源代码有可能无法获取,既便能够,从中提炼出的用法也不一定可靠,更何况具体实现随时可能变化。再打个比方,如果你不清楚如何设置一个闹钟,应该去看看说明书。如果说明书仍不解决问题,最好询问厂家,而不是揭开闹钟的后盖去研究它的运行机制,即使你真是个钟表行家。”
“所以应该直接咨询代码的作者。”逗号发现,过早抢答往往会掉入老冒的陷阱。这回学乖了,等叹号落坑后才胸有成竹地应招。
“方向正确!”冒号肯定后再次考问,“对方应以何种方式回答?”
“可以口头,也可以书面啊。”逗号答毕,隐隐觉得还是着了道。
果然,冒号摇摇头:“正确的做法是,对方应通过改进并提交的文档来解释。该过程可多次循环,直至问题解决。只有这样,主客双方的代码维护者——包括当前的和将来的——才能真正受益。”
问号深究:“但假如无法联系到原作者呢?比如包括JDK库在内的软件?”
冒号回答:“除了盗版的商业软件,都应该能联系到原作者。当然,如果与作者使用的不是同一源码控制库,上述做法也是可以变通的。好在无论是JDK库,还是正规的第三方软件,文档注释应该都足够清晰,许多还会提供示例代码。如果这些还不能让你明白,要么是该软件不值信赖,也就没有重用的价值;要么是你自身的理解问题,只有求助有识之士了。”
句号体会到:“由此可见,封装的代码不仅要屏蔽客户代码的访问,最好还能屏蔽客户代码开发者的访问。这样既鼓励代码作者多写规范文档,又鼓励代码用户多读规范文档。一切以规范为中心,而不以源码实现为中心。”
“非常好的建议!”冒号竖起拇指,“访问控制只是个玻璃罩,能防止乱动的双手,却防止不了偷窥的双眼。它至多只能维护语法上的封装和信息隐藏,而语义上的封装只有靠规范来维护。对程序员而言,前者是一种需要学习的知识,后者是一种需要培养的素质。”
叹号觉得脑子里仍是半清半浊:“能举个语义上违反封装的例子吗?”
冒号爽快地接受请求:“第一个例子是上节课谈到对象封装时作为反例的User类,其中getBirthday直接返回了内部域birthday的引用。如果你在调用getBirthday后对返回值进行修改,就是一种违反封装的行为。”
叹号有些愕然:“那不是User类本身首先违反封装原则的吗?”
冒号食指微扬:“不错,User类的作者错在授人以隙,而你错在乘人之隙。”
众人一阵哄笑,叹号面红耳赤,仿佛真的犯了错似的。
“刚才我们说过,超市开放货舱后门属管理不善,而客户钻进去取货属不守规矩。类似地,行人横穿马路的问题也有两方面的因素。”冒号重提前例,“说回User类,其设计者肯定不希望客户通过此种方式来修改birthday,否则也不会提供setBirthday的接口。”
逗号颇为不服:“可是setBirthday中除了简单的赋值什么也没干哪!”
“哈哈,又忍不住偷看源代码了吧!”冒号逮了个正着,“你怎么能保证User类的作者哪天不心血来潮,在setBirthday中写一些不同寻常的代码?不要轻视任何一个接口,哪怕它暂时只有一个空语句的实现。事实上,许多空接口就是为将来的功能扩展预留的,随时可能被充实,或者被子类覆盖。”
逗号心里话:得,又掉沟里了!
冒号续道:“第二个例子涉及Java中的Swing。一般说来,如果一个组件的可视化性质如位置、尺寸等发生改变,都需要重新布局(layout )。凡是Swing组件(component )都要调用revalidate 方法。绝大多数情况下,setText、setFont、setIcon等方法的实现中会自动调用revalidate,但仍有少数例外。规范文档中又语焉不详,令人困惑。为保证不受源码变动的影响,同时免除记忆之困,最好在一个组件所有与布局相关的变化完毕后,专门调用一次revalidate。以轻微的性能代价换来长治久安,无疑是正确的。相反,依赖源代码而非规范文档编程,显然是危险的。如果说第一个例子直接破坏了封装,有可能马上被察觉,该例则隐蔽得多——只要在所依赖的源代码不变,一切都正常。然而一旦有变,后果难以预料。”
引号不免有些感慨:“一般人熟悉JDK的API文档多过熟悉源码,尚且可能犯依赖源码编程的错误。如果重用同一开发组的代码,甚至是本人的代码,对源码非常熟悉,偏偏文档还匮乏,这种错误更是在所难免。”
“意识到这一点就是很大的进步啊。”冒号欣慰道,“再举一例。有时在使用一个类时,你很想重用其中一个protected方法,但当前所在的客户类既不是其子类,所在的package也不同。怎么办?”
句号承认:“以前的确碰到这样的问题,第一感觉是恨那作者太小气:为什么不干脆将其设为public与众共享?转念一想,大不了写个继承的子类,别的事不做,专门把那些protected方法转化为public。”
“是不是这样?”冒号在黑板上飞快地写下——
class Reserved
{
protected void f(){/**/}
protected int g(){/**/}
…
}
class Open extends Reserved
{
public void f(){super.f();}
public int g(){return super.g();}
…
}
见句号点头,冒号问:“你不觉得有何不妥吗?”
“很俗很暴力。”句号的自评令众人喷饭。
冒号分析道:“你既然那么希望调用某个protected方法,说明它一定不平凡,但为何作者遮遮掩掩、不愿公开呢?假若他的设计是合理的,那么只有一个解释:它是为内部或子类服务的,本就不打算对外开放。你所需要的服务要么是设计者刻意回避的,要么接口另有所在,说不定还恰好调用了你所需要的方法呢!”
一束光芒从众人脑际划过。
冒号又补充道:“不轻易公开他人的protected成员还有一个理由。正因为protected的接口比public使用的范围狭窄得多,接口变动的可能性往往也更大,客户应该慎用。总之,道法自然,不自然的另一面通常是不正确,请注重培养这种编程嗅觉。”
逗号使劲吸了吸鼻子。
冒号遂作结语:“我们提倡针对接口编程(programming to an interface),避免针对实现编程(programming to an implementation)。以上三例则是通过接口深入实现(programming through an interface to an implementation——《code complete》),本质上正是针对实现编程。以违背服务初衷的方式享受的服务,如同盛夏的豆腐——即使没有变质,也是不能持久的。”