涉及方面:面向对象思想在实际编程之中的运用
摘要:观察者模式
1·观察者模式其实是对象和对象之间的一种交流方式
2·时刻要记得对象在不断的变化
呼~终于弄完了。在开始介绍"观察者模式"之前我先说点题外话。
说实话我最初实在看Hibernate源码,发现自己对观察者模式理解的不够,于是想回来补补课的。其实写Chapter1的时候,几乎都是照搬《HeadFirst》的设计模式,只是把鸭子转成游戏的角色了。呵呵
我也没有想到会有人关注这篇Blog,其实写的目的真的就是写给自己读读好加深印象。从文章的内容也可以看出来,只是想到哪里就写哪里,没有照顾读者的感受。(因为我预期的是只有自己看嘛 呵呵)
现在发现问题严重了很多,当我想给原来的例子加上"观察者模式的时候"我突然发觉已经无法再把他当成例子来对待了。
我发觉设计模式不再是设计模式了
就好比观察者模式 我现在已经完全把它当成是一个人的表达自己说的话。
我原来本是在编游戏,后来不知不觉转成了j2ee(就像这次看Hibernate源码事件一样)。在做网站的时候也用设计模式,甚至包括架构模式。什么MVC,什么SessionFactory。那个时候随便问一个人问我,我能很清楚的告诉他什么是MVC,什么是工厂模式。不过我发觉现在我对设计模式的理解有了很大的改变。我不知道我这种改变是对oo认识的加深,还是我自己的胡乱想象。
所以我恳请:
如果您对我的文章有兴趣。读完后帮着顶一下,或者踩一脚。不要顾及对我的面子,觉得踩人家不好。
如果您觉得我说得很有问题,希望在踩完后也能给予我一些意见。我在这里先谢过您了。
Ok,在我带您认识我的"孩子"之前,还是需要简单介绍一下 什么是书上说的观察者模式。
/** *观察者 */ public Obervice { public void update(){ System.out.println("收到消息"); } } /** *主题 */ public Subject { private List obervice = new ArrayList(0); //储存所有的观察者 //注册观察者 public void registerObserver(Obervice o){ obervice.add(o); } //移除观察者 public void removeObserver(Obervice o){ obervice.remove(o); } //通知观察者 public void notifyObserver(){ Iterator it = obervice.iterator; while(it.hasNext()){ it.next().update(); } } } public static void main(String[] arg){ Obervice o = new Obervice(); Subject s = new Subject; s.registerObserver(o); s.notifyObserver(); }
结果:
收到消息
程序是我在写字板里面手写的,所以可能需要改改小错误才能run起来。不过如果您对这个模式还不是很了解。建议您应该先看一下HeadFirst关于这章的解释然后再接着往下看。
好的,既然您读到这里了,我就假设您已经了解了观察者模式了,那下面我就要说说我对观察者模式的理解了。
经过这2天多的不断使用,我现在已经不把它当作"观察者模式"了。
浅显的说可以把其当作两个人,一个人在说,一个人在听。
还记得HeadFirst里面那个小闹剧么?
我来和您一起回忆一下:
里面有一个猎头(我给起的 Petter),两个软件人员(Ron,Jill)。
于是这场短剧就开始了:
Ron说: 猎头,我想找份Java工作。
Jill说: 猎头,我想找份C++工作。
Petter把他们的名字添加在了自己的小本本上面。
Petter拿起电话,按照本本上的名字从头到尾打了一遍。告诉他们同一件事情:
我这里有份C++编程的工作。
好,短剧先说到这里,让我们来分析一下:
对于Petter,Ron,Jill他们都是什么?Object没有问题吧。
Ron和Jill这两个Object只要关心自己的事情就好了。
Ron在家打着Dota,突然Petter给它打了一个电话:我这里有份C++编程的工作。Ron想了想,Petter有病吧,我又没要C++工作。于是挂断电话继续玩儿Dota去了
Jill呢,当听到Petter给自己打电话时候很高兴。于是去应聘C++工作去了。
Object之所以这样,因为这样耦合性最小。Petter(Object1)只管按照本本上的电话记录打电话即可,其他的不用关心。Ron(Object2)和Jill(Object3)也是这样做自己的事情,听到了Petter的电话。想想和自己有关无关,无关接着做自己的事情就好了。
那很容易的就可以吧Ron和Jill理解为有了耳朵,耳朵给予了他们听的功能。
而Petter有了嘴巴,嘴巴给予了他说得功能。
这样每个人就独立了起来,他们各自干各自的活儿。
*****插入图片1
很完美是吧?不过现实总是残酷的,让我们继续把故事将下去
*****插入图片2
啊哦,人心叵测啊。原来Ron不光有耳朵,而且有嘴巴啊。
让我们再回头看图片1:
除了图片有点傻,你还发现了什么?对!他们都是残疾人。
(注意要用唱两只老虎的语调来念后面的话^_^)一个只有嘴巴,一个只有耳朵,真奇怪,真奇怪。
是吧,我们的程序中不可能只存在3个对象或则只存在这些残疾人的。
如果你把对象当成人的话,而更大的对象或者说整个程序就是这些人组合在一起完成的一件工作,对不?人和人之间是需要交流的,Class和Class之间当然也不例外了啊。我把两个人做成连体婴儿了,那交流是最方便的了。不过这样的耦合度必然也是最高的了,往往你把连体婴儿分开他们总是不能活的好好的。那Ron,Jill,Petter这种关系好不好呢?两个哑巴和一个聋子。当整个程序中只有这三个对象的时候,且两个哑巴只关心同意见事情的时候显然没有问题。聋子告诉他们了消息,一个说谢谢,一个说你神经病。即使聋子没有听见也没有问题,问题依旧可以解决了。不过当对象多起来的时候,这种光听不说,或者这种光说不听的同志显然要给大家拉后腿的。那我们需要什么样的人呢?健全人嘛!这个大家自然能想到了。
当对象都是健全人的时候,完美了是吧?现实又一次摧残了我们。。。
为啥?因为健全人就需要操心更多事情。
Petter原来是聋子,所以不用关心Ron说他是神经病,Jill对他说谢谢。
现在他要想一想 "神经病?","谢谢" 恩看来这个不是要向我订阅职位广告的人。
想到解决方法了?好吧,你很聪明。
当人与人之间可以交流的时候,有时候会出现这种场景。一对情侣和朋友一起走在大街上。女孩对男孩说"我爱你",男孩对女孩说"我也爱你"。于是两个人热情的开始接吻。这时候他的朋友走了过来也对男孩说"我爱你"。男孩怎么办?男孩的观察对象不仅有女孩,而且有他的朋友(因为他们一起出去玩儿的。注意:男孩并不关心大街上那两条狗,及他不关系和他不认识的人)。而当两个人都说了"我爱你"的时候,男孩怎么作出反映呢?
显然上面的程序是不对的吧(请往上看看之前的Obervice和Subject类)
public static void main(String[] arg){ Obervice boy = new Obervice(); //男孩有耳朵 //他要对女孩即朋友说得话作出响应 Subject girlFriend = new Subject; Subject normalFriend = new Subject; boy.registerObserver(girlFriend); boy.registerObserver(normalFriend); girlFriend.notifyObserver(); normalFriend.notifyObserver(); System.out.println("出麻烦喽"); }
显然您已经想到了解决方法,暂时没有?不用着急Java已经替我们完成了这些工作。(详情请看HeadFirst里面相关介绍)
好吧,现在回头看看我们都解决了什么问题:
1·我们解决了处理不必要的消息的问题(神经病,谢谢 问题)
2·我们解决了处理相同语句的问题(我爱你 问题)
完美了是吧? 哦 No! do not Say That..
在此我就省略一下关于 现实世界 的那句话。但是确实有一些比上面这个问题更棘手的问题需要我们解决。不过这些问题就不能离开代码来空谈了。我们要正式的继续设计我们这个游戏Baby了(不了解的建议先看一下Chapter1)
首先我希望现在咱俩已经达成了一种共识。
听、说、这两个方法应该是两种能力。所以我只能说具有这种能力,而不能像Monstar extends Creautre 说 我也extends了听或者说(注意,我想成为正常人,所以我要有这两种能力。而不是其中的一种 因为extends只能有其中的一种能力)
好吧我们肯定要把Subject和Obervice变成接口。是不?(注:我开始认为Java中是以类方法实现的,不过我刚才试了一下Java中现在也可以以接口方法实现)
/** * 观察着模式之中的观察方,实现更新接口 * @author Tunied Nmetal * j2me.zor.org * */ public interface Obervice { /** * Object sender为告知接受方此消息是谁发送的 * 所有观察者都需要判断自己是否需要接收该发送方的消息 * int updateType为告知接受方发送的是何种更新消息 * 所有观察者都需要 用 final class UpdateType之中的数据和updateTyep之中的数据比较(代码重构前暂时不需要) * 以判断自己应该做什么时候能够作出正确的响应 * Object value为发送过去的得值 * 当需要使用数据的时候,观察者对value进行适当的转型来接收相应的数据 * @param sender * @param valueType * @param value */ public void update(Object sender,int valueType,Object value); } /** * 观察着模式中的被观察方,实现添加、移除观察者,以及通知观察者接口 * @author Tunied Nmetal * j2me.zor.org * */ public interface Subject { public void registerObserver(Obervice o); //注册观察者 public void removeObserver(Obervice o); //移除观察者 public void notifyObserver(); //通知观察者 }
现在让我们开始一场崔斯特与大怪物之间的殊死搏斗吧。战斗一般是怎么进行的呢?
首先让角色上场好了
Role 崔斯特 = new Role();
Monstar 地精 = new Monstar();
战斗开始了!
崔斯特.Attack(地精);
地精一看,情况不妙。急忙又找来了2个兄弟
Monstar 哥布林 = new Monstar();
Monstar 小蜘蛛 = new Monstar();
于是崔斯特对所有人发起了攻击
崔斯特.Attack(地精);
崔斯特.Attack(哥布林);
崔斯特.Attack(小蜘蛛);
哥布林一看,不行啊。打不过,我也要叫人!
....
Monstar 哥布林2 = new Monstar();
崔斯特一想,这么多人,干脆别一个个打了。我放个火球术好了。
什么?崔斯特还会火球术?我那本书上不是这么写的。拜托! 现在是我在写书好不好。。那好吧。。。
Role 会火球术的崔斯特 = new Role();
会火球术的崔斯特.克隆值(崔斯特);
....崔斯特会火球术确实不好,那还是让崔斯特叫出关法海来协助作战吧^_^ Oh~No!
*(*&*(&&……*战斗被迫停止了。
所以,我们需要重新认识一下角色这个问题了。角色什么老在变?
攻击方式在变。
我可以用弓箭进攻,我可以用双刀。我更可以施放魔法! 好的,还有呢?
防御方式在变。
我可以穿布甲,我可以穿锁子甲。我甚至可以开坦克。。。(好吧,崔斯特一下成重装机兵。)不过你说得对,这些都会变。
我想您一定还能想出好多,其实我也想出出好多来了。不过那个时候代码已经改了三遍了(就是因为这个才耗了这么长时间),我可能需要再进一步认识一下角色,对代码进行再次重构了。
不过在此之前 让咱们假设常会变的只有这两样好么?
好的,如果咱们达成了共识 我想继续。
那我下一个问题是:在这些会变的单元里面什么不会变?
这块可能需要好好想想了,这里我先发表自己的看法。多种多样的攻击方式以及防御方式之中。
即使我在显示器上面的图标变了又变,伤害值加了又加,属性伤害添了又填。不变是什么?
不变的是那种行为,我用剑也好,弓也好。我砍你也好劈你也好,我对你做了些什么?
我攻击你了^_^
你穿上皮夹克,你开上大坦克。你又干嘛了?
你防御了我这次攻击了。
所以说,攻击这种行为,防御这种行为是不会变的。
Ok,回顾一下,看看我们又达成了什么共识。
1·我们必要将攻击,防御提出来单独设计,因为其总在变化(想想火球术)
2·之所以称木剑可用来攻击,坦克可用来防御。是因为其具有攻击,防御这两种属性。
好的,既然这样,我们是不是可以这么认为呢:
+++++插入图片3
啊哦,除了比较屎的图片你还发现了啥?
恩,你发现了图上一下自有了6个对象,并且他们之间是有关联的。 (and more~~)
你在给我介绍观察者模式,所以他们之间一定是用观察者模式进行交流!
好吧,让我们重新回到刚才的战斗。
首先角色上场
Role 崔斯特 = new Role();
Monstar 地精 = new Monstar();
崔斯特这个时候说:开始前,咱们先达成个协议
//我攻击你的具有防御属性的东西,而不直接攻击你
崔斯特.getAttack().set观察者(地精.getDefence())
地精一想很不错嘛,公平起见。我也攻击你的防御性东西
地精.getAttack().set观察者(崔斯特.getDefence())
好了达成协议了!战斗开始吧。
崔斯特.getAttack().Attack(); //崔斯特大喊我攻击完啦
地精的具有防御属性的东西(比如烂衣服)听到了这个消息。恩~有人攻击我了,我来看看。伤害有100点,我自己能抗住10点
于是烂衣服高喊:臭地精!你受到90点伤害。地精听到了想了想,我有100格血,受到90点伤害。还有10点血。一点都不好玩儿!我要叫同伴
Monstar 哥布林 = new Monstar();
此时崔斯特和也和这个哥布林攀谈起来。咱俩也打成个协议好不?协议?好吧。
崔斯特.getAttack().set观察者(哥布林.getDefence());
哥布林.getAttack().set观察者(崔斯特.getDefence());
达成协议后,崔斯特心想。这么多怪,我要打多变天啊。干脆来个火球术好了
崔斯特.setAttack(new 火球术Attack());
崔斯特.getAttack().Attack();
让我们先忘掉哥布林,回到地精这里。地精的烂衣服有一次受到了攻击的消息。恩~又有人攻击我了,我来看看。伤害有5000点,我自己能抗住10点
于是烂衣服高喊:臭地精!你受到4990点伤害。地精听到了想了想,我有10格血,受到4990点伤害。还有...总之我死了!于是地精死前高喊:我死啦!
烂衣服听到了这个消息赶忙说:我的主人死啦,别攻击我啦。崔斯特的 火球术Attack() 听到了这个消息 对自己的主人说:主人您刚才让我攻击的对象死了,这是他的尸体
于是崔斯特解到尸体取走了100点经验值(注:由于得到的是对象,就是之前所说的Objcet value,所以也就是得到了 (哥布林)value,当以后扩充以后不仅可以得到经验值,更可以得到哥布林相关的一切一切东西)。崔斯特取走了100点经验值后,发觉自己升级了。于是大喊:我升级啦!火球术Attack()听到主人升级了,于是增加了自己的攻击力...
战斗就这样完美的结束了。。。。
战斗结束了,我所阐述的关于"观察者模式"的理解也蕴含在这里面了。
我想把剩下的时间留给您自己,发挥您的想象 我想您现在应该可以做以下一些事情的一些或所有
1·确定是顶我一下还是踩我一下 (怕...怕...)
2·看一下我给出的源码,自己Debug运行一遍。我源码给出了注释,因为代码改动比较多,所以有些注释可能并不是其所表达的那样。忘海涵。
3·加入我的圈子,进一步讨论游戏设计相关的知识 http://gameart.group.iteye.com/(请先阅读置顶文章)
4·给我提出改进建议,和我一起共同完成咱们的"GameBaby"
5·最重要的,如果您认同我所讲的"观察者模式"结合您自己的领域,体会其用法,并分享给大家。我愿意做其中的一个观察者。只要您这个主题让我订阅的话 呵呵
好吧,到此。一切都完美了么? NO~~又来了.....
过场话我就不说了(写了3个小时了,没精力了呵呵) 我给大家说一个我犯的错误,以及这个错误的解决方法。 看代码先:
这个是Defence类: /** * 当宿主死亡且宿主是怪物类 * 把宿主传送出去 * 并调用清理函数 */ if (valueType == Language.Creature_Monstar_Dead ) { for(int i = 0; i < Obervice.size(); i++){ Obervice obervice = (Obervice)Obervice.get(i); obervice.update(sender,Language.Behavior_Defense_MonstarDead,sender); //此处需特别注意!如果BeAttack类得知宿主死亡,且宿主是Monstar类 //则将怪物类(此处为传进来的Sender)传送出去,并且告诉监听者它已经死亡了 } removeObserver((Obervice)sender); //把宿主从自己的观察者队列里面移除,因为其死亡了 /** * 判断自己的观察者里面是否还有自己的宿主 * 如果没有了自己的宿主则清理自己 */ Iterator it = Obervice.iterator(); boolean isNoCreature = true; while(it.hasNext()){ /** * 如果还存在生物宿主则false且返回 */ if(GameTools.isExtends(it.next(), Creature.class)){ //这是我自己写的小工具,判断it.next()是否是一个 Creature.class //(此工具可以判断abstract类) 如果您对该工具有任何更改建议请及时通知我,我将万分感激 isNoCreature = false; break; } } if(isNoCreature){ CleanMyself(); //执行清理函数,好让GC清理自己 } } }
如果您忍的住的话,请只看代码考虑5分钟,如果您能够想明白问题出在哪里。我觉得您对观察者思想的造诣已经超越我太多太多了。我自己是想了好久,Debug好久才弄明白怎么回事的。
我的想法是这样的:
1·Defence类收到了自己宿主死亡的消息,首先把这个消息告诉自己所有观察者。我的宿主死亡啦!
2·接着Defence判断自己是否还有其他的宿主(这里现在没有必要,不过那个时候我考虑可能会和共体技能有关(两个人共享伤害,War3白牛的技能))
3·发现自己没有宿主了,调用清理函数,清理自己。
没有问题?呵呵 把问题抽象一些 看图:
++++++插入图片4+++++++++++
B是A的观察者,C是B的观察者,D是C的观察者,同时D也是B的观察者。
A发送消息告诉B
B发送消息告诉C
C发送消息告诉D
D接收到消息,此时清理的了自己
C已经发送完所有的观察者,返回
B发送消息给D
系统发送消息给你 NullPointExcption -。-
为啥?因为电脑的函数调用是栈式调用。你过早的清理了自己。所以上面必然在调用你时候必然会抛出异常。
那时候一定要清理自己呢?留给Gc干这些事情好不好?请注意!游戏中的死亡不是现实中Java对象的死亡。我的血量<0了,所以我认为自己死亡了。不代表Gc也认为我死亡了。
所以你总应该做一些事情好让Gc去回收你。具体怎么做?先自己想想,没准你比我有更好的解决办法呢。如果想知道我是怎么解决的。看代码中死神类(Azrael.class)都干了些什么
好了,完....饿....没力气闹了。
接着看代码:
还是Defence类: /** * 当Defense收到由Attack发送过来的:你被攻击了 *将发送方传过来的value(Attack对象自己)所造成的伤害削减后 * 传给自己的我给观察者。告诉他们:我被攻击了 */ if (valueType == Language.Behavior_Attack_Attack) { Attack attacker = (Attack) value; int type = attacker.getType(); // 取得攻击对象的攻击类型 /** * 判断自己受到伤害的类型 按物理系以及魔法系给出不同的受伤公式 */ switch (type) { case AttackType.ATK: attacker.setDemage(attacker.getDemage() - DEF); break; case AttackType.MATK: attacker.setDemage(attacker.getDemage() - MDEF); } for (int i = 0; i < Obervice.size(); i++) { Obervice obervice = (Obervice) Obervice.get(i); obervice.update(this, Language.Behavior_Defense_BeAttack, attacker); } }
这块做的事情已经写得很清楚了,受到攻击消减完再把消息传递出去。可是当你Junit代码的时候,你会发现崔斯特没攻击一次怪物,自己的攻击力就下降了点
百思不得其解,最后又是跟踪了半天代码才明白了这点。并且这个我觉得是很重要得放。
抛开游戏的不谈,在您使用观察者模式的时候可能也会出现这种情况:
观察方对主题方的数据进行了变动,而您并不是想要这样做的。那该怎么办?
还是看代码吧 呵呵。我的思路来源自UDP数据包。我把观察者模式理解成计算机之间的不同节点。
好了就这么多了。用最后的力气才说几句废话。
1·如果您觉得我写得没用,请务必踩我一脚啊。我好下次不用写这么多东西了 感觉都快成写小说的了-。-
2·如果你对游戏开发有兴趣,记得来我的圈子啊。先阅读置顶帖哦 http://gameart.group.iteye.com/group/admin
3·感谢一下所有在Chapter1里面回复的人。真的很感谢,你们的支持是我最大的东西
4·特别感谢一下40020072,没想到你对我的代码这么上心。 谢谢,谢谢。
感谢所有看完这篇Blog的人,不用看,您就是其中一位。谢谢,谢谢了。
关于代码的任何修改,批评,指责,谩骂,问题 等等等等 任何有关的事情,请一定联系我。
谢谢