冒号和他的学生们(连载24)——对象封装

 

 

冒号和他的学生们

——程序员提高班纪事

 

24.对象封装

阴阳地理两分张,隐者为阴显者阳                             ——《玉髓经.曜星论》

  

“用广东话说,真是有型有料又有性格啊!”叹号啧啧连声,“这哪里是在设计软件,分明是在设计心仪的对象嘛。”

“我们可不就是在谈对象设计吗?”冒号笑着反问,“在OOP的世界里,每位程序员都是造物主。保持热情、专注力和审美情趣,说不定哪一天就像希腊神话里的皮格玛利翁一样,雕塑的美女变活了。”

“哇,那可就美了!”逗号极尽夸张之调。

全班哄堂大笑。

“刚才提到抽象是OOP三大基本特性的基础,下面我们逐个剖析。”冒号很快收拢了话题,“首当其冲的是封装性。记得前面谈对象范式时,引号曾试图为我们解释封装性,可惜被我无情地打断了。现在我们请他继续讲解吧。”

在众人逗趣式的掌声中,引号竟有些腼腆了:“所谓封装性,就是将数据与相关行为包装在一起以实现信息隐藏。”

“几乎无懈可击。”冒号赞扬得有些保守,“那么封装(encapsulation)与信息隐藏information hiding)有区别吗?”

“应该是一回事吧。”在冒号的逼视下,引号有些犹豫了,“嗯。。。信息隐藏是一种原则,而封装是实现这种原则的一种方式。”

“言之有理!”冒号这回赞扬得很干脆,“尽管大多数参考书对二者不加区分,我还是要解析一番。其实广义的封装仅仅只是一种打包,即packagebundle,是密封的但可以是透明的。或者说,封装就是把一些数据和方法装在一个封闭的盒子里——可能是黑盒子,也可能是白盒子。从语法上说,这是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支持重载,该前缀的确能省去;但从另一角度看,即使JavaC++不支持重载,前缀用样能省去。因为函数add已经不再是全局函数,Queue类就是其上下文(context)。换句话说,分属不同类的函数是不可能产生歧义(ambiguity)的,哪怕它们的签名signature)一模一样。因此我们要把功劳记在封装的名下。”

句号心悦诚服。

冒号继续讲解:“狭义的封装是在打包的基础上加上访问控制access control),以实现信息隐藏。相对于上述广义的封装,不妨认为多了一个将白盒子刷成黑盒子的过程。这一过程可以看作对抽象的一种补充:抽象意味着用户可以从高层的接口来看待或使用一类对象,而不用关心它底层的实现,而黑盒封装意味着用户无权访问底层的实现。”

逗号有点茫然:“那谈起封装,究竟指哪一个?”

“一般所说的封装大多是狭义的。”冒号回复道,“考试中最无趣的一类试题就是名词解释,因为那只能印证记忆,不能印证理解。软件编程中也有无数的名词和概念,机械式的记忆没有任何意义——除了面试时应付某些同样无趣的考官。我们在这里着意诠释封装的概念,不是出于学术理论的目的,而是为了让大家深刻体会封装的目的和意义,以便在实践中灵活运用。”

问号询问:“前面提到,代码既要合法又要合理,那访问控制还重要吗?”

“合法合理是对程序员的要求。对于语言,我们还是希望它尽可能地提供更多的保障。这就好比社会和谐不能只靠法律,但法制当然越健全越好。”冒号解答道,“访问控制不仅是一种语法限制,也是一种语义规范——标有public的公用接口对代码阅读者而言,显然比注释文档更正式更直观。因此,其重要性是不言而喻的。值得一提的是,访问控制也不是滴水不漏的。C++用户可以通过指针来间接访问private成员,Java也可以通过反射机制来访问。”

见众人颇有疑义,冒号便写了一段Java代码——

// 通过反射机制访问私有变量
import java.lang.reflect.*;

class Private 
{
    
private String field = "这是私有变量";

    
private void method() 
    
{
        System.out.println(
"调用私有方法");
    }

}


public class AccessTest
{
    
public static void main(String[] args) throws Exception
    
{
        Private privateObj 
= new Private();

        Field f 
= Private.class.getDeclaredField("field");
        f.setAccessible(
true);
        System.out.println(f.get(privateObj));

        Method m 
= Private.class.getDeclaredMethod("method"new Class[0]);
        m.setAccessible(
true);
        m.invoke(privateObj, 
new Object[0]);
    }

}

 

冒号讲述道:“运行这段代码,可以看到privateObj的域成员和方法成员都被访问了。这是一种hack,仅限于特殊用途,不在我们关心之列。问题是,即使不考虑此类非常规做法,要实现信息隐藏也不是件容易的事。”

叹号不解:“信息隐藏困难在哪里呢?加上private不就隐藏了成员吗?”

“如果所有信息都隐藏了,这个对象还有什么用吗?”冒号一语破的。

逗号一愣:“可以用getter方法返回信息啊。”

冒号更不答话,投影出一段代码——

import java.util.Date;
import java.util.Calendar;

class User
{
    
private Date birthday; /** 生日 */
    
private boolean sex; /** 性别。true代表男,false代表女 */

    
public User(Date birthday, boolean sex)
    
{
        
this.birthday = birthday;
        
this.sex = sex;
    }


    
public Date getBirthday()
    
{
        
return birthday;
    }


    
public void setBirthday(Date birthday)
    
{
        
this.birthday = birthday;
    }


    
public boolean getSex()
    
{
        
return sex;
    }


    
public void setSex(boolean sex)
    
{
        
this.sex = sex;
    }


    
/** 计算年龄,负数表示未知 */
    
public int computeAge()
    
{
        
if (birthday == nullreturn -1

        Calendar dob 
= Calendar.getInstance();
        dob.setTime(birthday);
        Calendar now 
= Calendar.getInstance();
       
int age = now.get(Calendar.YEAR) - dob.get(Calendar.YEAR);
       
if (now.get(Calendar.DAY_OF_YEAR) < dob.get(Calendar.DAY_OF_YEAR))
            
--age;
       
return age;
    }

}

 

冒号提问:“这段代码简单得勿需多言,请问它的信息隐藏做得如何?”

众人目不转睛地盯了好一阵,无人应答。

冒号突发惊人之语:“如果我说User所有的方法都违背了信息隐藏原则,你们相信吗?”

直直的眼睛全都变圆了。

引号忽然明白了:“记得书上曾说不能直接返回类的内部对象。GetBirthday返回Date类型的生日,用户可以在调用此方法后直接对生日进行操作。”

“说得对极了!”冒号夸赞道,“如果一个方法返回了一个可变(mutable域对象field object)的引用,无异于前门紧闭而后门洞开。解决的方法是防御性复制(defensive copying),即返回一个clone的对象,以免授人以柄(handle)。”

逗号有些难以置信:“好像这类做法很普通啊。”

冒号耐心详解:“首先,请注意可变引用两个条件,所有基本类型的域不是引用,因而是安全的,而JavaString之类非基本类由于是不可变的immutable),也是安全的。同样,在C++C#中的非基本类的值类型value type)也不在此列。此外C++中申明了const的指针或引用返回值也能防止客户修改。其次,普通的做法不代表是正确的。事实上,恕我直言:普通的程序员是不合格的,合格的程序员是不普通的。最后,信息隐藏原则固然极其重要,但也不是金科玉律,在一定条件下也是允许的。比如仅作数据储存之用的类甚至可以开放所有的域成员,又比如不同类的对象共享同一引用。此外在一定范围之内为提高效率也可能采取变通之法,当然是在对用户晓以利害之后。”

问号举一反三:“同样道理,setBirthday也会导致信息泄漏。考虑到Date类型如此常用,Java是不是该引入一个不可变的日期类型呢?”

叹号喃喃自语:“getSexsetSex会有什么问题呢?boolean是基本类型啊。”

冒号提示:“考虑一下性别的可能性。”

叹号讶然道:“难不成还有不男不女型?”

众皆大笑。

冒号淡淡一笑:“不排除这种可能。更实际的情况是,有时性别是未知的。”

句号建议:“可以将小boolean换成大Boolean,多一个null值。”

冒号进一步指出:“如果想处理三种以上的可能性,可以采用char类型或String类型。总之这是实现细节,最好不要暴露给客户。因此不妨将getSex换成isMaleisFemale两个接口。”

引号细细玩味:“如果isMaleisFemale均返回false,那么性别不是保密就是中性了。至于性别用booleanBooleanchar还是String来实现,用户是懵然不知的,这样比直接了当的getSex更隐蔽也更灵活。”

冒号揭开最后的答案:“方法computeAge的问题不在其实现,而在其命名。该名暗示年龄是计算出来的,这暴露了实现方式,应该改为getAge。请注意,信息隐藏中的信息不仅仅是数据结构,还包括实现方式和策略。试想,如果将来把年龄而不是生日作为User的输入,用年龄倒推生日,getBirthday是不是要换成computeBirthday呢?”

叹号不禁喟曰:“不想如此简单的getset竟如此讲究!”

通,则大处圆融合一而小处各具其妙;不通,则大处千变万化而小处无所分别。”冒号又打起了禅语,“领会OOP的精髓绝非一年半载之功,但若以抽象与封装为钥,必可早日开启通达之门。封装的故事远未结束,下节课继续。布置一下课后作业,请将示例中的User类按刚才的提示进行改进。”

posted on 2008-07-20 16:27 郑晖 阅读(2843) 评论(3)  编辑  收藏 所属分类: 冒号和他的学生们

评论

# re: 冒号和他的学生们(连载24)——对象封装 2008-07-21 08:55 隔叶黄莺

我读来很有味,难道是你的讲义,嗯,看起来不太像。
如果把这真拿到课堂,整几个月的纯理论,还不让你的学员云里来雾里去。  回复  更多评论   

# re: 冒号和他的学生们(连载24)——对象封装 2008-07-29 11:47 Christ Chang

每次看都会有收获~  回复  更多评论   

# re: 冒号和他的学生们(连载24)——对象封装 2008-07-29 14:00 Mr liang

好极了!  回复  更多评论   


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


网站导航:
 

导航

统计

公告

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

留言簿(17)

随笔分类(61)

随笔档案(61)

文章分类(1)

文章档案(1)

最新随笔

积分与排名

最新评论

阅读排行榜

评论排行榜