冒号和他的学生们(连载27)——接口服务

 

冒号和他的学生们

 

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 方法。绝大多数情况下,setTextsetFontsetIcon等方法的实现中会自动调用revalidate,但仍有少数例外。规范文档中又语焉不详,令人困惑。为保证不受源码变动的影响,同时免除记忆之困,最好在一个组件所有与布局相关的变化完毕后,专门调用一次revalidate。以轻微的性能代价换来长治久安,无疑是正确的。相反,依赖源代码而非规范文档编程,显然是危险的。如果说第一个例子直接破坏了封装,有可能马上被察觉,该例则隐蔽得多——只要在所依赖的源代码不变,一切都正常。然而一旦有变,后果难以预料。”

引号不免有些感慨:“一般人熟悉JDKAPI文档多过熟悉源码,尚且可能犯依赖源码编程的错误。如果重用同一开发组的代码,甚至是本人的代码,对源码非常熟悉,偏偏文档还匮乏,这种错误更是在所难免。”

“意识到这一点就是很大的进步啊。”冒号欣慰道,“再举一例。有时在使用一个类时,你很想重用其中一个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》),本质上正是针对实现编程。以违背服务初衷的方式享受的服务,如同盛夏的豆腐——即使没有变质,也是不能持久的。”

posted on 2008-08-07 19:31 郑晖 阅读(2187) 评论(2)  编辑  收藏 所属分类: 冒号和他的学生们

评论

# re: 冒号和他的学生们(连载27)——接口服务 2008-10-18 20:23 wanget

很好的文章,看了一下午眼睛都疼了 ^_^

问个问题,看到了你说的“除了盗版的商业软件,都应该能联系到原作者“,如果公司里写那段代码的程序员走了,而代码写的又不规范,最要命的假如我本人需要维护那段代码,而代码又有好几万行……这种情况下有没有什么好的思路呢? (C程序)  回复  更多评论   

# re: 冒号和他的学生们(连载27)——接口服务[未登录] 2008-10-19 12:49 郑晖

@wanget
几万行的代码在提交时竟然没有明确规范的说明,此一错;公司在代码作者离职时没有安排与接手人进行必要的交接,此二错;没有留下作者的联系方式,此三错。这一方面说明公司的管理不规范,另一方面也说明这部分代码并不是那么重要,否则难以想象公司如何靠软件开发来生存。维护这样的代码,首先要看这段代码在整个系统或子系统中所扮演的角色,自己草拟一份规范说明,并与其他相关人员核实。(如果整个系统都没有明确的规范的话,你得考虑跳槽了。)其次,找出所有这段代码被外部调用的接口和公开变量(包括C程序中的全局变量)。最好不要依赖IDE,自己写一段script来搜索并保留结果。这样有两个重要作用:一、通过客户代码反向推出接口的用法;二、在维护代码时可以自由地改变其他的非接口部分,而无需担心对其他部分代码的影响。
  回复  更多评论   


只有注册用户登录后才能发表评论。


网站导航:
 

导航

统计

公告

博客搬家:http://blog.zhenghui.org
《冒号课堂》一书于2009年10月上市,详情请见
冒号课堂

留言簿(17)

随笔分类(61)

随笔档案(61)

文章分类(1)

文章档案(1)

最新随笔

积分与排名

最新评论

阅读排行榜

评论排行榜