冒号和他的学生们(连载26)——访问控制
夫轻诺必寡信,多易必多难 ——《老子·德经》
问号提问:“信息隐藏是否专指用private来控制访问?”
“这正是我们的下一个焦点。”冒号微颔,“访问修饰符(access modifier)除了可以应用于类成员外,在Java和C#中还能应用于整个类。public类自然是公开的,而缺省的类在Java 和C#中分别仅对同一package和assembly开放。”
逗号不觉有异:“这有什么讲究吗?”
“当然。越是基本的语法越讲究,也越容易被忽视。”冒号的嘴角漾着一丝微笑,“许多人在写类时习惯上来就敲‘public class’,或者通过IDE自动生成类的雏形,缺省的一般都是public类。这样看似无害,但绝非好习惯。事实上大多数类是不需要公开的,这是一种更高层次的信息隐藏。每个package或assembly,如果设计合理,应该是具有相关功能的类和接口的集合。其中不少只是内部使用的,毫无必要公诸于众。”
引号了然于心:“这与对象封装类似,既向客户明确了接口类,又可消弭修改内部类对客户的影响。”
叹号仍心存疑虑:“OOP是鼓励和支持重用的。好不容易开发出来的类却深藏不露,岂不可惜?客户也许一时用不上,但指不定以后会用上。”
“问得好!这也是大多数人的心理。按照你的逻辑,我同样可以说:这个类的方法是好不容易开发出来的,藏起来太可惜了,统统public吧!”冒号惟妙惟肖地学着叹号的神态和腔调,把众人都逗乐了。
叹号心欲辩而口难言。
“重用是令人兴奋的,合理的重用既节省了开发时间,又节省了维护时间,代码也显得更简洁优美,可谓一举多得。但是——”冒号一转话锋,“过犹不及,过度追求重用也会造成滥用和误用。一方面,开发者容易沉溺于局部重用的妙处而忽略整体的设计,淡忘所开发类最核心的职责;另一方面,一旦所重用的类或方法发生改变,所有的重用者均受牵连,先前节省的时间或许会加倍地偿还。”
引号深有感触地说:“难就难在如何把握这个度啊!”
“任何一门技艺到了高级阶段,都是‘度’的学问。”冒号一如既往地推而广之,“初级程序员的理想是为所欲为——能用编程解决一切问题;中级程序员的理想是尽善而为——追求最佳解决方案;高级程序员的理想是有所为有所不为——重在整体设计的选择,能抵制局部技巧的诱惑;最高理想是无为而无不为——无论宏观设计还是微观实现,均非刻意选择,却自然合度。”
句号心念一动:“这四个阶段可分别用四句广告词来代表:从全球通的‘我能’,到奥运会的‘更快、更高、更强’,到安踏的‘我选择,我喜欢’,最后是马爹利的‘心意有别,心中有度’。”
“到底是时尚小青年,我的推而广之到你这儿变成了广而告之啦。”冒号半夸半谑,“书归正传,我们再说说类成员的访问修饰符。”
冒号在黑板上画了一张表格——
范围" 语言 |
C++ |
Java |
C# |
无限制 |
Public |
Public |
public |
子类或同一包 |
无 |
Protected |
protected internal |
同一类或子类 |
Protected |
private protected(已弃用) |
protected |
同一包 |
无 |
package(缺省) |
internal |
同一类 |
private(缺省) |
Private |
private(缺省) |
“其中public和private是两个极端,一个没有限制,一个仅限于同一类。Java和C#比C++多了个包(package或assembly)的概念,相应也多了package(缺省)和internal的修饰符。”冒号解说道,“值得注意的是,Java中的protected相当于C#的protected internal,不仅可被同类和子类访问,还能被同一包内的任何类访问。而C++和C#中的protected只能被同类和子类访问,相当于Java中昙花一现的private protected。”
逗号急欲得知:“选择这些修饰符有什么诀窍吗?”
冒号回应:“一个基本原则是尽可能地使用限制性更强的修饰符。即使以后改变主意,再放宽限制也不迟。相反,将一个修饰符收窄就要顾忌对客户的影响了。尤其是域成员,没有特殊理由都应该是private的,除非类是一个用作储存的具体数据类型或内部类(inner class)。在JDK源代码中有不少package、proctected域成员甚至public域成员,绝不能以之为榜样。顺便说一句,C++和C#中缺省的成员修饰符是private,显然比Java中缺省package更科学一些。”
问号刨根问底:“为什么特别强调域成员呢?”
冒号条分缕析:“域成员代表对象的状态,从运行方面看,若外界随意读取和改动,将无法保证内在逻辑的一致性;从设计方面看,属结构性信息,极易变化;从接口方面看,公开接口都是以方法而非域的形式出现的。这些都要求隐藏域成员。”
引号思路很清晰:“既然域成员一般都用private,那关键是如何选择方法成员的访问修饰符了。”
“如果将每个类看作一个服务者,它向不同范围内的客户承诺不同的服务,或者说提供了层次化的服务。以Java为例,每一个public方法成员即是向所有类提供的服务;protected方法成员对该类的子类和同一package下的类提供服务;默认的package方法成员仅对同一package下的类提供服务;private方法成员则只对该类本身提供服务。那么如何具体界定一个服务的范围呢?大多时候这是一个伪问题。”说到这里,冒号有意停顿了一会,查看大家的反应。
众人脸上果然满是狐疑之色。
“因为合理设计的服务,其内容与范围往往是密不可分的。一个服务如果已经设计好了,甚至已经实现了,而此时尚未决定其使用者的范围,是否有些荒谬?当然,荒谬其实也是现实中的一种常态。”冒号语带反讽,“作为一个抽象数据类型的类,其核心是抽象接口,因此首先应该设计公共接口,它们的修饰符自然都是public。如果该类是一个抽象类(abstract class)——此‘抽象’与抽象数据类型的‘抽象’意义有所不同,暂且不表——那么可能会有一些为其子类提供的服务,它们的修饰符自然该是protected。”
问号听得仔细:“难道非抽象类就不能有protected的接口吗?”
冒号回道:“从语法上说当然可以。但一般不建议继承非抽象类,因而protected的意义就不大了。至于个中缘由,留待下节课再解释。”
叹号追问:“那package服务和private服务呢?”
冒号应答:“package一般作为library的单位,因此package方法成员存在的唯一理由是仅为该library服务,但这种情况相对少见。说到private方法成员,已谈不上是真正的服务,纯粹是实现细节。由于它的改动不会影响客户,因此采用private访问控制不需要任何理由,不采用它才需要理由。”
众人听得频频点头。
冒号总结道:“从软件应变的角度来看,访问控制是对修改所带来的副作用的控制。具体地说,如果修改仅仅涉及到private成员,那只要检查该类的源代码即可;如果修改涉及到package成员,只需检查该类所在的package内的所有类。虽然这些类可能很多,但仍可控制,毕竟package是封闭的;如果修改涉及到protected成员,则不仅要检查该类所在的package内的所有类,还需检查该类的子类,如果该类本身是public,涉及的类可以超出该package的范围,已难以真正掌控;如果修改涉及到public成员,那就意味着任何类都可能调用该接口,也就可能因此而无法编译、运行和工作。因此访问控制越松的成员,辐射范围越广,软件重用的效率越高,承担的责任越大,修改的代价也越大。成熟的程序员对public和protected接口的设计一定是慎之又慎,往往在其上花的功夫更甚于具体代码的编写。”
逗号似有些不服:“可是谁又能保证接口永远不变呢?”
“所以Java和C#才分别提供了deprecated和obsolete以将方法标记为过时啊。”冒号笑言以对,“当然这是不得已而为之的下策,可以理解为设计者对使用者的一种致歉。”
见众人并无共鸣,冒号心道:响鼓也需重锤,遂言:“看来你们感触还不深,原因是缺乏一种关键的意识——客户意识。客户意识对一个程序员的重要性,丝毫不亚于对一个企业的重要性。”
逗号忍不住问:“您一再提到客户,究竟什么是客户?”
“这里的客户当然不是一般所指的软件终端消费者,而是指软件中间消费者或重用者,即调用该软件的代码或代码编写者。”冒号作了个名词解释,“客户意识的缺乏有几种原因。首先,不是每个人都有机会开发大型、关键的软件,许多程序员的客户主要是他自己或少数几个开发组成员,修改软件影响到的代码不多,影响到的人也不多,没有真正吃过设计不当的苦头。其次,开发库和框架的人毕竟是少数,大多数人开发的代码主要是自己调用或针对终端消费者的。即使他们写了一些可重用的代码,如果代码质量不被认可,也可能无人采用。当一个人习惯于自弹自唱时,是很难培养客户意识的。最后,正如我们在对象范式中指出的,过程式编程鼓励自顶向下,而OOP鼓励自底向上。显然,顶是底的客户。问题是许多OOP程序员仍习惯于过程式编程的风格,所设计的类或接口主要是调用其他的类或接口,而不是被调用。换句话说,他们设计的代码主要角色是客户,而不是客户的服务者。”
一番理论令众人心服口服。
引号关心地问:“如何培养客户意识呢?”
“如果认识到客户意识的重要性,培养起来并不难。”冒号带着安慰的口吻,“谨记一点:轻诺者,必寡信。每一个public类、每一个非private成员,都是一份承诺。在没有明确职责、没有准备承担变更后果之前,请采用最严格的访问控制。有了客户意识,才有接口责任感。千万不要为追求廉价的重用而轻易扩大接口范围,莫以自身之便而致客户之不便,莫以一时之便而致长期之不便。另外,单元测试对培养客户意识很有帮助。它不仅仅能发现程序的逻辑缺陷,还能发现程序的设计缺陷。因为测试代码就是最典型的客户代码,它能让你站客户的角度重新审视自己的接口设计。”