写在前面:本博客为本人原创,严禁任何形式的转载!本博客只允许放在博客园(.cnblogs.com),如果您在其他网站看到这篇博文,请通过下面这个唯一的合法链接转到原文!
本博客全网唯一合法URL:http://www.cnblogs.com/acm-icpcer/p/8601527.html
大一的时候上c++课程就开始学习面向对象的思想,当时每周的代码练习写各种类和对象写得不亦悦乎。虽然后来到了大二又学习了Java面向对象语言,但是我始终不知道,为什么面向对象的语言在类机制中要设立访问控制权限?
就拿Java来说,即使是学过Java皮毛的同学都晓得Java访问控制权限有:private、default(一般省略)、public、protected,常用的也就只有private、public、protected。其中:
(1)private: Java语言中对访问权限限制的最窄的修饰符,一般称之为“私有的”。被其修饰的属性以及方法只能被该类的对象访问,其子类不能访问,更不能允许跨包访问。
(2)protected: 介于public 和 private 之间的一种访问修饰符,一般称之为“保护访问权限”。被其修饰的属性以及方法只能被类本身的方法及子类访问,即使子类在不同的包中也可以访问。
(3)public: Java语言中访问限制最宽的修饰符,一般称之为“公共的”。被其修饰的类、属性以及方法不仅可以跨类访问,而且允许跨包访问。
上面这些东西是最最基本的,估计学过Java或者c++的同学一辈子也忘不掉。但是你知道为什么要设立这样的访问权限控制机制吗?
初期学习编程,我往往只知道要怎么怎么样,认为这就是语法的死规定,认为这就是语言的所谓“特性”,但是却始终不明白为什么要引入这些规定和特性。
今天上了Jin Xing老师的课,才真正的知道访问权限控制机制引入的原因。
对于面向对象语言,我们都知道,要去访问一个对象的私有成员,必须通过该对象的公有成员去访问,否则在类对象外部是不可能访问到内部成员的。那么为什么要这样做?仅仅是因为安全性的问题或者说代码保密性的原因吗?
不!这些都不是主要原因,真正要使用到private的场合是:
(1)复用性。比如说,在一个类内部,有5个方法体,它们都有一部分代码实现同样一个功能,那么,为了提高复用性和代码效率,我们会把功能重复的这一段代码单独拿出来做一个函数,让这5个方法去调用这个单独做出来的函数,那么为了保证这5个方法可以安全无虞地访问单独的这个函数,我们就把这个函数设为private,也就是让它只被类内的方法访问,本质是为了节省类内函数代码重复所造成的代码浪费。
(2)易更新性(最普遍的情况)。你一开始写了一个程序,但是你对这段程序不是很满意,或者你觉得这段程序的功能不是很完整,这个时候,你可以把这段代码用private权限封装到一个方法中,这样,相对于在不稳定的代码和稳定的代码之间用private权限架了一道防火墙,不仅便于你自己修改不满意的代码,还可以保证这段不影响别的代码,也就是说保证了方法体的签名不被修改。
(3)可能为了赶上发布应用的时间,来不及写好某段代码,或者没有测试好某段代码,为了方便以后对这段代码进行重构,将方法封装为private。
(4)用private修饰类的数据成员。因为数据的灵活性远远小于函数,故而在函数没有很大可能发生重大改变时,数据是不会轻易改变的,特别是对于java和c++这样类型严格的语言。这样的用法透露出来一种编程哲学,那就是:代码是面向未来的,而不是面向现在的。用马克思主义基本原理的说法来说就是:事物总是在波浪式前进、螺旋式上升的过程中不断改变的。
(5)一般来说,在设计一个应用程序的时候,把未来不会轻易改变的程序部分做成公有,把未来很有可能改变的程序部分做成私有。
好的,我已经写清楚了为什么要使用访问权限控制:为了提高代码的灵活性。下面,我通过一个例子来进一步阐述要使用访问权限控制的原因。
考虑如下面向对象代码:
class A { private int ID; public int getID() { return ID; } public void setID(int newID) { ID=newID; } }
大家看了代码,有的同学会想,既然我们在类内通过get和set方法实现了对变量的存取,实现了int型变量ID的所有功能(变量的功能不就是存和取吗?)。那么为什么不直接把"private int ID;"改成"public int ID;"?这样不就可以删掉看似多余的get和set方法了吗?
这个问题我在大一疯狂写类与对象继承封装练习题的时候想过,但是由于沉迷刷题,始终没想明白,直到今天才明白。说白了是为了舍弃简便性而换取灵活性。
现在假定这段代码作为一个应用程序的核心代码已经被发布到应用市场上去了,而且反响热烈,人民群众用的不亦乐乎,而且还有很多极客基于我们的这个应用程序扩展出了其他应用程序。现在老板突然告诉我,运维的同学升级了核心数据库,修改了数据库中的数据表,要求我们开发组把上面代码中的ID的类型换为double。
那这好办,直接把int类型全部换成double就行了,对吧?NO!
如果有极客在使用我们的应用程序,他们基于我们的程序进行了扩展(这种情况是很普遍的!想想Linux在1990年代刚发布的时候,也是由不同的极客对最初版本的Linux不断的进行功能扩展才发展出今天的Linux)。在涉及class A的代码段,他们统统使用了"int xxx=a.getID();"这样的代码直接访问A类的数据成员ID,那么如果他们不知道我们更新了A类的数据成员ID的类型为double,那么"int xxx=a.getID();"就犯了"int xxx=(double)ID;"这样的错误,极客们那边就会造成数据溢出。
但由于我们的代码仅允许他们使用get和set方法访问数据成员ID,那么我们为了防止其他极客的扩展程序出bug,就必须在get和set方法中使用适当的算法来使数据成员ID的类型满足原来的需求,比如,对于set方法,我们可以更新代码为:
public int getID() { return (int)ID; }
这样,虽然在我们这边发生了数据的截断,但是其他极客什么都不用做就能安安稳稳的继续开发扩展程序了。也就是说,错误没有扩散,而是被我们控制住了。
本质上来讲,我们一开始就运用了访问权限控制的机制,为解决未来更新可能导致的错误留下了挽回的余地。
那么如果我们一开始就为了简便,把"private int ID;"改成"public int ID;",并删除了get和set方法,那么我们将没有任何的补救措施来挽回错误的扩散。因为早在一开始,所有极客就使用"int xxx=a.ID;a.ID=yyy;"这样的代码来运用A类,也就是说,错误早就已经随着我们第一次发布应用程序就扩散出去了,所有开发者都和我们一起在犯错误,单单凭我们一方的改变不能解决bug,必须所有开发者一起修改代码才能解决所有问题。那么,这样付出的成本将远远超过我们的预料,因为早在开始进行编程架构的时候,我们就没有为现在的更新、维护留有余地。
总结一下:访问权限控制的使用主要就是为未来的更新和维护留有余地而出现的机制,核心思想是"代码是不是面向现在的,也不是面向对象的,而是面向未来的!"
tz@COI HZAU
2018/3/19