下面是小编整理的冒号和他的学生们―程序员提高班纪事24:对象封装,本文共2篇,欢迎您阅读,希望对您有所帮助。

篇1:冒号和他的学生们―程序员提高班纪事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代码——
//通过反射机制访问私有变量
importjava.lang.reflect.*;
classPrivate
{
privateString field=“这是私有变量”;
privatevoidmethod
{
System.out.println(“调用私有方法”);
}
}
publicclassAccessTest
{
publicstaticvoidmain(String[] args)throwsException
{
Private privateObj=newPrivate();
Field f=Private.class.getDeclaredField(“field”);
f.setAccessible(true);
System.out.println(f.get(privateObj));
Method m=Private.class.getDeclaredMethod(“method”,newClass[0]);
m.setAccessible(true);
m.invoke(privateObj,newObject[0]);
}
}
冒号讲述道:“运行这段代码,可以看到privateObj的域成员和方法成员都被访问了,
这是一种hack,仅限于特殊用途,不在我们关心之列。问题是,即使不考虑此类非常规做法,要实现信息隐藏也不是件容易的事。”
叹号不解:“信息隐藏困难在哪里呢?加上private不就隐藏了成员吗?”
“如果所有信息都隐藏了,这个对象还有什么用吗?”冒号一语破的。
逗号一愣:“可以用getter方法返回信息啊。”
冒号更不答话,投影出一段代码——
importjava.util.Date;
importjava.util.Calendar;
classUser
{
privateDate birthday;/** *//**生日*/
privatebooleansex;/** *//**性别。true代表男,false代表女*/
publicUser(Date birthday,booleansex)
{
this.birthday=birthday;
this.sex=sex;
}
publicDate getBirthday()
{
returnbirthday;
}
publicvoidsetBirthday(Date birthday)
{
this.birthday=birthday;
}
publicbooleangetSex()
{
returnsex;
}
publicvoidsetSex(booleansex)
{
this.sex=sex;
}
/** *//**计算年龄,负数表示未知*/
publicintcomputeAge()
{
if(birthday==null)return-1;
Calendar dob=Calendar.getInstance();
dob.setTime(birthday);
Calendar now=Calendar.getInstance();
intage=now.get(Calendar.YEAR)-dob.get(Calendar.YEAR);
if(now.get(Calendar.DAY_OF_YEAR)
--age;
returnage;
}
}
冒号提问:“这段代码简单得勿需多言,请问它的信息隐藏做得如何?”
众人目不转睛地盯了好一阵,无人应答。
冒号突发惊人之语:“如果我说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类按刚才的提示进行改进。”
来自:www.blogjava.net/xyz98/archive//07/20/216185.html
篇2:冒号和他的学生们―程序员提高班纪事23:数据抽象
系列文章汇总:《冒号和他的学生们——程序员提高班纪事》
数据抽象
善张网者引其纲,不一一摄万目而后得——《韩非子·外储说右下》
问号抢着说:“我知道了:过程抽象的结果是函数,数据抽象的结果应该是数据类型,”
冒号首肯:“数据类型与数据运算是程序语言的基本要素,除了内建的类型与运算外,程序语言还提供了用户定义(user-defined)的扩展机制,以提高编程者的效率。正如函数是一些基本运算的复合,自定义类型通常是一些基本类型的复合。不过单纯的复合类型并不是真正意义上的数据抽象,我们关注的是抽象数据类型(ADT)。”
逗号说了句老实话:“学数据结构时常提到抽象数据类型,但二者究竟什么关系还真没搞明白。”
冒号解析道:“作为数据与运算的有机集合体,它们可看作同一事物的两个方面。数据结构强调具体实现,侧重应用;抽象数据类型强调抽象接口,侧重设计。比如栈、队列、链表、二叉树等作为数据结构,人们关心的是如何利用它们有效地组织数据;而作为抽象数据类型,人们更关心的是类型的设计及其背后的数学模型。”
引号推想:“既然有抽象数据类型,想必就有具体数据类型吧?”
“这是当然。”冒号肯定道,“具体数据类型主要用于数据储存,除了getter和setter之外没有其他的运算。例如由省、市、街道和邮编组成的通讯地址便是一个典型的具体类型,谁能告诉我定义这种类型的意义?”
句号回答:“定义这种类型可以绑定省、市、街道和邮编这四个相关的数据,便于统一管理,包括创建、复制、作为参数传递或作为函数返回值等等。”
“说得不错!”冒号满意地点点头,“J2EE中常用一种设计模式:数据传输对象(Data Transfer Objects或DTO),又称值对象(Value Object或VO),这类对象不含任何业务逻辑,仅仅作为简单的数据容器,实质上也属于具体数据类型。”
“究竟这里的‘具体’具体在哪里,‘抽象’又抽象在哪里?”叹号的眼前飘浮的迷雾也是那么具体而抽象。
冒号轻轻拨开雾霭:“如果一个数据类型依赖于其具体实现,它就是具体的,反之则是抽象的。再拿通讯地址为例,它所有的域即省、市、街道和邮编对于客户都应该是透明的——至于是通过getter、setter还是直接访问并无本质区别,如此用户才能有针对性地进行数据储存、传递和获取。如果对该类型进行修改,比如增加一个代表国家的域或者减少代表邮编的域,必须知会用户,否则毫无意义。显然这种类型与实现细节密切相关,因而是具体的。作为抽象类型的例子,让我们看看队列(Queue)吧。队列是一种非常基本的数据结构,广泛应用于操作系统、网络和现实生活中。请问它的特征是什么?”
引号最擅长这类问题:“队列的特征是先进先出(FIFO)——每次数据只能从队尾加入,从队首移除。”
“好的。队列一般至少包括类似数据库的CRUD(增删改查)操作:创建操作——建队;删除操作——撤队;修改操作——入队、出队;查询操作——是否为空队、队列长度、队首。下面我们用C来表述队列的操作接口。”冒号投影出一段代码——
typedefcharItemType;/*队列成员的数据类型,char可换成其他类型*/
/*QueueType待定。。。*/
typedef QueueType*Queue;
/** 初始化队列。成功返回0,否则返回-1。*/
intqueue_initialize(Queue);
/** 终结化队列*/
voidqueue_finalize(Queue);
/** 加入队列尾部。成功返回0,否则返回-1。*/
intqueue_add(Queue, ItemType);
/** 移除队列头部。成功返回0,否则返回-1。*/
intqueue_remove(Queue, ItemType*);
/** 队列是否为空?*/
intqueue_empty(Queue);
/** 队列长度*/
intqueue_length(Queue);
/** 返回(但不移除)队列头部。成功返回0,否则返回-1。*/
intqueue_head(Queue, ItemType*);
冒号解释:“特意用C语言是为了表明ADT不独OOP专有。由于C不支持异常(exception),因此用非零返回值来表示错误发生。我们尚未定义队列类型QueueType,其核心是队列的成员集合。无论是用数组来实现,还是用链表来实现,用户根本不需关心。这便是队列的抽象所在——用户不应知道也不必知道它的具体实现,只能通过指定接口来进行‘暗箱操作’。这样经过数据抽象,队列的本质特征由API展现,非本质特征则屏蔽于客户的视野之外。”
问号问道:“这种数据抽象与前面提到的参数抽象和规范抽象有何关系?”
“参数抽象使得数据接口普适化,规范抽象使得数据接口契约化。”冒号的回答简明扼要,“此外,一个完整的数据抽象除了对每个接口作规范说明外,还需对该数据类型作整体规范说明,OOP中的类注释文档即作此用。”
逗号要求:“能不能给出完整的实现代码?光有接口没实现,似乎不太过瘾。”
冒号戏言:“我感觉你在把程序当烟抽——光有烟嘴的接口,没有香烟的实现,的确不太过瘾。”
众笑。
冒号借题发挥:“许多程序员都有一个通病:重实现,轻接口。在编写代码时表现为:不等接口设计好就技痒难忍,揎拳捋袖地开始大干;在阅读代码时表现为:看到API文档便恹恹欲睡,看到代码便两眼放光。务必谨记:接口是纲,实现是目。纲若不举,目无以张。也就是常说的:‘Programming to an Interface, not an Implementation’。不过为满足你们的要求,我还是写了一段基于循环数组的实现代码。”
逗号正感当靶子的滋味不好受,一见代码便心旌摇荡,宠辱皆忘了。
/*队列类型定义*/
/*文件QueueImpl.c:队列的循环数组(circular array)实现*/
typedefstruct
{
ItemType*data;/**//*队列成员数据*/
intfirst;/**//*队首位置*/
intlast;/**//*队尾位置*/
intcount;/**//*队列长度*/
intsize;/**//*队列容量*/
}QueueType;
intqueue_initialize(Queue q)
{
intsize=100;/**//*初始容量*/
q->size=size;
q->data=(ItemType*)malloc(sizeof(ItemType)*size);
if(q->data==NULL)return-1;/**//*内存不足*/
q->first=0;
q->last=-1;
q->count=0;
return0;
}
voidqueue_finalize(Queue q)
{
free(q->data);
q->data=NULL;
q->first=0;
q->count=0;
}
intqueue_empty(Queue q)
{
returnq->count<=0;
}
intqueue_length(Queue q)
{
returnq->count;
}
/**//*(内部函数)扩大队列容量*/
staticintqueue_resize(Queue q)
{
intoldSize=q->size;
intnewSize=oldSize*2;
intnewIndex;
intoldIndex=q->first;
ItemType*data=(ItemType*)malloc(sizeof(ItemType)*newSize);
if(data==NULL)return-1;/**//*内存不足*/
for(newIndex=0; newIndex
{
data[newIndex]=q->data[oldIndex];
oldIndex=(oldIndex+1)%oldSize;
}
free(q->data);
q->data=data;
q->first=0;
q->last=oldSize-1;
q->size=newSize;
return0;
}
intqueue_add(Queue q, ItemType item)
{
if(q->count>=q->size)/**//*超出容量后自动扩容*/
{
if(queue_resize(q)<0)return-1;/**//*内存不足*/
}
q->last=(q->last+1)%q->size;
q->data[q->last]=item;
++q->count;
return0;
}
intqueue_remove(Queue q, ItemType*item)
{
if(q->count<=0)return-1;
*item=q->data[q->first];
q->first=(q->first+1)%q->size;
--q->count;
return0;
}
intqueue_head(Queue q, ItemType*item)
{
if(q->count<=0)return-1;
*item=q->data[q->first];
return0;
}
“由于函数queue_resize并非公共接口,因此前面加上static,以避免被外部调用,
与Java中的涵义不同,C中static函数表示文件内部函数。作为对比,我们再看看队列的链表实现。”冒号说罢投影出另两段代码——
/*队列类型定义*/
typedefstructNodeType
{
ItemType item;/**//*队列成员数据*/
structNodeType*next;
}NodeType;
typedef NodeType*Node;
typedefstruct
{
Node head;/**//*队首*/
Node tail;/**//*队尾*/
intcount;/**//*队列长度*/
}QueueType;
/*文件QueueImpl.c:队列的链表(linked list)实现*/
intqueue_initialize(Queue q)
{
q->head=NULL;
q->tail=NULL;
q->count=0;
return0;
}
voidqueue_finalize(Queue q)
{
ItemType item;
while(queue_remove(q,&item)>=0)
;
}
intqueue_empty(Queue q)
{
returnq->count<=0;
}
intqueue_length(Queue q)
{
returnq->count;
}
intqueue_add(Queue q, ItemType item)
{
Node node=(Node)malloc(sizeof(NodeType));
if(node==NULL)return-1;/**//*内存不足*/
node->item=item;
node->next=NULL;
if(q->tail)
{
q->tail->next=node;
q->tail=node;
}
else
{
q->head=q->tail=node;
}
++q->count;
return0;
}
intqueue_remove(Queue q, ItemType*item)
{
Node oldHead=q->head;
if(q->count<=0)return-1;
*item=oldHead->item;
q->head=oldHead->next;
free(oldHead);
if(--q->count==0)
{
q->tail=NULL;
}
return0;
}
intqueue_head(Queue q, ItemType*item)
{
if(q->count<=0)return-1;
*item=q->head->item;
return0;
}
叹号发现:“两种实现方式看起来迥然不同啊。”
“不同的内部数据结构,导致不同的算法。正是注意到这一点,人们多采取‘整体设计以数据为中心,局部实现以算法为中心’的方针,以增强系统的可维护性。最后看看示例客户代码。”冒号继续放幻灯——
/*客户测试代码*/
QueueType queue;
Queue q=&queue;
charitem;
inti;
queue_initialize(q);
for(i=0; i<26;++i)/**//*将26个字母加入队列*/
{
queue_add(q,“a”+i);
}
printf(“Queue is %s”, queue_empty(q)?“empty”:“nonempty”);
printf(“Queue length = %d”, queue_length(q));
while(queue_remove(q,&item)==0)/**//*一一出队*/
{
printf(“removing queue item:[%c].”, item);
}
printf(“Queue is %s”, queue_empty(q)?“empty”:“nonempty”);
queue_finalize(q);
冒号指出:“尽管两种实现方式大相径庭,客户代码却毫无二致。这种数据类型的接口与实现的分离,有利于开发时间的分离以及开发人员的分离。开发时间的分离指的是:开发人员可以推迟在不同实现方式中作抉择,以保证整体开发进程;开发人员的分离指的是:程序的修改和维护不局限于原作者。”
问号发现一个问题:“C语法中没有private关键词,用户仍然有权访问和修改队列的域成员,整个代码逻辑有可能被破坏。”
“没错。但作为一个合格的程序员,写出的代码不仅要合法,还要合理。”冒号掷地有声,“合法指合乎语法,合理指合乎语义。既然用到队列这个数据结构,当然要遵循其使用规范。打个比方,法律只是维护社会秩序的最低限度的规范,一个只遵守法律而不遵守通用规范的人必定与社会格格不入。从另一个角度看,假设所有程序员都是遵守规范的,那么类似C这种非OOP语言,只要将数据抽象与过程抽象有机结合,同样具有与OOP不相上下的可维护性和可重用性。”
引号有些困惑:“OOP中的类是否就是ADT?”
冒号释疑:“可以将类理解为具有继承和多态机制的ADT。但严格说来,并不是所有的类都有抽象性,比如前面提到的仅作存储用的值对象。在C#中有值类型与引用类型之分,分别用struct和class的关键字来指明。可以把ADT作为选择原则:是ADT则采用引用类型,否则采用值类型。C++中struct与class在机制上没有区别,只是前者成员缺省为public而后者缺省为private。但习惯上也是前者作具体类型,后者作抽象类型。Java和C中没有类似的区分,一个只支持class,一个只支持struct。”
句号沉吟半晌,忽道:“能不能这样总结一下抽象数据类型?抽象——接口与实现相分离;数据——以数据为中心组织逻辑;类型——单纯而定义良好的概念。”
“精辟!”冒号赞赏有加,“许多人能将OOP中的封装、继承和多态说得头头是道,用得得心应手,便自认为精通OOP了。殊不知抽象——尤其是数据抽象——才是OOP的核心和起源,尽管它们并非OOP的专利。没有抽象作基础,封装、继承和多态尽皆无本之木。只有贯彻ADT思想,设计出来的类才会是‘万人迷’:有优雅的外形——抽象,有丰富的内涵——数据,有鲜明的个性——类型。”
附:示例源代码下载(queue.rar) (酷勤网备用下载地址:queue.rar 2.9k)
来自:www.blogjava.net/xyz98/archive/2008/07/16/215180.html
文档为doc格式