weitom1982

向各位技术前辈学习,学习再学习.
posts - 299, comments - 79, trackbacks - 0, articles - 0
  IT博客 :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理

 

                                      

   
 在单位,我可能是最令大家羡慕的男士了:大学毕业后顺利考取公务员,最后进入航空公司办事处,刚刚三十出头就晋升为办公室主任。达到婚龄时,又适时认识了刚从空姐岗位退下来被外资公司高薪聘请为人力资源部经理的妻子。结婚后,在单位的福利补贴下,在东三环南段的潘家园拥有一套150平方米的住房。婚后一年,增添了活泼可爱的儿子,随后又以一次性付款的方式,开回了一辆桑塔纳“时代超人”。 房子、车子、妻子、儿子、 票子我全部都拥有了。
    
     妻子温柔又美丽,可是我却开始越来越觉得自己对妻子没有激情了。成为有车一族后,下班免不了会有同事坐我的顺风车,坐得最多的就是刚进公司不久的小秘书。她住在国贸桥附近,和我顺路。小秘书刚大学毕业,正是花样年华。进入收入丰厚的航空公司,拿着大把的钞票自然是不遗余力地打扮自己,从化妆品到新款时装,让我每天看得眼花缭乱。
    
     我的心便开始花了,尽管还没什么实质性的进展,但我知道跨出那一步只是迟早的事情。回到家,看着甜蜜地喂儿子吃饭的妻子,心里多少也会有一点愧疚。但是,心仍然开始不可避免的背离,我甚至开始憧憬那一天地早日到来。
    
     就这样到了12月7日,从下午2点开始雪开始飘飘洒洒地落了下来,到下午下班时地上已经有了薄薄的一层,我走出办公室没有丝毫犹豫地把车开了出来。副驾驶位上坐着的当然还是小秘书,车上了机场高速,情况不妙了,平时道路宽阔,车速可以跑到100码以上的路面突兀地拥挤了许多,勉强以在长安街上晨跑的速度一点一点地往前蹭。
    
     根据以往的经验,只要四环一闹“肠梗阻” 三环绝对会“便秘”, 可是车在高速上半点不由人。8点半,终于在三元桥绕上了三环,车刚上三环主道,满眼黑压压的车顿时让我倒抽了一口冷气,不幸遭遇新世纪最严重地大堵车了!
    
     手机在这时候响起了,是妻子打来的。她说她已经步行回家了,孩子也已经接了。她问我估计什么时候能到家,我随口道:“12点以前能到家就不错了,你早点休息吧” 便挂了电话。小秘书终于对着镜子补好了唇膏,嘴唇在欧莱雅幻滟唇的滋润下如水晶般充满诱惑,又借口暖气太热而脱掉了大衣,一件天蓝的珠光紧身毛衫将身体的曲线勾勒得淋漓尽致,难道在这个滑车的雪夜会发生那人让我憧憬的故事吗?
    
     刚准备对小秘书有所暗示,电话铃又响了,是一个朋友,他也被堵在了半路,估计我和他的糟遇一样,便打来电话和我一起诉苦打发时间,不知不觉时间已经到了10点,车在打电话的功夫一直在慢慢地向前蠕动着,整整一个半小时大约只前进了不到2公里。在农馆附近 终于蠕动累了,便又集体趴了下来。
    
     再一次鼓起勇气准备开口,妻子的电话又来了,问我到了哪儿,我很不耐烦地告诉她还在农展馆。轻轻将右手搭在小秘书的肩膀上,她肩膀一动,我的手滑了下来。我不知道她这是婉拒还是半推半就,不敢贸然行事。我只好终止冒失的行为,开始有一搭没一搭地和她找话题闲扯,一个小时又过去了。我们从车里播放的席琳狄翁的CD谈到曾火爆的《泰坦尼克号》再委婉地转移到爱情这一主题,终于绕到了正题,再剩下的就只需要我的一点点暗示和小秘书的反应了 。
    
     就在这关键时刻,电话又来了,还是妻子的。还是问我现在到哪里了,我强忍火气告诉她我已经到了京广桥,妻子很开心地告诉我:“累死我了,终于快找到你了。我现在已经走到光华桥了,我怕你肚子饿,煲好了汤盛在保温瓶里就出来了,又怕坐车找你和你错过了,只好顺着三环走过来找你。对了,我就在靠你这边的马路牙子上往前走的,你要是看见我了 就叫我一声,我一路上使劲瞅车牌,眼睛都花了,可别把你漏过去了。” 妻子挂了电话,我开始发呆:妻子在满心期盼地为我煲汤,从潘家园步行到光华桥给我送来,可我却在车里计划着如何吸引另外一个女孩,我还是人吗?
    
     看我一脸沉重,小秘书问我:你刚才不是说要告诉我你心目中的罗丝是谁吗? 说啊 。我凝视着她的双眼,一字一顿道:“是我妻子,是马上给我送汤来的我的妻子。” 

  远远地看见了妻子的身影,我使劲地摁着喇叭,又一个劲地变更远近光灯,妻子兴奋地朝我挥手,我冲出去一把将她揽进了怀里。回到车时,小秘书已经悄悄地坐到了后排坐椅上。 我向妻子介绍她是我的同事,妻子很热情地邀请她赶快趁热喝一碗汤,那天的汤香浓无比,肌肠辘辘的我和小秘书将一保温瓶汤一扫而光。吃饱以后,我问妻子:孩子呢?妻子笑笑说:“我做好饭喂他吃完了 让他在家好好地呆着我就出来了,他现在可能还在家等着我们呢。” “你没吃饭?”我一愣 “本想过来陪你一块在车上吃的,你有朋友在,你们就先吃吧,我呆会儿回家再吃。”
    
     车到国贸桥小秘书下了车,10分钟后我的手机收到了一条短信息,是小秘书发的:你有一个多么好的妻子啊,幸亏我没有做出对不起她的事情,我喝了属于她的那份汤,绝不能再夺走属于她的男人,我承认我失败了,不是败给了人,是败给了一碗汤,一个如此用心煲汤的女人是值得每个男人为之珍惜一辈子的……
    
     妻子问我是谁的信息 我一笑:“是小秘书的,她在夸你的汤很好喝,夸我有眼力,找了一个优秀的妻子。” 妻子甜蜜地笑了,轻轻将头靠在了我的肩膀上,我顺势吻了下去。这一吻,这雪夜拥挤的三环上的一吻将让我铭记终身……
    
     这种女人是值得每个男人为之珍惜一辈子的……

posted @ 2006-04-03 16:22 高山流水 阅读(200) | 评论 (0)编辑 收藏

 

                                     南宁,今夜请允许我放荡(1)

我发现女人学坏,不是在诱惑下,而是在绝望与空虚里开始。当日子变得像伤口一样明明白白的让你痛时,很容易把身边随手可得的男人当成救命绳。“南宁,今夜请允许我放荡。”,我在键盘上敲下这几行字后,峰就从背后抱住我。他弓起身子,吻从肩膀上一直落到乳房上。这样吻着乳房的感觉是奇妙的,我的脸贴着他的脖子,可以咬他的耳朵,有种耳鬓厮磨的温存之感。他的双手从下往上拎起乳房,我知道我的乳房已失去了弹性,可是他完全可以在脂肪堆积的部份轻揉,这样就像提着两代豆浆。呵呵呵,这个时候我居然会想起豆浆,想起豆浆,我没法不想起乳白的精液,胃粘膜又开始操蛋了,想吐。觉得他的口水也是腥臭的,一把推开他。
  “行了,别闹了,我还想写点什么。”
  “写什么,我代表所有南宁男人,允许你放荡。
  “放荡也分两种的,一种是行为的放荡,一种是精神的放荡。精神上的放荡得经过自已这一关。”
  他借着蛮力将我放倒在床上,“不如先行为放荡一把吧,天这么晚了,待会儿,我就困了。这么久不见我,你不想吗?是不是又找了别的男人?”
  “胡说什么啊你。”我面对着他,轻轻吻了吻他的脸。女人也得安抚安抚男人的心,他毕竟一直毫无所求的在满足着我,没功劳也有苦劳。
  关上灯,屋子里黑下来,可是窗外的月光洒进来,雪白一片。风一吹,月光就像白窗纱在飘动。
  我感觉到他的进入十分干涩,闭上眼睛,将如水的月光与几颗零散的星星挡在眼皮之外,才慢慢湿润起来。
  我开始所谓的“叫床”,据说男人都喜欢这个,不就是哼哼呀呀吗?可是我越哼哼越觉得清醒,哼哼了一半,突然想到这部小说该怎么构思,主人翁的性格定位在什么类型。于是哼哼得特别有规律,“嗯,啊,嗯,啊。”就像在给他的动作喊口令。
  “你搞什么鬼?有你这样叫的吗?”他扑嗤一笑,有点软了,软了就会溜出来,溜出来不就是把关在笼子里的鸟放飞了吗?
  “对不起啊!刚想点事,噢,你带套了没有?”
  “带了,知道你嫌精子脏。没带早射了,还用磨这么久吗?”
  “嗯,辛苦了,那快点吧。”
  这一次他真是好卖力才爬上高潮。
  他如释重负,倒在枕头上便睡着了。我毫无睡意,看看了睡熟的他。有点怨恨他这么快入睡,虽然我不爱他,可他也应该一直紧张着我,怎么能在我睡之前睡着呢。
  这样想着,真想动手去揪揪他的鼻子,可是看到他睡着了,眉头还皱着,就不忍心了。
  我记得解放初期电影中有一句很经典的台词,“都是苦命的孩子。”我们都是离过婚的人,都是苦命的孩子。
  键盘的敲击声在小区的夜色中嘀嘀哒哒,我很喜欢这样的时候,月色好,周遭一片宁静,南宁只有在这个时候才真正属于我。
    
    “燕子,燕子。” 
  “唔。”我的睡意还很浓,睁不开眼,也不愿被打挠。怎么会有人叫我呢,不应该有人叫我的,在我的小屋里,从头天白天,睡到第二天晚上,也不会有人管我。因为在南宁,我几乎没有朋友,也没我几个认识我的人。这样一考虑,才发现自已是在峰的家里。她母亲送孩子去上学了,我必须赶在老人家回来之前离开这间房子,不然撞见就难看了。
  我们是在偷情,我得遵守游戏规则,何况这个规则是我自已定的。我承受不了老人家鄙视的目光。
  两人都离异了,好好谈恋爱人家又怎么鄙视你?问题不是这样。我们第一次见面便上床了,
  第二次见面也是为了上床。
  我跳出来,做了几个弹跳,推开窗,在窗前做了个深呼吸。
  “峰,我喜欢这个地方,多安静啊!要是在别的路段,真是炒死人了。”
  “喜欢就常来呗。”他在叠被子。
  “可以常来吗?嘻嘻嘻。”我走到峰身边,环抱着他的腰,将脸贴在他胸口上,用胯部磨他的敏感区。
  “别弄了,快去洗脸吧,我得上班,要不然它又不肯服贴了。”
  “嗯,我要嘛!”
  “只有十分钟时间,你洗漱完,我送你到公车站,然后我去上班。”

  我坐在峰的摩托车后,眯着眼看南宁的天空。原来晴朗的清晨,南宁也会有像湖北一样的朝霞,蓝的背景蓝得海,桔红的云彩红得像火,镶嵌在一起就像一块绚丽的锦,铺满了半边天。
  车走在白沙大道上,得从高大的木棉树下穿过,一路红彤彤的云,伸向地平线。
  “峰,现在是春天吗?”
  “当然是,你没看见木棉花开了吗?”
  “是春天为什么还这么冷?”
  “冷就抱紧一点。”
  我没有抱紧他,反而伸开双臂,闭上眼睛,想像鸟儿一样飞翔在朝阳与木棉花间,风呼呼的在我耳边吹过,我闭上眼睛想像着云团的包裹,与鸟儿的轻盈,让肺里装满南宁清晨最新鲜的空气。
  “喂,喂,你玩什么特技啊?等下交警看到,要罚款的味。”(听他那口纯正的南宁普通话我就想笑,每说一句话要在语末加个“味”的后缀音,就像打电话时人们常问的第一句话,“喂”的读法一样。如,“吃饭了味;走了味;洗澡了味”。)
  峰一边笑,一边大声的制止我。
  我不听他的,这种飞的感觉多好!摩托车开到了上坡的路段,车速加快了,我看着眼前的红云连成一片,好像我已踏在红云之上,冉冉升空。一朵木棉像玛瑙一样在我身边的树枝上跌下来,我似乎听到了砸在地上那清脆的声音。
  车到了5路公交车站,峰急急地下了车。
  我站着,将脖子扬起来,他便帮我解开头盔的扣子。我仰着头,刚好可以看到他低下来那张专注而微笑着的脸,很喜欢他这样细心,像照顾小女儿一样轻柔的动作。
  “燕子。”
  “嗯。”
  “怎么像个孩子一样呢?”
  是啊,快三十的人了,还像个孩子一样,难道是想扮成孩子的样子,换取男人的疼爱?可峰也不算很疼爱我,起码在床上,他从不跟我客气。
  我没有回答他,跳上驶过来的公交车,向他挥了挥手。
  
  我今天又起得太早了吧?车上只有三个人,坐在最后一排,看着前面摇摇晃晃的空位置,一会儿又坐满了人,再过一会儿,又空了,就像在变戏法。我的心不知飞到哪里去了。
南宁,今夜请允许我放荡(2)

 《三》
  回到秀灵路的出租房里,打开楼下的铁栅栏门,再打开院子里的大铁门,然后顺着黑忽忽的走廊摸索到楼梯口,轻抬脚,慢起步的走到我的房间门口。闭着眼睛在一大串钥匙中间摸到防盗门的钥匙,打开防盗门后的木门,才能正式进入我的房间。南方就是这样,都装出候门深锁的样子。千家万户一律装高档的防盗门。没有万贯家财也封闭得死死的,看样子像是防贼,可我感觉就像祖先是做贼的一样。不然谁有心把家藏得那么严密呢?弄得回家不像回家,像私入民宅。
  才一晚上功夫,房间里就有一股受潮的霉味儿。初春是回潮天,太阳越大,家里的水份与湿气就越多。墙壁上和地板上都泌出了水珠子,现在灰尘泡软了,就适合做清洁,轻轻一抹就掉了,楼上的老板娘估计正忙乎着,弄得砰嘣直响,这个家这么大,够她拾缀的,如果我有这么大的家业,估计也有这么勤劳,贫穷才是懒惰的根源。管它呢,好困,再睡一会儿。
  反手锁上门,又开了窗。一点可怜的阳光从小巷折射到对面楼的墙壁上。因为角度不够,阳光怎么也进不了我的屋子。
  守候着这束阳光,慢慢保留着粗重的呼吸,停止了脑子里那沉重的运转。
  
  大概已经睡饱了,手机正好把我吵醒。睁开眼一看,阳光不见了。屋子里暗得像晚上,打开灯。找到手机,是妈妈打来的电话。
  “燕。”
  “嗯。”
  “找到工作没有?那里怎么样啊?”
  “挺好的,找工作很容易,我要找个工资高点的,所以还没定下来,定下来我给家里打电话。”
  “一个人在外面要注意…..”
  “妈,我知道了,电话费不多了,就这样啊。”
  不是我妈提醒,我还真把找工作的事给忘记了。口袋里还有一百多块钱,能用几天呢?
  肚子有点饿,厨房有方便面,泡碗面吃,再去人才市场吧。
  我背着大书包,胡乱将头发用发带绑成一团,还留出长长一截发尾贴在后脑勺上,这造型多像个江湖游侠。看看天,又看看街上的人,才发现好像已经到了下午了。
  走到公交车牌下,原来我住的地方跟人才市场刚好在城市的两头。南宁公交车可不像武汉那么飚,坐公交车比走路还慢,司机这样小心谨慎,永远都没什么事故发生,可我讨厌这样温吞的行为,摇晃到琅西,估计也要一个多小时,去那儿说不定人家都下班了。
  还是明天吧,我习惯把一切交给明天,打了个方便面味的饱嗝,步伐坚定的逃进了网吧。
    我们应该感谢网络,当我躯壳无家可归,灵魂几乎魂飞魄散的时候,还有网吧能够收留我。上通宵网才收8块钱,比旅馆便宜多了,也比旅馆有意思。上一晚上网与睡一晚上觉,前者才是享受。尽管现在我已经租了房子,可觉得如果比睡觉,还是在网吧里趴在键盘上听QQ提示音入睡更快一些。
  我四下看着,一人一个小格子,低头关注着眼前的显示屏,完全超然忘我,有点阴森恐怖了吧,这一个个如饥似渴趴在键盘上的人,多像养在瓶子里的标本。
  网速有点慢,打开电脑还得花上几分钟。
  [对不起,你的余额已不多,请到服务台充值。]
  看到这个窗口,我知道我该省钱用了,其实上网也没什么,可是不上网就不行。站起来走到收银台,希望能翻出一张拾块的。没有拾块的,只有一张一百块的,和几张零票子,那么就是说,我身上的全部财产就是一百零几毛了。
  “小姐,请问您充多少钱?充五十元可以免费赠送十小时。”
  心够黑的,最后一百块,还想给我用去一半。
  “不,就充拾块吧。”
  “行,这是找您的钱,充值已完成了。”
  其实一百块一张和九张拾块握在手上感觉就是不一样。一张很单薄,给人感觉那像是最后的餐票,可是九张感觉希望还有很多,还有时间挣扎、抢救。
  拾元钱只能上十个小时。我得节省时间,所以两小时后就自觉下网了。
  从网吧出来,对面就是菜市场。买点菜,打发了肚皮,再想想这漫长的夜跟谁过吧。
  
  正吃着鸡旦面条,辉打电话进来了。
  “燕,你找到工作没有?”
  “找到怎么样?没找到怎么样?”
  “我是关心你。”
  “我说过了,不用你关心。你还是操心操心你自已吧?是不是想做爱了,憋不住了,想要我回去救火啊?!”
  “为什么每次打电话给你,你都这么生气?你是不是在那里不好啊?不好就回来啊!”
  “去死吧你!”
  他没什么对不起我的,初夜都献给我了,可我就是烦他。这世上多庸男,他偏是庸中之庸。除了会煮一手上好的榨菜肉丝面,有几两蛮劲与我拼得几回合,简直一无是处。
  我讨厌五百块钱一个月的工作都作得兴高采烈的男人,难道为了五百元的工资就应该付出青春吗?
  峰也是。在国营企业待了十年了,简直不敢想像,月工资不够一千,节假日也忙个不停,给人小总管当当居然就把国家的奴役当成终身的事业。
  我强烈鄙视这样的男人。
  所以虽然在Y市领着月薪八百,可油水丰厚的董事小秘,我还是毅然辞职来到南宁。这是个淘金的城市吗?每次我站在邕江桥头,真想看透这个城市里爬行着的人类,是什么力量令他们如此安于现状。可这是不可能的,看所有的人都那么安逸知足,只有我在挣扎,在困惑。也许这真不是一个淘金的城市,只是生活的温床。

南宁,今夜请允许我放荡(3)

 “峰,你中午有空吗?”
  “燕子,什么事?”他的语气温柔得让我吃惊,我们很熟吗?只不过上了两次床而已,大概两次。
  我有点意外的感动,通电话都显得跟我很亲热的样子,让我觉得他并不是在玩弄我,虽然我们从床上下来了,他还能保持着床上的温度,在现代,男人已鲜有这种品质。
  “我想去一下人才市场,你带我去好吗?”
  “行,没问题,你说什么时候去,我就赶过去接你。”
  相信吧,贫穷才能给人力量。今天的阳光也不错。我跳起来,打开电视机,跟着音乐摇晃了一下僵硬的四肢。找了套能让我看起来像女人的套裙,化了浅妆,又在箱子里翻出以前找工作的简历,登记照,身份证,还有剩下的银子。
  一切收拾停当了,我静静的坐着。
  在清醒中保持静止的状态,是我常玩的一个杀死时间的游戏。就如佛法中的坐禅,可是我心中无佛,有的恐怕也是魔。
  “他妈的,如果底资达不到一千,死也不做!”在学校工作了两年,出来又混了四年,再做小文员,我真不服气。怎么着也能做个办公室主任不是?相信南宁的机会多,而且凭我的姿色,和外来和尚这个资本,还怕人家不用我?!
  想到正得意处,峰在下面喊我。
  “燕子,快下来!”
  他这样的男人,好得让人触目惊心。我住在单人房里,他从来不直接冲上来骚扰我,总是将摩托车泊在街边的榕树下,耐心的等我下楼。
  峰,既然你如此仗义,等我找到好工作,一定好好犒劳你一晚。
  
  都说南宁是个骑在摩托车上的城市,也是唯一一个有摩托车道的城市。南宁市的工薪族没有汽车的大多都有摩托车。
  坐在摩托车后逛南宁市是比较惬意的。从民族大道的绿色通道里穿行,南国亚热带植物厚重的浓荫洒在我身上,就像披了一身绿色的纱缕。
  再往前,高楼林立,花团锦簇。这就是号称“小香港”、“小浦东”的琅东吗?
  “燕子,要是能在这儿买上一间房子,就满足了?”
  “呵呵,这就是你的理想吗?”
  “是啊,买房子是所有没有新房子人的理想。你好好努力吧,我们一个月那么点工资,做一辈子都不行啊!”
  “算了吧,想那么远累。过得去就算了。”
  “是啊,我也这样想,所以我那房子装修一下也能住,而且家里人不是很多。我都说叫你搬到我那儿住,省二百块房租了。”
  我笑了笑,紧紧抱了他,将脸贴着他厚实的背。奇怪,南宁这么陌生,我的感觉却还是这么踏实。

  这是我第几次进人才市场?少说也有十次了吧。老油条了,还怕啥。让峰在外面等我,我大踏步走进大厅,领了表格,办了人才卡。现场招聘会少说也有四五百人,那戴着近视镜的脑袋晃得我眼发花。
  自认为是“人才”或“千里马”毕业生生们都盯着招聘简章看。我暗暗冷笑,“看招聘简章有屁用啊?还不如根据自已的自身条件,找个比较好说话的招聘官。”
  工资招聘简章上一般都不会注明,真正能给高工资的公司不会用薪水来吸引人,更希望员工有奉献精神,注明高工资的公司,大多是空头支票,骗得了一时是一时。
  [大众文化传播公司,招聘,记者,网站文字编辑]
  这个倒合我味口。
  那位招聘的先生戴着眼镜,正跟一个刚出校门的女同学聊着。
  “发表过什么作品吗?”
  “没有,可是我在大学时做过系报编辑。”
  “在南宁有地方住吗?”
  “暂时没有。”
  “噢。”那位招聘官的表情有了细微的变化,女孩子赶忙补上一句。
  “但是我自已能想办法解决。”
  “好了,我得先看你的作品。最好你能将你的部份作品送到我们公司去,跟你的详细简历一起。”
  “我的简历在这里了,我非常喜欢这份工作,希望您能给个机会。”
  “行,行,行,我们会认真评审每一个面试者能力,机会对于大家都是公平的。下一位。”
  “可是,我真的很需要这份工作,您看……”那位女生还坐在位置上,一副你不录我,我就不走的架式。
  招聘的眼镜对准了我,“你是来面试的吗?”
  我故意两边看了看,“是我吗?”
  “是的。我是来面试的。”
  这招聘官原来是想借我来打发那女同学。那女孩子终于失望的站起来,一脸的颓丧。人生最痛苦的事情莫过于学了几年的本事,毕业出来却找不到工作。
  “填一份表格吧。”
  “我带了简历来。”
  “再填一份吧,你那个不符合我们公司的规定,我们这个要更详细些。”
  拿起圆珠笔,我看了看他的表格跟我的表格,就发现他在扯淡,除了格子比我大,内容都一样。
  我的钢笔字还行,圆珠笔就差了点。
  他看了看,皱了皱眉头。
  我心说:“不用我,没关系,别跟我打马虎。”
  “把日期填上。”
  我拿过来一看,真的日期漏了,可这有什么关系呢?
  “喂,今天几号啊?”
  他诧异的看着我,“你是在跟我说话吗?”
  我呵呵一笑。抬头看,招聘简章上是2003年2月15日。
  他接过简历,“这个电话是你的吧?”
  “是的,24小时开机。”
  “行,那谢谢你。”
  他还算明白过弯来了。一个管招聘的大不了也就是公司一个人力资源部经理,大得过办公室主任吗?当然我还不是办公室主任,也不是他的办公室主任。

南宁,今夜请允许我放荡(4)


  我就下楼了。峰忙问我,“情况怎么样?”
  我戴上头盔坐上车,满有把握地说:“行了,送我回去等消息吧。”
  他不放心,还将车支着,“感觉怎么样啊?”
  “不怎么样啊,反正我是有实力的,怕什么?”我冲他暖昧地一笑。
  “投了多少份简历?手上有的就全投了,机会多一点儿。”他没有心情跟我说笑,固执的将车停在那儿。
  “是啊,全投了。”
    其实我骗他。这个招聘会虽然有一百多个位置,可没有一个是适合我的。
  名义上招秘书,其实秘书都有人选了,招的只是文员。虽然招的是文员,可支付的都是打字员的薪水。招聘启事写招业务经理的,实际全是招业务员,业务员的薪水,不如一个餐厅服务员。南宁的现状就这样了,服务员工资400—600元;业务员工资600—800元;业务主管1000-1200元;秘书800—1200。再往高处走,我的学历也不够。
  我投了一个主管经理,和一个董事长秘书。
  回来的路上,我不再乐观了。这段日子在人才网上已经投了不少简历了,所以一直懒得进现场招聘会。可是十天过去了,邮箱里连个自动回复也没有。
  我想,等我房子租下来,钱也该用完了。只能走一步算一步。实在不行,也得回头当个小小的业务员了。
  “燕子,实在有什么困难你告诉我啊。”峰见不我说话,知道我心情沉重了。
  “行了,我自已能搞定,瞎操什么心,你看这不是挺顺利的吗?三天后等消息。”我可不想把这压抑的心情传染给唯一一个认识的朋友。
  “峰。”
  “嗯,什么?”
  “你对我真好。”我学着台湾片里的语气说。
  他哈哈大笑,“你说什么?不是在逗我开心吧。”
  我也觉得,逗他开心挺容易的。“峰,我们回家做爱吧?”
  他又是一阵哈哈大笑,难道做爱很好笑吗?是不是没有人这样明目张胆的向他求欢过,有些受宠若惊了?
  做吧,做一次吧!累了什么都不想,对他好一点,爱他多一点,他有一颗如此善良的心,爱他也不会有错。
  我们一起回了我的小房子。
  我打开电视。那个节目里刚好有大兵,我喜欢这湖南小子,看他的节目,有一种想一边揍他,一边乐的冲动。我一边骂着大兵这臭小子,一边跟着电视里的观众呵呵傻笑。
  峰突然说,:“燕子,你到底那个不那个?”
  噢,我一下子忘了,说好了回来做爱的。
  “你那个的话,我们就那个,你不那个,我得回去上班了,我请假出来的。”
  他说这话时很不自在,那腼腆的样子,像个处男,一点都不像有个女儿的父亲。
  “什么那个,这个的?”我像蛇一样缠到他身上,将他压倒在床上,手伸向他那里,故意逗着他。
  “哈哈哈!”他又笑了。原来女人的放荡最容易让男人开怀大笑吗?我有些得意。又似乎心里暗暗生些恨意,轻轻咬住他的嘴,狠狠的吸着,将舌伸进他嘴里,搅动着。然后粗暴的解开他的上衣,扯开他的皮带。
  他的呼吸变得急促,“强奸的感觉比较好,是吗?”我在他耳边轻轻问着。
  “是的,亲爱的,你真是个魔鬼,我受不了你,一会冷,一会热。啊——”
  我已经上去了,并且是疯狂的,粗暴的。床在摇晃,砰砰的响,那节奏让寡居的人听了想自杀。
  最后的一刻,我问他,“有没有别的办法?不要射在里面。”
  他说“有。”
  说完就拔了出来,将那家伙握在手里抽蓄着。我躲进卫生间,不敢多看。
   
2003年2月19日。

  不知什么时候,下起了雨。我哪儿都不想去,睡着,等手机响,可是看到电话进来,似乎又恨某人挠了我的清静。
  中午接到去桂春路复试的通知。
  打电话来的小姐很懂礼貌。“请问您是叶海燕小姐吗?今天下午三点您有空来我们公司参加第二次面试吗?”
  “噢,是的,谢谢您,谢谢您。请问贵公司地址?”
  “桂春路85号。”
  “谢谢,我一定到。”
  我觉得自已在这个电话面前,有点过于卑躬屈膝了,完全没有初来南宁的信心与傲气。明明不知道是哪家公司,生怕人家怀疑一“稿”多投,硬是不敢问对方公司的名字。
  从秀灵路坐公车到桂春路,足足花了一个半小时。找到那女孩子说的地址,轻轻敲开门。
  看来一间简陋的四房二厅的门宅,客厅里挂着一个新做的匾,用强劲的书法写着:大众文化传播有限责任公司。
  “原来如此。”
  人才市场见过的那位身材苗条的面试官坐在几张桌子中摆的文件最多的一张前。
  “叶小姐,您好!请坐,请坐。”
  “行,不用客气了。”
  “先熟悉一下我们公司的运作模式好吗?你是应聘做网站编辑吧?我们已经上网看过你的文集,感觉还不错。”
  我拿着他递过来的一份《大公报》
  “嗯?《大公报》?这不是香港的报纸吗?”我确实不知道文化界是怎么挣钱的,所以对他们的运作模式,有点兴趣。
  也许沾上港澳台他们都觉得是实力,接下来才给我南宁市发行量最高的《南国早报》。
  “你看这则广告,这是我们准备做的一个文化展。”
  所谓的文化展,其实也就是土特产展,说白了就是文艺广告活动。一扯上广告,毕然有跟商家的业务合作,有业务合作就需要能说会道的业务员。我马上就猜到他们名义上招的是网站编辑,其实要的只是能拉广告的业务员。
  我当时就决定,撤人!
  可是大老远跑过来总得歇口气吧?于是端着他倒的茶,一边喝一边看着《大公报》与《南国早报》。
  这时,有人敲门,开门一看,原来又一个上钩的来了。
  也是位女士,穿着牛仔衣,紧身裤,相貌一般,可眉头扬得挺高,打定要高调的样子。
  “我上那个原来在南国早报干过的。应聘记者。”
  “您坐!您坐!”
  “不坐了,我顺便上来看看,你们老板呢?”她肩上挎着包,真不想坐的样子。
  那位同志又递上《大公报》和《南国早报》。
  “这些我不用看了,我做这一行做了七八年,我懂。你们这儿的记者用下乡组稿吗?”
  刚才那个土特产的文化展,不下乡组稿,估计不行,我这边猜着,那位同志果然也尴尬地点了点头。
  “要下乡啊?我以为不用下乡呢?这年头,谁还想下乡啊?我先走了,你们忙啊。”
  然后就来个干脆的右转,踩着高跟鞋嘀哒嘀哒的走了。



posted @ 2006-04-03 16:21 高山流水 阅读(697) | 评论 (0)编辑 收藏

C语言测试是招聘嵌入式系统程序员过程中必须而且有效的方法。这些年,我既参加也组织

了许多这种测试,在这过程中我意识到这些测试能为带面试者和被面试者提供许多有用信

息,此外,撇开面试的压力不谈,这种测试也是相当有趣的。
从被面试者的角度来讲,你能了解许多关于出题者或监考者的情况。这个测试只是出题者

为显示其对ANSI标准细节的知识而不是技术技巧而设计吗?这个愚蠢的问题吗?如要你答

出某个字符的ASCII值。这些问题着重考察你的系统调用和内存分配策略方面的能力吗?这

标志着出题者也许花时间在微机上而不上在嵌入式系统上。如果上述任何问题的答案是“

是”的话,那么我知道我得认真考虑我是否应该去做这份工作。
从面试者的角度来讲,一个测试也许能从多方面揭示应试者的素质:最基本的,你能了解

应试者C语言的水平。不管怎么样,看一下这人如何回答他不会的问题也是满有趣。应试者

是以好的直觉做出明智的选择,还是只是瞎蒙呢?当应试者在某个问题上卡住时是找借口

呢,还是表现出对问题的真正的好奇心,把这看成学习的机会呢?我发现这些信息与他们

的测试成绩一样有用。
有了这些想法,我决定出一些真正针对嵌入式系统的考题,希望这些令人头痛的考题能给

正在找工作的人一点帮住。这些问题都是我这些年实际碰到的。其中有些题很难,但它们

应该都能给你一点启迪。
这个测试适于不同水平的应试者,大多数初级水平的应试者的成绩会很差,经验丰富的程

序员应该有很好的成绩。为了让你能自己决定某些问题的偏好,每个问题没有分配分数,

如果选择这些考题为你所用,请自行按你的意思分配分数。
预处理器(Preprocessor)

1 . 用预处理指令#define 声明一个常数,用以表明1年中有多少秒(忽略闰年问题)


#define SECONDS_PER_YEAR (60 * 60 * 24 * 365)UL
我在这想看到几件事情:
•; #define 语法的基本知识(例如:不能以分号结束,括号的使用,等等)


•; 懂得预处理器将为你计算常数表达式的值,因此,直接写出你是如何计算一年中

有多少秒而不是计算出实际的值,是更清晰而没有代价的。
•; 意识到这个表达式将使一个16位机的整型数溢出-因此要用到长整型符号L,告诉

编译器这个常数是的长整型数。
•; 如果你在你的表达式中用到UL(表示无符号长整型),那么你有了一个好的起点

。记住,第一印象很重要。
2 . 写一个“标准”宏MIN ,这个宏输入两个参数并返回较小的一个。


#define MIN(A,B) ((A) <= (B) ? (A) : (B)) 

这个测试是为下面的目的而设的:
&#8226;; 标识#define在宏中应用的基本知识。这是很重要的,因为直到嵌入(inline)操

作符变为标准C的一部分,宏是方便产生嵌入代码的唯一方法,对于嵌入式系统来说,为了

能达到要求的性能,嵌入代码经常是必须的方法。
&#8226;; 三重条件操作符的知识。这个操作符存在C语言中的原因是它使得编译器能产生

比if-then-else更优化的代码,了解这个用法是很重要的。
&#8226;; 懂得在宏中小心地把参数用括号括起来
&#8226;; 我也用这个问题开始讨论宏的副作用,例如:当你写下面的代码时会发生什么事



least = MIN(*p++, b);


3. 预处理器标识#error的目的是什么?
如果你不知道答案,请看参考文献1。这问题对区分一个正常的伙计和一个书呆子是很有用

的。只有书呆子才会读C语言课本的附录去找出象这种问题的答案。当然如果你不是在找一

个书呆子,那么应试者最好希望自己不要知道答案。
死循环(Infinite loops)


4. 嵌入式系统中经常要用到无限循环,你怎么样用C编写死循环呢?
这个问题用几个解决方案。我首选的方案是:

while(1)
{
?}



一些程序员更喜欢如下方案:

for(;;)
{
?}



这个实现方式让我为难,因为这个语法没有确切表达到底怎么回事。如果一个应试者给出

这个作为方案,我将用这个作为一个机会去探究他们这样做的基本原理。如果他们的基本

答案是:“我被教着这样做,但从没有想到过为什么。”这会给我留下一个坏印象。
第三个方案是用 goto

Loop:
...
goto Loop;


应试者如给出上面的方案,这说明或者他是一个汇编语言程序员(这也许是好事)或者他

是一个想进入新领域的BASIC/FORTRAN程序员。

数据声明(Data declarations) 

5. 用变量a给出下面的定义
a) 一个整型数(An integer) 
b)一个指向整型数的指针( A pointer to an integer) 
c)一个指向指针的的指针,它指向的指针是指向一个整型数( A pointer to a pointer 

to an intege)r 
d)一个有10个整型数的数组( An array of 10 integers) 
e) 一个有10个指针的数组,该指针是指向一个整型数的。(An array of 10 pointers t

o integers) 
f) 一个指向有10个整型数数组的指针( A pointer to an array of 10 integers) 
g) 一个指向函数的指针,该函数有一个整型参数并返回一个整型数(A pointer to a fu

nction that takes an integer as an argument and returns an integer) 
h)一个有10个指针的数组,该指针指向一个函数,该函数有一个整型参数并返回一个整型

数( An array of ten pointers to functions that take an integer argument and r

eturn an integer )

答案是: 
a) int a; // An integer 
b) int *a; // A pointer to an integer 
c) int **a; // A pointer to a pointer to an integer 
d) int a[10]; // An array of 10 integers 
e) int *a[10]; // An array of 10 pointers to integers 
f) int (*a)[10]; // A pointer to an array of 10 integers 
g) int (*a)(int); // A pointer to a function a that takes an integer argument 

and returns an integer 
h) int (*a[10])(int); // An array of 10 pointers to functions that take an int

eger argument and return an integer 
人们经常声称这里有几个问题是那种要翻一下书才能回答的问题,我同意这种说法。当我

写这篇文章时,为了确定语法的正确性,我的确查了一下书。但是当我被面试的时候,我

期望被问到这个问题(或者相近的问题)。因为在被面试的这段时间里,我确定我知道这

个问题的答案。应试者如果不知道所有的答案(或至少大部分答案),那么也就没有为这

次面试做准备,如果该面试者没有为这次面试做准备,那么他又能为什么出准备呢?
Static 
6. 关键字static的作用是什么?
这个简单的问题很少有人能回答完全。在C语言中,关键字static有三个明显的作用:
&#8226;; 在函数体,一个被声明为静态的变量在这一函数被调用过程中维持其值不变。


&#8226;; 在模块内(但在函数体外),一个被声明为静态的变量可以被模块内所用函数访

问,但不能被模块外其它函数访问。它是一个本地的全局变量。
&#8226;; 在模块内,一个被声明为静态的函数只可被这一模块内的其它函数调用。那就是

,这个函数被限制在声明它的模块的本地范围内使用。
大多数应试者能正确回答第一部分,一部分能正确回答第二部分,同是很少的人能懂得第

三部分。这是一个应试者的严重的缺点,因为他显然不懂得本地化数据和代码范围的好处

和重要性。


Const 

7.关键字const有什么含意?
我只要一听到被面试者说:“const意味着常数”,我就知道我正在和一个业余者打交道。

去年Dan Saks已经在他的文章里完全概括了const的所有用法,因此ESP(译者:Embedded 

Systems Programming)的每一位读者应该非常熟悉const能做什么和不能做什么.如果你从

没有读到那篇文章,只要能说出const意味着“只读”就可以了。尽管这个答案不是完全的

答案,但我接受它作为一个正确的答案。(如果你想知道更详细的答案,仔细读一下Saks

的文章吧。)
如果应试者能正确回答这个问题,我将问他一个附加的问题:
下面的声明都是什么意思?

const int a;
int const a;
const int *a;
int * const a;
int const * a const;

/******/
前两个的作用是一样,a是一个常整型数。第三个意味着a是一个指向常整型数的指针(也

就是,整型数是不可修改的,但指针可以)。第四个意思a是一个指向整型数的常指针(也

就是说,指针指向的整型数是可以修改的,但指针是不可修改的)。最后一个意味着a是一

个指向常整型数的常指针(也就是说,指针指向的整型数是不可修改的,同时指针也是不

可修改的)。如果应试者能正确回答这些问题,那么他就给我留下了一个好印象。顺带提

一句,也许你可能会问,即使不用关键字const,也还是能很容易写出功能正确的程序,那

么我为什么还要如此看重关键字const呢?我也如下的几下理由:
&#8226;; 关键字const的作用是为给读你代码的人传达非常有用的信息,实际上,声明一

个参数为常量是为了告诉了用户这个参数的应用目的。如果你曾花很多时间清理其它人留

下的垃圾,你就会很快学会感谢这点多余的信息。(当然,懂得用const的程序员很少会留

下的垃圾让别人来清理的。)
&#8226;; 通过给优化器一些附加的信息,使用关键字const也许能产生更紧凑的代码。
&#8226;; 合理地使用关键字const可以使编译器很自然地保护那些不希望被改变的参数,

防止其被无意的代码修改。简而言之,这样可以减少bug的出现。
Volatile 

8. 关键字volatile有什么含意?并给出三个不同的例子。
一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去

假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读

取这个变量的值,而不是使用保存在寄存器里的备份。下面是volatile变量的几个例子:


&#8226;; 并行设备的硬件寄存器(如:状态寄存器)
&#8226;; 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)
&#8226;; 多线程应用中被几个任务共享的变量

回答不出这个问题的人是不会被雇佣的。我认为这是区分C程序员和嵌入式系统程序员的最

基本的问题。搞嵌入式的家伙们经常同硬件、中断、RTOS等等打交道,所有这些都要求用

到volatile变量。不懂得volatile的内容将会带来灾难。
假设被面试者正确地回答了这是问题(嗯,怀疑是否会是这样),我将稍微深究一下,看

一下这家伙是不是直正懂得volatile完全的重要性。
&#8226;; 一个参数既可以是const还可以是volatile吗?解释为什么。
&#8226;; 一个指针可以是volatile 吗?解释为什么。
&#8226;; 下面的函数有什么错误:
int square(volatile int *ptr)
{
return *ptr * *ptr;
}

下面是答案:
&#8226;; 是的。一个例子是只读的状态寄存器。它是volatile因为它可能被意想不到地改

变。它是const因为程序不应该试图去修改它。
&#8226;; 是的。尽管这并不很常见。一个例子是当一个中服务子程序修该一个指向一个b

uffer的指针时。
&#8226;; 这段代码有点变态。这段代码的目的是用来返指针*ptr指向值的平方,但是,由

于*ptr指向一个volatile型参数,编译器将产生类似下面的代码:


int square(volatile int *ptr) 
{
int a,b;
a = *ptr;
b = *ptr;
return a * b;
}


由于*ptr的值可能被意想不到地该变,因此a和b可能是不同的。结果,这段代码可能返不

是你所期望的平方值!正确的代码如下:

long square(volatile int *ptr) 
{
int a;
a = *ptr;
return a * a;
}

位操作(Bit manipulation) 

9. 嵌入式系统总是要用户对变量或寄存器进行位操作。给定一个整型变量a,写两段代码

,第一个设置a的bit 3,第二个清除a 的bit 3。在以上两个操作中,要保持其它位不变。


对这个问题有三种基本的反应
&#8226;; 不知道如何下手。该被面者从没做过任何嵌入式系统的工作。
&#8226;; 用bit fields。Bit fields是被扔到C语言死角的东西,它保证你的代码在不同

编译器之间是不可移植的,同时也保证了的你的代码是不可重用的。我最近不幸看到Infi

neon为其较复杂的通信芯片写的驱动程序,它用到了bit fields因此完全对我无用,因为

我的编译器用其它的方式来实现bit fields的。从道德讲:永远不要让一个非嵌入式的家

伙粘实际硬件的边。
&#8226;; 用 #defines 和 bit masks 操作。这是一个有极高可移植性的方法,是应该被

用到的方法。最佳的解决方案如下:


#define BIT3 (0x1 << 3)
static int a;

void set_bit3(void) {
a |= BIT3;
}
void clear_bit3(void) {
a &= ~BIT3;
}

一些人喜欢为设置和清除值而定义一个掩码同时定义一些说明常数,这也是可以接受的。

我希望看到几个要点:说明常数、|=和&=~操作。
访问固定的内存位置(Accessing fixed memory locations) 

10. 嵌入式系统经常具有要求程序员去访问某特定的内存位置的特点。在某工程中,要求

设置一绝对地址为0x67a9的整型变量的值为0xaa66。编译器是一个纯粹的ANSI编译器。写

代码去完成这一任务。
这一问题测试你是否知道为了访问一绝对地址把一个整型数强制转换(typecast)为一指

针是合法的。这一问题的实现方式随着个人风格不同而不同。典型的类似代码如下:

int *ptr;
ptr = (int *)0x67a9;
*ptr = 0xaa55;

A more obscure approach is: 
一个较晦涩的方法是:

*(int * const)(0x67a9) = 0xaa55;

即使你的品味更接近第二种方案,但我建议你在面试时使用第一种方案。

中断(Interrupts) 

11. 中断是嵌入式系统中重要的组成部分,这导致了很多编译开发商提供一种扩展—让标

准C支持中断。具代表事实是,产生了一个新的关键字__interrupt。下面的代码就使用了

__interrupt关键字去定义了一个中断服务子程序(ISR),请评论一下这段代码的。

__interrupt double compute_area (double radius) 
{
double area = PI * radius * radius;
printf("\nArea = %f", area);
return area;
}

这个函数有太多的错误了,以至让人不知从何说起了:
&#8226;; ISR 不能返回一个值。如果你不懂这个,那么你不会被雇用的。
&#8226;; ISR 不能传递参数。如果你没有看到这一点,你被雇用的机会等同第一项。
&#8226;; 在许多的处理器/编译器中,浮点一般都是不可重入的。有些处理器/编译器需要

让额处的寄存器入栈,有些处理器/编译器就是不允许在ISR中做浮点运算。此外,ISR应该

是短而有效率的,在ISR中做浮点运算是不明智的。
&#8226;; 与第三点一脉相承,printf()经常有重入和性能上的问题。如果你丢掉了第三和

第四点,我不会太为难你的。不用说,如果你能得到后两点,那么你的被雇用前景越来越

光明了。

*****
代码例子(Code examples)

12 . 下面的代码输出是什么,为什么?

void foo(void)
{
unsigned int a = 6;
int b = -20;
(a+b > 6) ? puts("> 6") : puts("<= 6");
}
这个问题测试你是否懂得C语言中的整数自动转换原则,我发现有些开发者懂得极少这些东

西。不管如何,这无符号整型问题的答案是输出是 ”>6”。原因是当表达式中存在有符号

类型和无符号类型时所有的操作数都自动转换为无符号类型。 因此-20变成了一个非常大

的正整数,所以该表达式计算出的结果大于6。这一点对于应当频繁用到无符号数据类型的

嵌入式系统来说是丰常重要的。如果你答错了这个问题,你也就到了得不到这份工作的边

缘。
13. 评价下面的代码片断:

unsigned int zero = 0;
unsigned int compzero = 0xFFFF; 
/*1's complement of zero */

对于一个int型不是16位的处理器为说,上面的代码是不正确的。应编写如下:

unsigned int compzero = ~0;

这一问题真正能揭露出应试者是否懂得处理器字长的重要性。在我的经验里,好的嵌入式

程序员非常准确地明白硬件的细节和它的局限,然而PC机程序往往把硬件作为一个无法避

免的烦恼。
到了这个阶段,应试者或者完全垂头丧气了或者信心满满志在必得。如果显然应试者不是

很好,那么这个测试就在这里结束了。但如果显然应试者做得不错,那么我就扔出下面的

追加问题,这些问题是比较难的,我想仅仅非常优秀的应试者能做得不错。提出这些问题

,我希望更多看到应试者应付问题的方法,而不是答案。不管如何,你就当是这个娱乐吧



动态内存分配(Dynamic memory allocation) 
14. 尽管不像非嵌入式计算机那么常见,嵌入式系统还是有从堆(heap)中动态分配内存

的过程的。那么嵌入式系统中,动态分配内存可能发生的问题是什么?
这里,我期望应试者能提到内存碎片,碎片收集的问题,变量的持行时间等等。这个主题

已经在ESP杂志中被广泛地讨论过了(主要是 P.J. Plauger, 他的解释远远超过我这里能

提到的任何解释),所有回过头看一下这些杂志吧!让应试者进入一种虚假的安全感觉后

,我拿出这么一个小节目:
下面的代码片段的输出是什么,为什么?

char *ptr;
if ((ptr = (char *)malloc(0)) == 
NULL) 
else
puts("Got a null pointer");
puts("Got a valid pointer");

这是一个有趣的问题。最近在我的一个同事不经意把0值传给了函数malloc,得到了一个合

法的指针之后,我才想到这个问题。这就是上面的代码,该代码的输出是“Got a valid 

pointer”。我用这个来开始讨论这样的一问题,看看被面试者是否想到库例程这样做是正

确。得到正确的答案固然重要,但解决问题的方法和你做决定的基本原理更重要些。
Typedef 

15 Typedef 在C语言中频繁用以声明一个已经存在的数据类型的同义字。也可以用预处理

器做类似的事。例如,思考一下下面的例子:

#define dPS struct s *
typedef struct s * tPS;

以上两种情况的意图都是要定义dPS 和 tPS 作为一个指向结构s指针。哪种方法更好呢?

(如果有的话)为什么?
这是一个非常微妙的问题,任何人答对这个问题(正当的原因)是应当被恭喜的。答案是

:typedef更好。思考下面的例子:

dPS p1,p2;
tPS p3,p4;

第一个扩展为

struct s * p1, p2;

.
上面的代码定义p1为一个指向结构的指,p2为一个实际的结构,这也许不是你想要的。第

二个例子正确地定义了p3 和p4 两个指针。

晦涩的语法

16 . C语言同意一些令人震惊的结构,下面的结构是合法的吗,如果是它做些什么?

int a = 5, b = 7, c;
c = a+++b;

这个问题将做为这个测验的一个愉快的结尾。不管你相不相信,上面的例子是完全合乎语

法的。问题是编译器如何处理它?水平不高的编译作者实际上会争论这个问题,根据最处

理原则,编译器应当能处理尽可能所有合法的用法。因此,上面的代码被处理成:

c = a++ + b;

因此, 这段代码持行后a = 6, b = 7, c = 12。
如果你知道答案,或猜出正确答案,做得好。如果你不知道答案,我也不把这个当作问题

。我发现这个问题的最大好处是这是一个关于代码编写风格,代码的可读性,代码的可修

改性的好的话题。
好了,伙计们,你现在已经做完所有的测试了。

posted @ 2006-04-03 16:04 高山流水 阅读(246) | 评论 (0)编辑 收藏

 
设计模式之Factory
板桥里人 http://www.jdon.com 2002/10/07
定义:提供创建对象的接口.

为何使用?
工厂模式是我们最常用的模式了,著名的Jive论坛 ,就大量使用了工厂模式,工厂模式在Java程序系统可以说是随处可见。

为什么工厂模式是如此常用?因为工厂模式就相当于创建实例对象的new,我们经常要根据类Class生成实例对象,如A a=new A() 工厂模式也是用来创建实例对象的,所以以后new时就要多个心眼,是否可以考虑实用工厂模式,虽然这样做,可能多做一些工作,但会给你系统带来更大的可扩展性和尽量少的修改量。

我们以类Sample为例, 如果我们要创建Sample的实例对象:

Sample sample=new Sample();

可是,实际情况是,通常我们都要在创建sample实例时做点初始化的工作,比如赋值 查询数据库等。

首先,我们想到的是,可以使用Sample的构造函数,这样生成实例就写成:

Sample sample=new Sample(参数);

但是,如果创建sample实例时所做的初始化工作不是象赋值这样简单的事,可能是很长一段代码,如果也写入构造函数中,那你的代码很难看了(就需要Refactor重整)。

为什么说代码很难看,初学者可能没有这种感觉,我们分析如下,初始化工作如果是很长一段代码,说明要做的工作很多,将很多工作装入一个方法中,相当于将很多鸡蛋放在一个篮子里,是很危险的,这也是有背于Java面向对象的原则,面向对象的封装(Encapsulation)和分派(Delegation)告诉我们,尽量将长的代码分派“切割”成每段,将每段再“封装”起来(减少段和段之间偶合联系性),这样,就会将风险分散,以后如果需要修改,只要更改每段,不会再发生牵一动百的事情。

在本例中,首先,我们需要将创建实例的工作与使用实例的工作分开, 也就是说,让创建实例所需要的大量初始化工作从Sample的构造函数中分离出去。

这时我们就需要Factory工厂模式来生成对象了,不能再用上面简单new Sample(参数)。还有,如果Sample有个继承如MySample, 按照面向接口编程,我们需要将Sample抽象成一个接口.现在Sample是接口,有两个子类MySample 和HisSample .我们要实例化他们时,如下:

Sample mysample=new MySample();
Sample hissample=new HisSample();

随着项目的深入,Sample可能还会"生出很多儿子出来", 那么我们要对这些儿子一个个实例化,更糟糕的是,可能还要对以前的代码进行修改:加入后来生出儿子的实例.这在传统程序中是无法避免的.

但如果你一开始就有意识使用了工厂模式,这些麻烦就没有了.

工厂方法
你会建立一个专门生产Sample实例的工厂:

public class Factory{

  public static Sample creator(int which){

  //getClass 产生Sample 一般可使用动态类装载装入类。
  if (which==1)
    return new SampleA();
  else if (which==2)
    return new SampleB();

  }

}

那么在你的程序中,如果要实例化Sample时.就使用

Sample sampleA=Factory.creator(1);

这样,在整个就不涉及到Sample的具体子类,达到封装效果,也就减少错误修改的机会,这个原理可以用很通俗的话来比喻:就是具体事情做得越多,越容易范错误.这每个做过具体工作的人都深有体会,相反,官做得越高,说出的话越抽象越笼统,范错误可能性就越少.好象我们从编程序中也能悟出人生道理?呵呵.

使用工厂方法 要注意几个角色,首先你要定义产品接口,如上面的Sample,产品接口下有Sample接口的实现类,如SampleA,其次要有一个factory类,用来生成产品Sample,如下图,最右边是生产的对象Sample:

进一步稍微复杂一点,就是在工厂类上进行拓展,工厂类也有继承它的实现类concreteFactory了

抽象工厂
工厂模式中有: 工厂方法(Factory Method) 抽象工厂(Abstract Factory).

这两个模式区别在于需要创建对象的复杂程度上。如果我们创建对象的方法变得复杂了,如上面工厂方法中是创建一个对象Sample,如果我们还有新的产品接口Sample2.

这里假设:Sample有两个concrete类SampleA和SamleB,而Sample2也有两个concrete类Sample2A和SampleB2

那么,我们就将上例中Factory变成抽象类,将共同部分封装在抽象类中,不同部分使用子类实现,下面就是将上例中的Factory拓展成抽象工厂:

public abstract class Factory{

  public abstract Sample creator();

  public abstract Sample2 creator(String name);

}

public class SimpleFactory extends Factory{

  public Sample creator(){
    .........
    return new SampleA
  }

  public Sample2 creator(String name){
    .........
    return new Sample2A
  }

}

public class BombFactory extends Factory{

  public Sample creator(){
    ......
    return new SampleB
  }

  public Sample2 creator(String name){
    ......
    return new Sample2B
  }

}

 

从上面看到两个工厂各自生产出一套Sample和Sample2,也许你会疑问,为什么我不可以使用两个工厂方法来分别生产Sample和Sample2?

抽象工厂还有另外一个关键要点,是因为 SimpleFactory内,生产Sample和生产Sample2的方法之间有一定联系,所以才要将这两个方法捆绑在一个类中,这个工厂类有其本身特征,也许制造过程是统一的,比如:制造工艺比较简单,所以名称叫SimpleFactory。

在实际应用中,工厂方法用得比较多一些,而且是和动态类装入器组合在一起应用,

举例

我们以Jive的ForumFactory为例,这个例子在前面的Singleton模式中我们讨论过,现在再讨论其工厂模式:

public abstract class ForumFactory {

  private static Object initLock = new Object();
  private static String className = "com.jivesoftware.forum.database.DbForumFactory";
  private static ForumFactory factory = null;

  public static ForumFactory getInstance(Authorization authorization) {
    //If no valid authorization passed in, return null.
    if (authorization == null) {
      return null;
    }
    //以下使用了Singleton 单态模式
    if (factory == null) {
      synchronized(initLock) {
        if (factory == null) {
            ......

          try {
              //动态转载类
              Class c = Class.forName(className);
              factory = (ForumFactory)c.newInstance();
          }
          catch (Exception e) {
              return null;
          }
        }
      }
    }

    //Now, 返回 proxy.用来限制授权对forum的访问
    return new ForumFactoryProxy(authorization, factory,
                    factory.getPermissions(authorization));
  }

  //真正创建forum的方法由继承forumfactory的子类去完成.
  public abstract Forum createForum(String name, String description)
  throws UnauthorizedException, ForumAlreadyExistsException;

  ....

}

 

 

因为现在的Jive是通过数据库系统存放论坛帖子等内容数据,如果希望更改为通过文件系统实现,这个工厂方法ForumFactory就提供了提供动态接口:

private static String className = "com.jivesoftware.forum.database.DbForumFactory";

你可以使用自己开发的创建forum的方法代替com.jivesoftware.forum.database.DbForumFactory就可以.

在上面的一段代码中一共用了三种模式,除了工厂模式外,还有Singleton单态模式,以及proxy模式,proxy模式主要用来授权用户对forum的访问,因为访问forum有两种人:一个是注册用户 一个是游客guest,那么那么相应的权限就不一样,而且这个权限是贯穿整个系统的,因此建立一个proxy,类似网关的概念,可以很好的达到这个效果.  

看看Java宠物店中的CatalogDAOFactory:

public class CatalogDAOFactory {

 

  /**

  * 本方法制定一个特别的子类来实现DAO模式。
  * 具体子类定义是在J2EE的部署描述器中。
  */

  public static CatalogDAO getDAO() throws CatalogDAOSysException {

    CatalogDAO catDao = null;

    try {

      InitialContext ic = new InitialContext();
      //动态装入CATALOG_DAO_CLASS
      //可以定义自己的CATALOG_DAO_CLASS,从而在无需变更太多代码
      //的前提下,完成系统的巨大变更。

      String className =(String) ic.lookup(JNDINames.CATALOG_DAO_CLASS);

      catDao = (CatalogDAO) Class.forName(className).newInstance();

    } catch (NamingException ne) {

      throw new CatalogDAOSysException("
        CatalogDAOFactory.getDAO: NamingException while
          getting DAO type : \n" + ne.getMessage());

    } catch (Exception se) {

      throw new CatalogDAOSysException("
        CatalogDAOFactory.getDAO: Exception while getting
          DAO type : \n" + se.getMessage());

    }

    return catDao;

  }

}


CatalogDAOFactory是典型的工厂方法,catDao是通过动态类装入器className获得CatalogDAOFactory具体实现子类,这个实现子类在Java宠物店是用来操作catalog数据库,用户可以根据数据库的类型不同,定制自己的具体实现子类,将自己的子类名给与CATALOG_DAO_CLASS变量就可以。

由此可见,工厂方法确实为系统结构提供了非常灵活强大的动态扩展机制,只要我们更换一下具体的工厂方法,系统其他地方无需一点变换,就有可能将系统功能进行改头换面的变化。 

posted @ 2006-03-31 13:59 高山流水 阅读(190) | 评论 (0)编辑 收藏

 
设计模式之Prototype(原型)
板桥里人 http://www.jdon.com 2002/05/07
定义:
用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象.

Prototype模式允许一个对象再创建另外一个可定制的对象,根本无需知道任何如何创建的细节,工作原理是:通过将一个原型对象传给那个要发动创建的对象,这个要发动创建的对象通过请求原型对象拷贝它们自己来实施创建。

如何使用?
因为Java中的提供clone()方法来实现对象的克隆(具体了解clone()按这里),所以Prototype模式实现一下子变得很简单.

以勺子为例:

public abstract class AbstractSpoon implements Cloneable
{
  String spoonName;

  public void setSpoonName(String spoonName) {this.spoonName = spoonName;}
  public String getSpoonName() {return this.spoonName;}

  public Object clone()
  {
    Object object = null;
    try {
      object = super.clone();
    } catch (CloneNotSupportedException exception) {
      System.err.println("AbstractSpoon is not Cloneable");
    }
    return object;
  }
}

有两个具体实现(ConcretePrototype):

public class SoupSpoon extends AbstractSpoon
{
  public SoupSpoon()
  {
    setSpoonName("Soup Spoon");
  }
}

public class SaladSpoon extends AbstractSpoon
{
  public SaladSpoon()
  {
    setSpoonName("Salad Spoon");
  }
}

调用Prototype模式很简单:

AbstractSpoon spoon = new SoupSpoon();
AbstractSpoon spoon = new SaladSpoon();

当然也可以结合工厂模式来创建AbstractSpoon实例。

在Java中Prototype模式变成clone()方法的使用,由于Java的纯洁的面向对象特性,使得在Java中使用设计模式变得很自然,两者已经几乎是浑然一体了。这反映在很多模式上,如Interator遍历模式。

posted @ 2006-03-31 13:58 高山流水 阅读(147) | 评论 (0)编辑 收藏

     摘要: 戏说Singleton模式 DragonCheng(原作)来源:csdn ...  阅读全文

posted @ 2006-03-31 13:58 高山流水 阅读(343) | 评论 (2)编辑 收藏

 
设计模式之Singleton(单态)
板桥里人 http://www.jdon.com 2002/05/07
定义:
Singleton模式主要作用是保证在Java应用程序中,一个类Class只有一个实例存在。

在很多操作中,比如建立目录 数据库连接都需要这样的单线程操作。

还有, singleton能够被状态化; 这样,多个单态类在一起就可以作为一个状态仓库一样向外提供服务,比如,你要论坛中的帖子计数器,每次浏览一次需要计数,单态类能否保持住这个计数,并且能synchronize的安全自动加1,如果你要把这个数字永久保存到数据库,你可以在不修改单态接口的情况下方便的做到。

另外方面,Singleton也能够被无状态化。提供工具性质的功能,

Singleton模式就为我们提供了这样实现的可能。使用Singleton的好处还在于可以节省内存,因为它限制了实例的个数,有利于Java垃圾回收(garbage collection)。

我们常常看到工厂模式中类装入器(class loader)中也用Singleton模式实现的,因为被装入的类实际也属于资源。

如何使用?
一般Singleton模式通常有几种形式:

public class Singleton {

  private Singleton(){}

  //在自己内部定义自己一个实例,是不是很奇怪?
  //注意这是private 只供内部调用

  private static Singleton instance = new Singleton();

  //这里提供了一个供外部访问本class的静态方法,可以直接访问  
  public static Singleton getInstance() {
    return instance;   
   }
}

 

第二种形式:

public class Singleton {

  private static Singleton instance = null;

  public static synchronized Singleton getInstance() {

  //这个方法比上面有所改进,不用每次都进行生成对象,只是第一次     
  //使用时生成实例,提高了效率!
  if (instance==null)
    instance=new Singleton();
  return instance;   }

}

 

使用Singleton.getInstance()可以访问单态类。

上面第二中形式是lazy initialization,也就是说第一次调用时初始Singleton,以后就不用再生成了。

注意到lazy initialization形式中的synchronized,这个synchronized很重要,如果没有synchronized,那么使用getInstance()是有可能得到多个Singleton实例。关于lazy initialization的Singleton有很多涉及double-checked locking (DCL)的讨论,有兴趣者进一步研究。

一般认为第一种形式要更加安全些。

使用Singleton注意事项
有时在某些情况下,使用Singleton并不能达到Singleton的目的,如有多个Singleton对象同时被不同的类装入器装载;在EJB这样的分布式系统中使用也要注意这种情况,因为EJB是跨服务器,跨JVM的。

我们以SUN公司的宠物店源码(Pet Store 1.3.1)的ServiceLocator为例稍微分析一下:

在Pet Store中ServiceLocator有两种,一个是EJB目录下;一个是WEB目录下,我们检查这两个ServiceLocator会发现内容差不多,都是提供EJB的查询定位服务,可是为什么要分开呢?仔细研究对这两种ServiceLocator才发现区别:在WEB中的ServiceLocator的采取Singleton模式,ServiceLocator属于资源定位,理所当然应该使用Singleton模式。但是在EJB中,Singleton模式已经失去作用,所以ServiceLocator才分成两种,一种面向WEB服务的,一种是面向EJB服务的。

Singleton模式看起来简单,使用方法也很方便,但是真正用好,是非常不容易,需要对Java的类 线程 内存等概念有相当的了解。

 

进一步深入可参考:

Double-checked locking and the Singleton pattern

When is a singleton not a singleton?  

posted @ 2006-03-31 13:57 高山流水 阅读(118) | 评论 (0)编辑 收藏

Java语言的一个优点就是取消了指针的概念,但也导致了许多程序员在编程中常常忽略了对象与引用的区别,本文会试图澄清这一概念。并且由于Java不能通过简单的赋值来解决对象复制的问题,在开发过程中,也常常要要应用clone()方法来复制对象。本文会让你了解什么是影子clone与深度clone,认识它们的区别、优点及缺点。

看到这个标题,是不是有点困惑:Java语言明确说明取消了指针,因为指针往往是在带来方便的同时也是导致代码不安全的根源,同时也会使程序的变得非常复杂难以理解,滥用指针写成的代码不亚于使用早已臭名昭著的"GOTO"语句。Java放弃指针的概念绝对是极其明智的。但这只是在Java语言中没有明确的指针定义,实质上每一个new语句返回的都是一个指针的引用,只不过在大多时候Java中不用关心如何操作这个"指针",更不用象在操作C++的指针那样胆战心惊。唯一要多多关心的是在给函数传递对象的时候。如下例程:

														package reference;
class Obj{
    String str = "init value";
    public String toString(){
        return str;
    }
}
public class ObjRef{
    Obj aObj = new Obj();
    int aInt = 11;
    public void changeObj(Obj inObj){
        inObj.str = "changed value";
    }
    public void changePri(int inInt){
        inInt = 22;
    }
    public static void main(String[] args) 
    {
        ObjRef oRef = new ObjRef();
        
        System.out.println("Before call changeObj() method: " + oRef.aObj);
        oRef.changeObj(oRef.aObj);
        System.out.println("After call changeObj() method: " + oRef.aObj);

        System.out.println("==================Print Primtive=================");
        System.out.println("Before call changePri() method: " + oRef.aInt);
        oRef.changePri(oRef.aInt);
        System.out.println("After call changePri() method: " + oRef.aInt);

    }
}

/* RUN RESULT
Before call changeObj() method: init value
After call changeObj() method: changed value
==================Print Primtive=================
Before call changePri() method: 11
After call changePri() method: 11

*
*/

												


这段代码的主要部分调用了两个很相近的方法,changeObj()和changePri()。唯一不同的是它们一个把对象作为输入参数,另一个把Java中的基本类型int作为输入参数。并且在这两个函数体内部都对输入的参数进行了改动。看似一样的方法,程序输出的结果却不太一样。changeObj()方法真正的把输入的参数改变了,而changePri()方法对输入的参数没有任何的改变。

从这个例子知道Java对对象和基本的数据类型的处理是不一样的。和C语言一样,当把Java的基本数据类型(如int,char,double等)作为入口参数传给函数体的时候,传入的参数在函数体内部变成了局部变量,这个局部变量是输入参数的一个拷贝,所有的函数体内部的操作都是针对这个拷贝的操作,函数执行结束后,这个局部变量也就完成了它的使命,它影响不到作为输入参数的变量。这种方式的参数传递被称为"值传递"。而在Java中用对象的作为入口参数的传递则缺省为"引用传递",也就是说仅仅传递了对象的一个"引用",这个"引用"的概念同C语言中的指针引用是一样的。当函数体内部对输入变量改变时,实质上就是在对这个对象的直接操作。

除了在函数传值的时候是"引用传递",在任何用"="向对象变量赋值的时候都是"引用传递"。如:

														package reference;
class PassObj
{
    String str = "init value";
}
public class ObjPassValue 
{

    public static void main(String[] args) 
    {
        PassObj objA = new PassObj();
        PassObj objB = objA;

        objA.str = "changed in objA";
        System.out.println("Print objB.str value: " + objB.str);
    }
}
/* RUN RESULT
Print objB.str value: changed in objA
*/

												


第一句是在内存中生成一个新的PassObj对象,然后把这个PassObj的引用赋给变量objA,第二句是把PassObj对象的引用又赋给了变量objB。此时objA和objB是两个完全一致的变量,以后任何对objA的改变都等同于对objB的改变。

即使明白了Java语言中的"指针"概念也许还会不经意间犯下面的错误。

Hashtable真的能存储对象吗?

看一看下面的很简单的代码,先是声明了一个Hashtable和StringBuffer对象,然后分四次把StriingBuffer对象放入到Hashtable表中,在每次放入之前都对这个StringBuffer对象append()了一些新的字符串:

														package reference;
import java.util.*;
public class HashtableAdd{
    public static void main(String[] args){
        Hashtable ht = new Hashtable();
        StringBuffer sb = new StringBuffer();
        sb.append("abc,");
        ht.put("1",sb);     
        sb.append("def,");
        ht.put("2",sb);
        sb.append("mno,");
        ht.put("3",sb);
        sb.append("xyz.");
        ht.put("4",sb);
        
        int numObj=0;
        Enumeration it = ht.elements();
        while(it.hasMoreElements()){
            System.out.print("get StringBufffer "+(++numObj)+" from Hashtable: ");
            System.out.println(it.nextElement());
        }
    }
}

												


如果你认为输出的结果是:
get StringBufffer 1 from Hashtable: abc,
get StringBufffer 2 from Hashtable: abc,def,
get StringBufffer 3 from Hashtable: abc,def,mno,
get StringBufffer 4 from Hashtable: abc,def,mno,xyz.

那么你就要回过头再仔细看一看上一个问题了,把对象时作为入口参数传给函数,实质上是传递了对象的引用,向Hashtable传递StringBuffer对象也是只传递了这个StringBuffer对象的引用!每一次向Hashtable表中put一次StringBuffer,并没有生成新的StringBuffer对象,只是在Hashtable表中又放入了一个指向同一StringBuffer对象的引用而已。

对Hashtable表存储的任何一个StringBuffer对象(更确切的说应该是对象的引用)的改动,实际上都是对同一个"StringBuffer"的改动。所以Hashtable并不能真正存储能对象,而只能存储对象的引用。也应该知道这条原则对与Hashtable相似的Vector, List, Map, Set等都是一样的。

上面的例程的实际输出的结果是:

														/* RUN RESULT
get StringBufffer 1 from Hashtable: abc,def,mno,xyz.
get StringBufffer 2 from Hashtable: abc,def,mno,xyz.
get StringBufffer 3 from Hashtable: abc,def,mno,xyz.
get StringBufffer 4 from Hashtable: abc,def,mno,xyz.
*/

												


类,对象与引用

Java最基本的概念就是类,类包括函数和变量。如果想要应用类,就要把类生成对象,这个过程被称作"类的实例化"。有几种方法把类实例化成对象,最常用的就是用"new"操作符。类实例化成对象后,就意味着要在内存中占据一块空间存放实例。想要对这块空间操作就要应用到对象的引用。引用在Java语言中的体现就是变量,而变量的类型就是这个引用的对象。虽然在语法上可以在生成一个对象后直接调用该对象的函数或变量,如:

														new String("Hello NDP")).substring(0,3)  //RETURN RESULT: Hel

												


但由于没有相应的引用,对这个对象的使用也只能局限这条语句中了。

  1. 产生:引用总是在把对象作参数"传递"的过程中自动发生,不需要人为的产生,也不能人为的控制引用的产生。这个传递包括把对象作为函数的入口参数的情况,也包括用"="进行对象赋值的时候。
  2. 范围:只有局部的引用,没有局部的对象。引用在Java语言的体现就是变量,而变量在Java语言中是有范围的,可以是局部的,也可以是全局的。
  3. 生存期:程序只能控制引用的生存周期。对象的生存期是由Java控制。用"new Object()"语句生成一个新的对象,是在计算机的内存中声明一块区域存储对象,只有Java的垃圾收集器才能决定在适当的时候回收对象占用的内存。
  4. 没有办法阻止对引用的改动。

什么是"clone"?

在实际编程过程中,我们常常要遇到这种情况:有一个对象A,在某一时刻A中已经包含了一些有效值,此时可能会需要一个和A完全相同新对象B,并且此后对B任何改动都不会影响到A中的值,也就是说,A与B是两个独立的对象,但B的初始值是由A对象确定的。在Java语言中,用简单的赋值语句是不能满足这种需求的。要满足这种需求虽然有很多途径,但实现clone()方法是其中最简单,也是最高效的手段。

Java的所有类都默认继承java.lang.Object类,在java.lang.Object类中有一个方法clone()。JDK API的说明文档解释这个方法将返回Object对象的一个拷贝。要说明的有两点:一是拷贝对象返回的是一个新对象,而不是一个引用。二是拷贝对象与用new操作符返回的新对象的区别就是这个拷贝已经包含了一些原来对象的信息,而不是对象的初始信息。

怎样应用clone()方法?

一个很典型的调用clone()代码如下:

														class CloneClass implements Cloneable{
    public int aInt;
    public Object clone(){
        CloneClass o = null;
        try{
            o = (CloneClass)super.clone();
        }catch(CloneNotSupportedException e){
            e.printStackTrace();
        }
        return o;
    }
}

												


有三个值得注意的地方,一是希望能实现clone功能的CloneClass类实现了Cloneable接口,这个接口属于java.lang包,java.lang包已经被缺省的导入类中,所以不需要写成java.lang.Cloneable。另一个值得请注意的是重载了clone()方法。最后在clone()方法中调用了super.clone(),这也意味着无论clone类的继承结构是什么样的,super.clone()直接或间接调用了java.lang.Object类的clone()方法。下面再详细的解释一下这几点。

应该说第三点是最重要的,仔细观察一下Object类的clone()一个native方法,native方法的效率一般来说都是远高于java中的非native方法。这也解释了为什么要用Object中clone()方法而不是先new一个类,然后把原始对象中的信息赋到新对象中,虽然这也实现了clone功能。对于第二点,也要观察Object类中的clone()还是一个protected属性的方法。这也意味着如果要应用clone()方法,必须继承Object类,在Java中所有的类是缺省继承Object类的,也就不用关心这点了。然后重载clone()方法。还有一点要考虑的是为了让其它类能调用这个clone类的clone()方法,重载之后要把clone()方法的属性设置为public。

那么clone类为什么还要实现Cloneable接口呢?稍微注意一下,Cloneable接口是不包含任何方法的!其实这个接口仅仅是一个标志,而且这个标志也仅仅是针对Object类中clone()方法的,如果clone类没有实现Cloneable接口,并调用了Object的clone()方法(也就是调用了super.Clone()方法),那么Object的clone()方法就会抛出CloneNotSupportedException异常。

以上是clone的最基本的步骤,想要完成一个成功的clone,还要了解什么是"影子clone"和"深度clone"。

什么是影子clone?

下面的例子包含三个类UnCloneA,CloneB,CloneMain。CloneB类包含了一个UnCloneA的实例和一个int类型变量,并且重载clone()方法。CloneMain类初始化UnCloneA类的一个实例b1,然后调用clone()方法生成了一个b1的拷贝b2。最后考察一下b1和b2的输出:

														package clone;
class UnCloneA {
    private int i;
    public UnCloneA(int ii) { i = ii; }
    public void doubleValue() { i *= 2; }
    public String toString() {
        return Integer.toString(i);
    }
}
class CloneB implements Cloneable{
    public int aInt;
    public UnCloneA unCA = new UnCloneA(111);
    public Object clone(){
        CloneB o = null;
        try{
            o = (CloneB)super.clone();
        }catch(CloneNotSupportedException e){
            e.printStackTrace();
        }
        return o;
    }
}
public class CloneMain {
    public static void main(String[] a){
        CloneB b1 = new CloneB();
        b1.aInt = 11;
        System.out.println("before clone,b1.aInt = "+ b1.aInt);
        System.out.println("before clone,b1.unCA = "+ b1.unCA);
                
        CloneB b2 = (CloneB)b1.clone();
        b2.aInt = 22;
        b2.unCA.doubleValue();
        System.out.println("=================================");
        System.out.println("after clone,b1.aInt = "+ b1.aInt);
        System.out.println("after clone,b1.unCA = "+ b1.unCA);
        System.out.println("=================================");
        System.out.println("after clone,b2.aInt = "+ b2.aInt);
        System.out.println("after clone,b2.unCA = "+ b2.unCA);
    }
}


/** RUN RESULT:
before clone,b1.aInt = 11
before clone,b1.unCA = 111
=================================
after clone,b1.aInt = 11
after clone,b1.unCA = 222
=================================
after clone,b2.aInt = 22
after clone,b2.unCA = 222
*/

												


输出的结果说明int类型的变量aInt和UnCloneA的实例对象unCA的clone结果不一致,int类型是真正的被clone了,因为改变了b2中的aInt变量,对b1的aInt没有产生影响,也就是说,b2.aInt与b1.aInt已经占据了不同的内存空间,b2.aInt是b1.aInt的一个真正拷贝。相反,对b2.unCA的改变同时改变了b1.unCA,很明显,b2.unCA和b1.unCA是仅仅指向同一个对象的不同引用!从中可以看出,调用Object类中clone()方法产生的效果是:先在内存中开辟一块和原始对象一样的空间,然后原样拷贝原始对象中的内容。对基本数据类型,这样的操作是没有问题的,但对非基本类型变量,我们知道它们保存的仅仅是对象的引用,这也导致clone后的非基本类型变量和原始对象中相应的变量指向的是同一个对象。

大多时候,这种clone的结果往往不是我们所希望的结果,这种clone也被称为"影子clone"。要想让b2.unCA指向与b2.unCA不同的对象,而且b2.unCA中还要包含b1.unCA中的信息作为初始信息,就要实现深度clone。

怎么进行深度clone?

把上面的例子改成深度clone很简单,需要两个改变:一是让UnCloneA类也实现和CloneB类一样的clone功能(实现Cloneable接口,重载clone()方法)。二是在CloneB的clone()方法中加入一句o.unCA = (UnCloneA)unCA.clone();

程序如下:

														package clone.ext;
class UnCloneA implements Cloneable{
    private int i;
    public UnCloneA(int ii) { i = ii; }
    public void doubleValue() { i *= 2; }
    public String toString() {
        return Integer.toString(i);
    }
    public Object clone(){
        UnCloneA o = null;
        try{
            o = (UnCloneA)super.clone();
        }catch(CloneNotSupportedException e){
            e.printStackTrace();
        }
        return o;
    }
}
class CloneB implements Cloneable{
    public int aInt;
    public UnCloneA unCA = new UnCloneA(111);
    public Object clone(){
        CloneB o = null;
        try{
            o = (CloneB)super.clone();
        }catch(CloneNotSupportedException e){
            e.printStackTrace();
        }
        o.unCA = (UnCloneA)unCA.clone();
        return o;
    }
}
public class CloneMain {
    public static void main(String[] a){
        CloneB b1 = new CloneB();
        b1.aInt = 11;
        System.out.println("before clone,b1.aInt = "+ b1.aInt);
        System.out.println("before clone,b1.unCA = "+ b1.unCA);
                
        CloneB b2 = (CloneB)b1.clone();
        b2.aInt = 22;
        b2.unCA.doubleValue();
        System.out.println("=================================");
        System.out.println("after clone,b1.aInt = "+ b1.aInt);
        System.out.println("after clone,b1.unCA = "+ b1.unCA);
        System.out.println("=================================");
        System.out.println("after clone,b2.aInt = "+ b2.aInt);
        System.out.println("after clone,b2.unCA = "+ b2.unCA);
    }
}

/** RUN RESULT:
before clone,b1.aInt = 11
before clone,b1.unCA = 111
=================================
after clone,b1.aInt = 11
after clone,b1.unCA = 111
=================================
after clone,b2.aInt = 22
after clone,b2.unCA = 222
*/

												


可以看出,现在b2.unCA的改变对b1.unCA没有产生影响。此时b1.unCA与b2.unCA指向了两个不同的UnCloneA实例,而且在CloneB b2 = (CloneB)b1.clone();调用的那一刻b1和b2拥有相同的值,在这里,b1.i = b2.i = 11。

要知道不是所有的类都能实现深度clone的。例如,如果把上面的CloneB类中的UnCloneA类型变量改成StringBuffer类型,看一下JDK API中关于StringBuffer的说明,StringBuffer没有重载clone()方法,更为严重的是StringBuffer还是一个final类,这也是说我们也不能用继承的办法间接实现StringBuffer的clone。如果一个类中包含有StringBuffer类型对象或和StringBuffer相似类的对象,我们有两种选择:要么只能实现影子clone,要么就在类的clone()方法中加一句(假设是SringBuffer对象,而且变量名仍是unCA): o.unCA = new StringBuffer(unCA.toString()); //原来的是:o.unCA = (UnCloneA)unCA.clone();

还要知道的是除了基本数据类型能自动实现深度clone以外,String对象是一个例外,它clone后的表现好象也实现了深度clone,虽然这只是一个假象,但却大大方便了我们的编程。

Clone中String和StringBuffer的区别

应该说明的是,这里不是着重说明String和StringBuffer的区别,但从这个例子里也能看出String类的一些与众不同的地方。

下面的例子中包括两个类,CloneC类包含一个String类型变量和一个StringBuffer类型变量,并且实现了clone()方法。在StrClone类中声明了CloneC类型变量c1,然后调用c1的clone()方法生成c1的拷贝c2,在对c2中的String和StringBuffer类型变量用相应的方法改动之后打印结果:

														package clone;
class CloneC implements Cloneable{
    public String str;
    public StringBuffer strBuff;
    public Object clone(){
        CloneC o = null;
        try{
            o = (CloneC)super.clone();
        }catch(CloneNotSupportedException e){
            e.printStackTrace();
        }
        return o;
    }
    
}
public class StrClone {
    public static void main(String[] a){
        CloneC c1 = new CloneC();
        c1.str = new String("initializeStr");
        c1.strBuff = new StringBuffer("initializeStrBuff");
        System.out.println("before clone,c1.str = "+ c1.str);
        System.out.println("before clone,c1.strBuff = "+ c1.strBuff);
                
        CloneC c2 = (CloneC)c1.clone();
        c2.str = c2.str.substring(0,5);
        c2.strBuff = c2.strBuff.append(" change strBuff clone");
        System.out.println("=================================");
        System.out.println("after clone,c1.str = "+ c1.str);
        System.out.println("after clone,c1.strBuff = "+ c1.strBuff);
        System.out.println("=================================");
        System.out.println("after clone,c2.str = "+ c2.str);
        System.out.println("after clone,c2.strBuff = "+ c2.strBuff);
    }
}
/* RUN RESULT
before clone,c1.str = initializeStr
before clone,c1.strBuff = initializeStrBuff
=================================
after clone,c1.str = initializeStr
after clone,c1.strBuff = initializeStrBuff change strBuff clone
=================================
after clone,c2.str = initi
after clone,c2.strBuff = initializeStrBuff change strBuff clone
*
*/

												


打印的结果可以看出,String类型的变量好象已经实现了深度clone,因为对c2.str的改动并没有影响到c1.str!难道Java把Sring类看成了基本数据类型?其实不然,这里有一个小小的把戏,秘密就在于c2.str = c2.str.substring(0,5)这一语句!实质上,在clone的时候c1.str与c2.str仍然是引用,而且都指向了同一个String对象。但在执行c2.str = c2.str.substring(0,5)的时候,它作用相当于生成了一个新的String类型,然后又赋回给c2.str。这是因为String被Sun公司的工程师写成了一个不可更改的类(immutable class),在所有String类中的函数都不能更改自身的值。下面给出很简单的一个例子:

package clone; public class StrTest { public static void main(String[] args) { String str1 = "This is a test for immutable"; String str2 = str1.substring(0,8); System.out.println("print str1 : " + str1); System.out.println("print str2 : " + str2); } } /* RUN RESULT print str1 : This is a test for immutable print str2 : This is */

例子中,虽然str1调用了substring()方法,但str1的值并没有改变。类似的,String类中的其它方法也是如此。当然如果我们把最上面的例子中的这两条语句

														c2.str = c2.str.substring(0,5);
c2.strBuff = c2.strBuff.append(" change strBuff clone");
       
												


改成下面这样:

														c2.str.substring(0,5);
c2.strBuff.append(" change strBuff clone");

												


去掉了重新赋值的过程,c2.str也就不能有变化了,我们的把戏也就露馅了。但在编程过程中只调用

														c2.str.substring(0,5);

												


语句是没有任何意义的。

应该知道的是在Java中所有的基本数据类型都有一个相对应的类,象Integer类对应int类型,Double类对应double类型等等,这些类也与String类相同,都是不可以改变的类。也就是说,这些的类中的所有方法都是不能改变其自身的值的。这也让我们在编clone类的时候有了一个更多的选择。同时我们也可以把自己的类编成不可更改的类。

posted @ 2006-03-31 13:57 高山流水 阅读(122) | 评论 (0)编辑 收藏

 
Role分析模式(一) 角色对象基本概念
来自BBS水木清华站∶精华区

 

概要

在任何应用系统中,每一个对象都可能扮演多种角色,你在家里是父亲,在公司则可能是一个程序员,一个为你提供原材料的公司可能同时又是你的客户。。,这样的问题一次又一次的出现,我经常看到应用系统不能很好处理这些问题,在本文中,我将仔细描述这一重要的分析模式,并使用TAOERP 基本业务元素(TBCC)展示如何使用这一分析模式处理组织机构相关的问题。

By 石一楹

本文从《ERP之道》www.erptao.org转载

======================================================================================

一个应用系统经常需要某一个对象在不同的上下文中具有不同行为的情形,最常见的例子是客户和供应商的问题。 例子:

某制鞋企业有很多为它们提供真皮的合作公司,在处理采购订单时,这些合作公司是它的供应商,但这些合作商同时从该制鞋企业采购皮鞋,所以在处理销售订单时,这些公司又变成了它的客户。

许多建模人员在处理这类问题的时候,经常轻率地做出判断,当用户需求不能满足时,他们才发现这样的判断是不正确的。

正如Martin Fowler所言,作为一个分析模式,我在这里主要关心在何种情况下需要使用什么样的模型,而并不是太关心具体的实现如何。本文中出现的Java代码虽然来自Tao BC库,但是出于示例的目的,都进行了简化。读者也应该把主要精力放在模型本身,或者说是接口上而不是实现。

我要提醒的是,本文虽然论述了比较复杂的Role建模方法,但是读者在应用这些模式之前需要仔细考虑它们是否必要应用到你的模型之中。因为,任何一种复杂的模型虽然能够解决复杂的问题,但是不可避免地会带来额外的开销,所以,如果能够用简单的模型处理你手头上的问题,不要用更复杂的。面向对象和框架开发方法最大的好处之一就是能够在你需要的时候进行修改而对系统的影响最小。按照Kent Beck的Extreme Programming 理论:“只做你现在需要的”。当然,一旦问题发生变化,你需要用其它面向对象的方法进行Iterator,这是另外一个主题,我也许会在其它的专栏中介绍。

上下文和动机

假设我们现在要开发一个企业销售管理系统,在这样的系统中,关键的抽象之一就是合作伙伴客户,因此我们的设计模型中将包括客户Customer类。该类的接口提供对客户名字、地址、电话、Email,客户信用度等属性的操作。

现在,假设我们也需要一个采购管理系统,这时候我们需要一个供应商的抽象,尽管供应商在很多方面和Customer一样,但是譬如供应商的提前期等操作显然和客户有所不同。这是我们抽取一个不同的Supplier类的原因。

但是,当我们的销售系统和采购管理系统一起集成到供应链管理的时候,我们会发现独立抽象Customer和Supplier这样的类会碰到很多问题。一个供应商可能同时是你的客户,又或者你的一个供应商后来变成了你的客户。你也许考虑供应商和客户从一个抽象的Person类继承。

但是,这还是有问题。首先,从对象标识的角度来讲,虽然我们的Customer和Supplier继承自同一个Partner,但是他们的对象属于不同子类的实例,所以他们不可能相同。因此,代表同一个合作伙伴的Customer和Supplier的两个对象具有不同的对象标识。它们之间的相同只能通过其它的机制模拟实现。如果两个对象指代同一个实际对象,他们从Partner上继承的属性应该相同。但是,我们会在多态搜索时发生问题,如果我们搜索系统中所有的Partner,那么相同的Partner(包括、供应商、客户)可能会重复出现,我们必须小心处理这些重复问题。

角色模式把对象在不同上下文(销售系统、采购系统)相关的视图建模成Role Object,这些角色对象动态地连结到核心对象(Core Object)。这样形成的对象聚集表达一个逻辑对象,它能够处理包含多个物理对象的问题。

象Partner这样的关键抽象建模为一个接口,也就是没有任何实现。Partner接口指定了处理类似于伙伴地址、帐户这样的通用信息。Partner另一部分重要的接口是维护角色所需要的接口,象上面的adRole()和getRole().PartnerCore类负责实现这些接口。

Partner的具体角色类由PartnerRole提供,PartnerRole也支持 Partner接口。PartnerRole是一个抽象类,它不能也不打算被实例化。PartnerRole的具体子类,如Customer和Supplier,定义并实现特定角色的具体接口。只有这些具体角色类在运行时被实例化。Customer类实现了该伙伴在销售系统上下文中的那一部分视图,同样,Supplier实现了在采购系统中的那一部分视图。

像供应链管理系统这样的一个客户(使用这些类的代码),可能会用Partner接口来存取PartnerCore类的实例,或者会存取一个特定的PartnerRole的具体类,如Customer.假设一个供应链系统通过Partner接口获得一个特定的Partner实例,供应链系统可能会检查该Partner是否同时扮演Suppiler的脚色。它会使用一个特定的Specification作为参数做一个hasRole()调用。Specification是用于指定需要满足的标准的一个接口,我们会在后面考察这个问题。现在处于简单目的,我们假设这个Specification就是一个字符串,该字符串指定了需要查找的类的名字。如果该Partner对象可以扮演一个名为”Supplier”的角色,销售管理系统可以通过getRole(“Supplier”)得到具体的供应商类的对象。然后供应链管理系统就可以通过此对象调用供应商特定的操作。

什么时候使用该模式

如果你想在不同的上下文中处理一个关键抽象,而每一个上下文可能对应自己的一个应用程序,而你又想把得到的上下文相关的接口放入同一个类接口中。

你可能想能够动态地对一个核心抽象增加或删除一个角色,在运行时而不是在编译时刻决定。

你想让角色/客户应用程序相互独立,改变其中的一个角色可以不影响其它角色对应的客户应用程序。

你想能够让这些独立地角色互相转换,并能辨别不同的角色实际上属于同一个逻辑对象时。

但是,该模型并不适用于所有情况,如果在这些角色之间具有很强的约束和交叉关系。这种模型不一定适用于你。尽管我们后面可以看到,某些约束和关系用Role来处理可能更直观和简洁。


结构和原理

下图显示了Role Object的基本结构:

我们可以看到客户应用程序大多数情况下使用Component接口来进行一些通用的操作。如果该应用程序并不关心具体的子类。在上面的供应链管理系统中,如果我们只关心Partner的一些基本信息,譬如地址、电话等等,客户应用程序只需要存取Partner接口(Component)。Component接口还提供了增加、修改、查询和删除对应Specification具体类的管理接口。客户应用程序在需要特定角色的操作时,可以使用getRole(aSpec)得到。

对那些通用的操作而言,真正操作是由ComponentCore来实现的,不论客户使用Component接口还是具体的角色类。在客户应用具体类的时候,由于每一个具体类都继承了ComponentRole, ComponentRole将这些操作传递给ComponentCore,最后由Core来完成操作。我们同样也可以看到,那些角色管理的接口也是有ComponentCore来实现的,它有一个所有具体对象的列表,可以在其中进行查询和其他操作。

使用RoleObject的优缺点

从概念上来讲,Role Object为系统提供了一个清晰的抽象。属于同一个逻辑对象的各个不同上下文中的角色都服从于这个抽象的接口。Role Object也提供了一个一致的接口对这些角色进行管理。这种一致的管理使得对这个关键抽象具有很强的可扩展性,你可以动态增加、删除角色而不影响客户应用程序。也就是使得客户应用程序和对应Role的绑定很少。

如果我们从面向对象的语言所能提供的设施来看,Role角色实际上提供了一种多重继承的特性。在我们上面的例子中,如果一种语言提供多重继承,我们很可能会让该Partner同时从Customer和Suppiler继承下来。但是,现在绝大多数面向对象语言都不支持这样的继承。即使支持的话,也没有办法动态增加和删除它的继承类。

但是,Role Object模式的应用不可避免地带来一些麻烦。首先如果客户代码需要使用一个具体类的接口,它必须首先询问这个Component是否能够扮演那样的角色。语言本身也不能进行强制的类型检查。

问题在Role之间具有相互约束的时候变得更加复杂。我们在这里申明,如果你所构建的具体类之间具有很复杂的相互约束关系,那么Role Object可能不是你的选择。当然,对Role Object进行适当的扩展和调整可以很好地处理某些约束关系。我们会在后面详述。

实现

基本实现

实现要从Role Object的基本意图来着手,使用Role进行建模最主要的两个目的是透明地扩展关键抽象、动态管理角色。

在面向对象社团中,已经有很好的实践和理论来处理这样的问题。Decorator是满足透明扩展的范例,而Product Trader模式可以让客户进行对象创建,这些被创建的对象匹配一个特定的产品角色协议并且满足附加的Specification.,使用这两个广为模式社团所承认的模式使得Role Object模式更加坚固。

我们所要解决的第一个问题就是Component、ComponentCore和ComponentRole之间的关系,从上面的原理图可以看到,Component抽象了所有角色都具有的通用接口。以后,我们可以使用角色管理协议增加、查询、删除角色。这些角色都从ComponentRole继承得到,它负责把具体角色的操作传递给Core来完成,也就是说ComponentRole实现了对ComponentCore的装修,同时,所有的具体类也对Core进行了装修,这一点和Decorator十分相似,但是至少有两点与Decorator是不同的:

1. 意图:Decorator按照GOF的说法是动态地给对象增加一些额外的职责,也就是增加功能。我们可以看下面的一个Decorator的实例:

在这里,核心的元素是TextView,ScrollDecorator和BorderDecorator的目的是为了透明地为TextView增加滚动条和边框,而客户应用程序仍然可以使用Component接口,使用抽象的Decorator使得装修的增加不会产生组合爆炸问题。

Role模式的意图是展现不同上下文中的不同角色,而其中的ComponentRole和ComponentCore之间具有等同的接口,也就是ComponentRole虽然包装了ComponentCore,但是它的目的不是增加功能而是作为一个中间传递者。把具体Role的所有通用接口(同时也是关键抽象Component的接口)转移到Core的具体实现上。

2. 核心实例和包装实例之间的关系 装修模式参与者之间的装修者都相互链在一起,一个装修者指向另一个装修者,最后指向核心,而所有的Role都直接指向核心,这是因为装修生成的最后类的实例贯穿了所有的包装物,而角色之间基本上都是相互独立的,除非它们之间具有约束,这是后话。

我们关心的第二个问题就是角色的管理,角色的管理需要我们考虑下面几个重要的问题:

1. 角色对象创建:角色实现了对核心对象的运行时包装,所以最重要的问题是如何创建角色实例,以及这些实例如何与核心对象相连。注意客户代码不知道如何以及何时创建这些角色对象,创建过程由核心来控制。

2. 角色对象删除:角色对象的删除是一个标准的垃圾收集问题。某一个客户代码应当无法删除一个角色对象,因为它不知道是否还有其他客户在使用这个角色对象。

3. 角色对象的管理:为了让一个核心对象能够管理它的角色,Component接口声明了一个角色管理协议,其中包括对角色对象的增加、删除、测试和查询。为了支持该接口协议,核心对象维护了一个Map,其中由角色的Specification映像到具体地角色实例。

下面是一个简单的例子:

public interface Partner {

public String getAddress();

public void addRole(String roleName);

public boolean hasRole(String roleName);

public void removeRole(String roleName);

public PartnerRole getRole(String roleName);

}

class PartnerCore implements Partner {

String address;

private Map roleMap = new HashMap();

public String getAddress() {

return address;

}

public void addRole(String roleName) {

PartnerRole aRole = CustomerRole.createFor(roleName,this);

if (aRole!= null)

roleMap.add (aRole);

}

public boolean hasRole(String roleName) {

return roleMap.containsKey(roleName);

}

public void removeRole(String roleName) {

if hasRole(roleName) {

roleMap.remove(roleName);

}

}

public PartnerRole getRole(String roleName) {

if hasRole(roleName) {

return (PartnerRole) roleMap.get(roleName);

} else {

return null;

}

}

}

在这里,我们使用roleName作为Specification,角色的Specification和实际的Role实例之间的映象用一个Map来实现。

现在我们定义一个PartnerRole抽象类:

abstract class PartnerRole implements Partner {

private PartnerCore core;

public String getAddress() {

return core.getAddress();

}

public void addRole(String roleName) {

core.addRole(roleName);

}

public static void createFor(String roleName, PartnerCore core) {

Creator creator = lookup(roleName);

if (creator == null) {

return null;

}

PartnerRole aRole = creator.create();

if (aRole != null)

aRole.core = core;

return aRole;

}

}

这里有几点需要注意,PartnerRole是一个抽象的类,它不能被实例化,对所有角色都通用的操作以及角色管理协议操作均被重新定向到core。在这里PartnerRole最有意义的操作是它的静态方法createFor,注意createFor的格式,它的第二个参数是一个ComponentCore而不是更通用的Component接口,因此我们就限制了一个ComponetRole不会担当ComponentCore的责任。

接下去需要实现具体的Role对象,譬如说我们的Customer,Supplier:

public class Customer extends PartnerRole {

public Money getCredit() {

…….

}

}

public class Supplier extends PartnerRole {

public void schedule() {

…..

}

}

下面是用户可能的使用方法:

Partner aPartner = Database.load(“North Bell”);

Customer aCustomer = (Customer)aPartner.getRole(“Customer”);

if (aCustomer != null) {

Money credit = aCustomer.getCredit();

….

}

这里在获得特定的角色之后需要进行强制的类型转化。

本文从介绍Role Object的动机开始,逐渐深入到它的原理、组成、优缺点和基本实现,并给出了一个基本的实现。Role Object的应用和实现在许多方面都需要进行扩展,包括如何使用Specification模式管理Role,应用Product Trader模式创建Role,以及如何处理Role之间的相互约束问题。同时,Role Object实现Role概念的一种方法,我将在后续的文章中继续本文的内容,提供更为深入和广泛的讨论和研究。欢迎建议。

关于作者
石一楹是一个专注于面向对象领域的系统设计师。目前的工作是国内一家ERP公司的CTO.他住在浙江杭州,有一个两个月的女儿。

posted @ 2006-03-31 13:56 高山流水 阅读(261) | 评论 (0)编辑 收藏

 
Role分析模式(二)角色对象创建和管理
来自BBS水木清华站∶精华区

 

概要
Role Object(一)提出对解决Role问题的基本方法,但是我们还有许多问题需要深入,这些问题包括如何创建具体的Role实例,如何管理这些角色并实现角色之间的约束。
By 石一楹

本文从《ERP之道》www.erptao.org转载

======================================================================================

Role Object提出了对解决Role问题的基本方法,但是我们还有许多问题需要深入,其中的一个问题是如何创建具体的Role实例。

动机

对Role的创建有几个基本的需求。前面我们使用角色的名字作为Specification来创建Role.譬如我们用字符串”Customer”作为Specification来创建Customer这个具体的Role对象。在大多数情况下,这样做可能就足够了,但是有时候确不行。

假设我们有一个核心的抽象叫做Person。我们知道Person可以是员工,所以有一个叫”Employee”的角色。但是可能有不同类型的员工,如销售人员、开发人员等等。所以一般Employee会建模为一个接口,而由Employee的子类来实现具体的角色。当一个客户应用需要知道一个人的工资时,它从核心去获取一个”Employee”的角色,但是现在把它作为类型的名字显然不适合,因为实际的角色可能具有的名字是”Salesman”,”Developer”等等。

这个关于Specification的问题同样出现在对角色的管理过程,如果单独使用一个类名字不能建立或者获取到一个具体的Role,那么整个管理协议都适应这个Specification进行,而不是单独把类名字当作Key进行管理。

创建过程

对于创建过程来说,我们所要解决的核心问题是需要一个类,当我们向这个类提出某些需求或标准的时候,这个类就会返回满足我们需求的具体对象实例,这种满足可能具有层次关系、或者是其它的关系。

所以,有两个问题,一个是如何表达这个Specification,另一个问题是由谁来做这个中间类,我们是需要重新定义一个,还是使用原来就有的ComponentRole.我们怎样能够这个中间类的使用可以让用户对这个创建过程不可见。

我们对设计模式的认识显然排除了那种使用一个大case的可能性,这样的代码在我们需要动态加入一个角色的时候无能为力。工厂方法看起来适合我们的基本需求,但是你很快就发现我们上面的例子中对任何一个“Employee”,它只能返回一种类型的角色。

幸好,解决方案是存在的,我们有一个模式叫做Product Trader,下面是它的原理图:

在上面的图中,客户负责为每一个具体的产品类建立一个Specification,然后为ProductTrader提供这一

在上面的图中,客户负责为每一个具体的产品类建立一个Specification,然后为ProductTrader提供这一个Specification,Product是一个抽象的接口,它定义了需要实例化的子类的操作。Product Trader这个类则实现了从Specification到一个具体的Creator的映射。并且提供各种操作来管理这个映射关系。这里的映射关系可以是一个非常简单的HashMap或者是一个具有完备功能的中间对象trader.现在如何创建的任务落到了Creator上,Creator知道如何实例化一个具体的产品。Specification是一个表达满足关系的抽象,我们可以看到最简单的例子是一个字符串,可能是一个层次关系或者最复杂的情况下可能是一个需要经过很多计算的公式。它的目的是作为一个关键字搜索到具体的Creator.下面是具体的序列图:

针对我们的Role Object,可以看到ProductTrader的责任由ComponentRole来承担,而每一个具体的Role则是在上图中的ConcreteProduct.而客户实际上是Role Object中的ComponentCore.

为什么使用ProductTrader能够使得我们的角色对象可以被更好地创建呢?

首先,ProductTrader如同其它的创建性设计模式(Factory Method等),可以使得客户独立于具体的产品类。也就是使得ComponentCore独立于具体的Role.应用ProductTrader使得ComponentCore完全独立于具体Role的类层次。ComponentCore只需使用某一特定的Specification调用ComponentRole的createFor方法,由ComponentRole来做具体的创建。

其次,具体的角色类可以在运行时决定。ProductTrader的应用可以让ComponentRole在运行时把某些范围或者条件转化为一个Specification,然后使用这样一个Specification进行具体角色类的搜索。因此,你可以具有可变的运行时才决定的角色创建过程。这个优点带来的最直接的后果就是,你可以进行方便的配置,你可以任意增加、删除特定的角色,只要你通过ComponentRole的方法增加、替换及删除相应的Creator即可。这些Creator可以按照Specification搜索特定的角色类。

我们引入ProductTrader的一个最重要的原因是它可以让你轻松地加入具体角色的类层次。由于ComponentCore成功地和具体角色的类层次、类名字、层次结构和实现策略分离,所以对上述内容的改变不会影响到ComponentCore创建具体角色的接口和方法。

能够很好地配置以及改变具体角色类层次使得加入和删除一个具体的角色类变得什么简单,我们不需要修改任何存在的代码,只要对配置文件或脚本修改即可。


实现的考虑

在角色对象中应用ProductTrader需要考虑四个主要的问题,他们包括: l 如何实现从Specification到Creator的映射

我们需要在一个ProductTrader中维护一个Specification、Creator和它们之间的一个映射关系。

l 如何实现Creator

有很多设计模式可以用来处理这样的麻烦,我们耳熟能详的就有prototype、类对象等等。总而言之,无论用何种方法,你都不会希望每次增加一个新的角色,都需要手工去建立一个Creator。所以,对于Creator的要求就是这个类里面由一种机制可以直接创建一个角色实例.


class Creator {

public Creator(Specification aSpec) {

aSpecification =aSpec;

}

public Specification getSpecification() {

return aSpecification;

}

public ComponentRole create() ;

private Specification aSpec;

}


class ConcreteCreator extends Creator {

private class concreteRoleType;

public ConcreteRole(class concreteRoleType,Specification spec) {

super(spec);

this.concreteRoleType = concreteRoleType;

}

public ComponentRole create() {

return new concreteRoleType.newInstance();

}

}

这个Creator相当简单,注意其实在第一个抽象类Creator中,Specification的实例应当放在ComponentRole抽象类里面用于映射到某个Creator,但是我们处于管理方面的目的,在这里加入了这个成员,你可以在后面看到。

其次,要注意的是,如果使用象C++一样的template,我们可以做到类型检查,但是在这里所有的Specification是抽象的Specification,而具体的Creator生成的是ComponentRole而不是ConcreteRole.

再仔细看一下这个Creator,你会发现create过程没有参数,某些时候,你可能需要其它的创建参数,那么也许你要通过反射得到concreteRoleType这个class中符合你参数的构建方法,然后用你的参数进行创建,而不是直接使用newInstance().当然,前提是在create方法中假如你的参数。

l 如何实现Specification

Specification可以使用某些原语类型来实现,如String代表Class name.当然,复杂的情况需要使用一个自有的对象。如果使用一个专门的接口而并非简单的字符串,Specification的接口看起来如下:


public interface Specification {

public void newFormClient(Object anObject);

public void newFromManager();

public void adaptTo();

public boolean matches(Specification spec);

}

这些接口需要由具体的Specification来实现,其中getRoleClass得到为该Specification对应的Role Class. Matches用于判断两个specification之间的匹配性。NewFromClient则由客户使用。它可以创建一个Specification然后交给ProductTrader. 而adaptTo方法用于把某一个具体的specification和该roleClass相对应,它由ComponentRole使用,ComponentRole通过该方法直接从roleClass中获取到建立Specification所需要的信息。

Specification中newFromManager可以直接缺省实现为adaptTo即可,matches可以实现为相等。我喜欢使用的方法之一先定义接口,然后实现一个abstract类:

public abstract class AbstractSpecification implements {

public void newFormClient(Object anObject);

public void newFromManager() {

adaptTo();

}

public void adaptTo();

public boolean matches(Specification spec) {

getClass().equals(spec.getClass());

}

}

这时候,我们可以实现ComponentRoleSpecification的如下:

class ComponentRoleSpecification extends AbstractSpecification {

private String roleType;

//假设这里的表示方法是一个字符串roleType,你可以使用其它的如roleClass等等

public void newFormClient(Object anObject) {

roleType = (String)anObject;

}

public void newFromManager() {

adaptTo();

}

//注意这里需要ComponentRole有一个静态的getRoleType方法

public void adaptTo() {

roleType = ComponentRole.getRoleType();

}

public boolean matches(Specification spec) {

if (!(super.match(spec))

return false;

if (!(spec instanceOf ComponentRoleSpecification))

return false;

return roleType.equals( (ComponentRoleSpecification)spec.getRoleType());

}

public String getRoleType() {

return roleType;

}

}

最后的问题是ComponentRole如何建立映射,从而把一个Specification映射到一个Creator,这些方法包括addCreator(Creator),removeCreator(Creator),substitue(Creator),我们可以看到前面的Creator中包含了getSpecification这个方法,这是我们可以用如下代码:

addCreator(Creator creator) {

mapping.put(creator.getSpecification(), creator);

}

除了这些维护方法外,还需要有lookup,这个lookup仅仅就是通过一个Specification找到creator的过程,相当简单。

ProductTrader模式的应用可能走得更远,它甚至可以用于整个应用系统的所有对象的创建,对ProductTrader的深入讨论可能会超出Role角色所需要的内容,如果读者需要进一步研究该模式,请参见PLOP3.

Role角色管理

在ComponentRole中,还需要管理那些角色,最重要的操作包括:

hasRole(Spec)

getRole(Spec)

removeRole(Spec)

addRole(Spec)

我们看到,前面的match用于两个Specification之间的精确匹配,理由是我们用它来精确匹配对应的Creator。同时,我们看到,这固然解决了ComponentRole具有多层次的问题,但是却不能提供上面的这些管理接口所需要的所有操作。

回忆一下我们在动机一节提出的问题:

“假设我们有一个核心的抽象叫做Person。我们知道Person可以是员工,所以有一个叫”Employee”的角色。但是可能有不同类型的员工,如销售人员、开发人员等等。所以一般Employee会建模为一个接口,而由Employee的子类来实现具体的角色。当一个客户应用需要知道一个人的工资时,它从核心去获取一个”Employee”的角色,但是现在把它作为类型的名字显然不适合,因为实际的角色可能具有的名字是”Salesman”,”Developer”等等。“

这里的问题是我们并不明确指定一个完全的对等关系,我们需要类层次中一样的概念,如果一个Salesman继承自Employee,那么所有Salesman的对象都是Employee,所以我们需要在Specification接口中加入两个新的操作,分别是:

isSpecialCaseOf(Specification)和isGeneralizationOf(Specification).

这时两个Specification之间可以比较。Specification B是Specification A的一个特殊情况当且仅当对于任何对象X,如果X满足A,那么X就满足B.从我们的角色对象来看,如果它所可能具有的任何一个角色的Specification是用户查询的Specification的一个特殊情况,那么该角色对象就具有用户指定Specification所代表的角色。IsGeneralizationOf则正好相反。

在我们的角色对象中,每次用户通过一个addRole(Spec)加入一个角色对象,ComponentCore在自己维护的一个Map中包存了spec实例到ComponentRole的一个映射,当用户使用hasRole(spec)或getRole(sepc)进行查询时,我们可以遍历该Map,如果发现有某一个spec满足用户的参数,则可以返回。对getRole来讲,可能有两种情况,一种情况是找到一个即返回,另外一种情况则是返回所有满足Specification的角色。


Role和Role之间的约束

我们在一开始就讲到,Role Object很难处理具体Role之间具有约束的问题。这是由Role Object本身的特点所决定的。具体Role 之间没有相互的关联,他们不象Decroator一样一个指向一个,最后指向ComponentCore。

如果Role具体对象之间的关系非常复杂,我们可能需要一个知识层(knowledge level)来处理Role之间的关系。我们在我以后讲到Party[待写]的时候看到,TAO BBC如何使用知识层和Role组合完成一个具有Role的Accountability模式。

其实,我们的Specification也可以看作是一个知识层,对于某些约束,Role可以通过Specification来解决。Specification可以解决具体Role具有一个类层次的问题。那么我们可以递归使用Role Object模式来处理这样的约束。

在实际应用中碰到最多的约束问题可能是,某一个对象的一个角色A可能是该对象能够充当其它角色的一个前提,在银行业务中,如果一个人希望充当贷款人、投资者,那么它首先必须是一个客户。

你可以看到,在上图中我们重复应用了Role Object模式,从而限制了一个Investor角色首先就必须是一个Customer角色,从而形成如下的对象链。

Role的属性列表

Role可能需要动态增加属性,并且处理这些属性之间相互约束,请参见专栏中的文章动态属性(属性列表)

Role的存储

我们在这里没有关心Role的实际存储,我们将Role设计成具体的存储无关,具体的存储方法可能需要另一个包装。譬如使用O/R mapping,或者EJB,我们需要实现对应的RoleEntity层次,然后把RoleEntity实现的行为分派到具体的Role模型。这是我们将在TAO J2EE模式研究中看到的课题。TAO BBC将使用这样的模式来实现具体的存储。


本文使用Product Trader模式和Specification模式讨论了Role的一些具体实现和管理问题。在下一篇文章中,我将描述实现Role的另一种方法。新的方法将采用类Decorator模式来处理Role,这种方式和Role Object各有自己的优缺点。

posted @ 2006-03-31 13:56 高山流水 阅读(298) | 评论 (1)编辑 收藏

仅列出标题
共30页: First 5 6 7 8 9 10 11 12 13 Last