——程序员提高班纪事
24.对象封装
阴阳地理两分张,隐者为阴显者阳 ——《玉髓经.曜星论》
“用广东话说,真是有型有料又有性格啊!”叹号啧啧连声,“这哪里是在设计软件,分明是在设计心仪的对象嘛。”
“我们可不就是在谈对象设计吗?”冒号笑着反问,“在OOP的世界里,每位程序员都是造物主。保持热情、专注力和审美情趣,说不定哪一天就像希腊神话里的皮格玛利翁一样,雕塑的美女变活了。”
“哇,那可就美了!”逗号极尽夸张之调。
全班哄堂大笑。
“刚才提到抽象是OOP三大基本特性的基础,下面我们逐个剖析。”冒号很快收拢了话题,“首当其冲的是封装性。记得前面谈对象范式时,引号曾试图为我们解释封装性,可惜被我无情地打断了。现在我们请他继续讲解吧。”
在众人逗趣式的掌声中,引号竟有些腼腆了:“所谓封装性,就是将数据与相关行为包装在一起以实现信息隐藏。”
“几乎无懈可击。”冒号赞扬得有些保守,“那么封装(encapsulation)与信息隐藏(information hiding)有区别吗?”
“应该是一回事吧。”在冒号的逼视下,引号有些犹豫了,“嗯。。。信息隐藏是一种原则,而封装是实现这种原则的一种方式。”
“言之有理!”冒号这回赞扬得很干脆,“尽管大多数参考书对二者不加区分,我还是要解析一番。其实广义的封装仅仅只是一种打包,即package或bundle,是密封的但可以是透明的。或者说,封装就是把一些数据和方法装在一个封闭的盒子里——可能是黑盒子,也可能是白盒子。从语法上说,这是OOP与诸如C之类的过程式语言最大的不同。请问这带来什么效果?”
句号反应很快:“这等于引入了一种新的模块机制,将相关的数据和作用其上的运算捆绑在一起形成被称为类的模块。”
“回答正确!”冒号很满意,“刚才我们用C实现了队列,但由于C不支持封装,只能以文件形式来划分模块,显然不如类划分那么方便和明晰。此外,封装还有语法糖(Syntactic sugar)效果。”
问号好奇地问:“什么是语法糖?是不是很甜?”
“所谓语法糖,就是一些语法上的甜头。它不是核心语法,并没有提供任何额外的功能,只是用起来更简洁实用、更自然方便,看起来更酷、更炫而已。”冒号有意用时髦的词汇来填补代沟,“我们知道,过程式函数采用谓语(主语,宾语)的形式,而OOP采用主语.谓语(宾语)的形式。”
“哦,就是那个狗吃屎和吃狗屎啊,那可不甜。”逗号又来插科打诨。
众人笑得前仰后合。
冒号不为所动:“再拿队列为例,如果增加一个队列成员,用刚才的C实现,我们需要写下:queue_add(queue, item)。假如用Java来实现,只需写queue.add(item)。由于封装使add绑定在queue上,一方面可以将对象queue前置,既更符合自然语言,又少敲一个字符;另一方面,这种绑定使add局限于Queue类中,因此不必加上‘queue_’的前缀以防与其他类的方法函数名相冲突。这同样节省了打字,也使接口更简单。”
句号提出:“如果C支持函数重载(overload),那么‘queue_’的前缀就可省去。”
“你说的既对也不对。”冒号辩证地评判,“如果C支持重载,该前缀的确能省去;但从另一角度看,即使Java或C++不支持重载,前缀用样能省去。因为函数add已经不再是全局函数,Queue类就是其上下文(context)。换句话说,分属不同类的函数是不可能产生歧义(ambiguity)的,哪怕它们的签名(signature)一模一样。因此我们要把功劳记在封装的名下。”
句号心悦诚服。
冒号继续讲解:“狭义的封装是在打包的基础上加上访问控制(access control),以实现信息隐藏。相对于上述广义的封装,不妨认为多了一个将白盒子刷成黑盒子的过程。这一过程可以看作对抽象的一种补充:抽象意味着用户可以从高层的接口来看待或使用一类对象,而不用关心它底层的实现,而黑盒封装意味着用户无权访问底层的实现。”
逗号有点茫然:“那谈起封装,究竟指哪一个?”
“一般所说的封装大多是狭义的。”冒号回复道,“考试中最无趣的一类试题就是名词解释,因为那只能印证记忆,不能印证理解。软件编程中也有无数的名词和概念,机械式的记忆没有任何意义——除了面试时应付某些同样无趣的考官。我们在这里着意诠释封装的概念,不是出于学术理论的目的,而是为了让大家深刻体会封装的目的和意义,以便在实践中灵活运用。”
问号询问:“前面提到,代码既要合法又要合理,那访问控制还重要吗?”
“合法合理是对程序员的要求。对于语言,我们还是希望它尽可能地提供更多的保障。这就好比社会和谐不能只靠法律,但法制当然越健全越好。”冒号解答道,“访问控制不仅是一种语法限制,也是一种语义规范——标有public的公用接口对代码阅读者而言,显然比注释文档更正式更直观。因此,其重要性是不言而喻的。值得一提的是,访问控制也不是滴水不漏的。C++用户可以通过指针来间接访问private成员,Java也可以通过反射机制来访问。”
见众人颇有疑义,冒号便写了一段Java代码——
冒号讲述道:“运行这段代码,可以看到privateObj的域成员和方法成员都被访问了。这是一种hack,仅限于特殊用途,不在我们关心之列。问题是,即使不考虑此类非常规做法,要实现信息隐藏也不是件容易的事。”
叹号不解:“信息隐藏困难在哪里呢?加上private不就隐藏了成员吗?”
“如果所有信息都隐藏了,这个对象还有什么用吗?”冒号一语破的。
逗号一愣:“可以用getter方法返回信息啊。”
冒号更不答话,投影出一段代码——
冒号提问:“这段代码简单得勿需多言,请问它的信息隐藏做得如何?”
众人目不转睛地盯了好一阵,无人应答。
冒号突发惊人之语:“如果我说User所有的方法都违背了信息隐藏原则,你们相信吗?”
直直的眼睛全都变圆了。
引号忽然明白了:“记得书上曾说不能直接返回类的内部对象。GetBirthday返回Date类型的生日,用户可以在调用此方法后直接对生日进行操作。”
“说得对极了!”冒号夸赞道,“如果一个方法返回了一个可变(mutable)域对象(field object)的引用,无异于前门紧闭而后门洞开。解决的方法是防御性复制(defensive copying),即返回一个clone的对象,以免授人以柄(handle)。”
逗号有些难以置信:“好像这类做法很普通啊。”
冒号耐心详解:“首先,请注意可变和引用两个条件,所有基本类型的域不是引用,因而是安全的,而Java中String之类非基本类由于是不可变的(immutable),也是安全的。同样,在C++和C#中的非基本类的值类型(value type)也不在此列。此外C++中申明了const的指针或引用返回值也能防止客户修改。其次,普通的做法不代表是正确的。事实上,恕我直言:普通的程序员是不合格的,合格的程序员是不普通的。最后,信息隐藏原则固然极其重要,但也不是金科玉律,在一定条件下也是允许的。比如仅作数据储存之用的类甚至可以开放所有的域成员,又比如不同类的对象共享同一引用。此外在一定范围之内为提高效率也可能采取变通之法,当然是在对用户晓以利害之后。”
问号举一反三:“同样道理,setBirthday也会导致信息泄漏。考虑到Date类型如此常用,Java是不是该引入一个不可变的日期类型呢?”
叹号喃喃自语:“getSex和setSex会有什么问题呢?boolean是基本类型啊。”
冒号提示:“考虑一下性别的可能性。”
叹号讶然道:“难不成还有不男不女型?”
众皆大笑。
冒号淡淡一笑:“不排除这种可能。更实际的情况是,有时性别是未知的。”
句号建议:“可以将小boolean换成大Boolean,多一个null值。”
冒号进一步指出:“如果想处理三种以上的可能性,可以采用char类型或String类型。总之这是实现细节,最好不要暴露给客户。因此不妨将getSex换成isMale和isFemale两个接口。”
引号细细玩味:“如果isMale和isFemale均返回false,那么性别不是保密就是中性了。至于性别用boolean、Boolean、char还是String来实现,用户是懵然不知的,这样比直接了当的getSex更隐蔽也更灵活。”
冒号揭开最后的答案:“方法computeAge的问题不在其实现,而在其命名。该名暗示年龄是计算出来的,这暴露了实现方式,应该改为getAge。请注意,信息隐藏中的信息不仅仅是数据结构,还包括实现方式和策略。试想,如果将来把年龄而不是生日作为User的输入,用年龄倒推生日,getBirthday是不是要换成computeBirthday呢?”
叹号不禁喟曰:“不想如此简单的get和set竟如此讲究!”
“通,则大处圆融合一而小处各具其妙;不通,则大处千变万化而小处无所分别。”冒号又打起了禅语,“领会OOP的精髓绝非一年半载之功,但若以抽象与封装为钥,必可早日开启通达之门。封装的故事远未结束,下节课继续。布置一下课后作业,请将示例中的User类按刚才的提示进行改进。”
posted on 2008-07-20 16:27 郑晖 阅读(2845) 评论(3) 编辑 收藏 所属分类: 冒号和他的学生们
我读来很有味,难道是你的讲义,嗯,看起来不太像。 如果把这真拿到课堂,整几个月的纯理论,还不让你的学员云里来雾里去。 回复 更多评论
每次看都会有收获~ 回复 更多评论
好极了! 回复 更多评论
Powered by: BlogJava Copyright © 郑晖