CSDN首页 空间 新闻 论坛 Blog 下载 读书 网摘 搜索 .NET Java 视频 接项目 求职 在线学习 买书 程序员 通知
不看会后悔的Windows XP之经验谈 简单快捷DIY实用家庭影院
CSDN社区
搜索 收藏 打印 关闭
CSDN社区 >  C/C++ >  模式及实现

Observer设计模式的陷阱,兼谈C++语言在模式面前的悲哀

楼主skyMountain(天山)2006-09-29 11:30:43 在 C/C++ / 模式及实现 提问

前几天,刚写的一个软件崩溃了。跟踪发现是下面函数的问题:  
  void   CSubject::OnMsg(CSMSG   *pMsg)  
  {  
  for(list<IMsgListener*>::iterator   it   =   m_lstMsgListener.begin();  
  it   !=   m_lstMsgListener.end();   it   ++)  
  {  
  ASSERT(   NULL   !=   (*it)   );  
  (*it)   ->Notify(pMsg);  
  }  
  }  
   
  有经验的人一看就知道这是使用的是GOF的“观察者模式”。m_lstListener是观察者集合,用户可以通过Attach或者Detach函数在   m_lstListener中增加或删除观察者。当观察者期待的某个时间产生时,通过上面这一段代码,每个观察者都可以收到相应的消息(通过Notify   接口)。  
  可是,相信熟悉STL的人看到这一段代码,第一时间都会想:这会不会导致迭代子失效?  
  这段代码里,Notify接口是个虚函数。也就是说,Notify函数的具体实现,是由使用者自行决定的。上面这段代码,本身并不会引起迭代子失效,但是如果用户在Notify函数里不慎修改了m_lstListener,那么这段代码就会崩溃。  
  也就是说,观察者如果在Notify函数中调用Attach或者Detach函数,迭代子就会失效,系统就会崩溃!  
  接触设计模式已经有两三年了,观察者模式是我最常用的模式之一。没想到这个简单的设计,竟然会有这么严重的问题。  
  奇怪的是,为什么那么多有关设计模式的书籍,对这个那么容易出错的问题只字不提呢?网上那么多设计模式的实现例子,都采用这个有严重错误的设计呢?甚至在GOF那本名著中,采用的也是这种类似的设计(只不过是用java语言而已)。谬种流传,害人不浅。  
   
  可能这也是C++语言的悲哀吧。jdk库中,已经有现成的observer模式支持,用户根本不用写这些代码。C++程序员却只能一行行自己编写,谁叫C++没有一个类似与JDK的基础库呢?  
  很多设计模式,C++语言实现起来都非常困难。就连最简单的工厂方法,C++也难以实现。用一个函数专门负责对象的生成,那么这个对象由谁负责释放,就成了问题。工厂模式直接违反了C++语言的基本编码规范:谁申请的内存,谁负责释放。JAVA中有垃圾回收机制,生成的对象不用关心如何释放,所以工厂模式使用起来得心应手。但C++程序员呢?高级一点的还可能会使用智能指针来解决这个问题,但那些连boost库都没听过过的程序员呢?他们就连工厂模式都用不好了。  
   
  那么这个问题如何解决呢?并不好解决。在单线程环境下,我建议用“观察者队列缓存”的方式:  
  一、增加一个bool型的迭代标志,标志观察者队列是否处于迭代之中。即在OnMsg函数进入的时候,将其设为true,出去的时候设为false。  
  二、增加一个“观察者增删缓存”队列。  
  三、修改attach函数和detach函数。用户请求增删迭代子的时候,如果系统正在迭代过程中,那么先将增删请求保存在观察者增删缓存队列中,等待迭代完成之后再将这些请求付诸实现。如果不处于迭代过程中,直接操作  
  观察者队列可以了。  
   
  本文转自我的blog:  
  http://blog.csdn.net/skyMountain/archive/2006/09/19/1245058.aspx 问题点数:0、回复次数:71Top

1 楼a_b_c_abc2(WeCallARoseByAnyOtherWordWouldSmellAsSweet)回复于 2006-09-29 12:06:39 得分 0

高手,拜。Top

2 楼fflush(stdin)回复于 2006-09-29 12:08:37 得分 0

lz的问题在于迭代过程中迭代子发生了不可预知的改变从而导致迭代失败。老实说,我也写过和lz很类似的代码,只是我的observer没有在notify的时候detach自身,所以也没有注意到存在这样的问题。刚看了lz的描述,有下面一点的想法:  
   
  迭代本身的设计也是一种模式(iterator模式),这里面就涉及到集合内部结构改变时迭代子是否失效的问题。Gof在iterator模式中对这些进行了讨论。一些迭代器的实现保证了集合结构改变时动态的调整迭代子,从而做到迭代的完全透明,不过大部分的设计(比如stl),采用了“鸵鸟”策略,即在迭代过程中对集合的修改完全无视,这导致了在这种情况下迭代的失败。但是就设计上而言,大部分迭代过程都不会修改集合,stl的设计不会有太大问题,并且这也保证了实现的简单和效率。因此,Lz的问题并不是observer模式本身的问题,而是iterator语义的问题。lz在后来提供了一个解决方案,可以解决问题,当然,我相信接触过模式的人(包括lz自己)都会觉得这样的解决方法很不优雅。我觉得比较好的方法是,如上所述,我们需要的是一个对修改sensative的迭代器,然后使用这个迭代器来迭代,基本不用改变observer模式相关的代码,毕竟,这与obersver无关  
   
  ps   lz关于c++在模式实现方面的悲哀我完全赞同,好像好的设计框架天生就需要gc的支持Top

3 楼Splendour()回复于 2006-09-29 15:28:14 得分 0

fflush(stdin)   说得对。这个弊病是iterator模式的,不应该是Observer的。lz已经复合了两种模式。lz说的二、增加一个“观察者增删缓存”队列。是比较常用的方法。Top

4 楼Polarislee(北极星)(无房无车,飘在北京)回复于 2006-09-29 15:41:12 得分 0

其实,换一个角度来说,不是“设计模式天生需要GC支持”而是“在没有GC的静态运行环境下需要不同的设计模式”。  
   
  Top

5 楼l_clove(倚天把剑观沧海·天下)回复于 2006-09-30 20:37:24 得分 0

STL又不是线程安全的,楼主不用互斥,不能说模式不好吧?Top

6 楼taodm((不能收CSDN社区短信息,请莫浪费精力))回复于 2006-10-01 12:00:29 得分 0

呃,模式,被烂用的名词,它起的误导甚至已经超过了它的正面作用。  
  Polarislee(北极星)(北京那么大,何处是我家)   的回答是正解。Top

7 楼SammyLan((基础决定你能走多远)--英语菜才是真的菜)回复于 2006-10-01 13:25:05 得分 0

深入语言去编程,不要浮于表面......  
  看你写类打C开头,相信也用过MFC吧,MFC的文档/视图不也照样工作的很好  
  Observers只是用来表现Subject的数据,原则上不允许Observers修改Subject的Observers链(Subject的其他数据原则上也不能由Subject直接修改)  
  这是使用Observer模式的契约,不遵循这个契约,你用Java也一样写不出一个好的模式来  
  所以不要埋怨语言,先从自身找原因  
  Top

8 楼SammyLan((基础决定你能走多远)--英语菜才是真的菜)回复于 2006-10-01 13:26:30 得分 0

勘误:  
  Subject的其他数据原则上也不能由Observers直接修改Top

9 楼justrun2005(机枪)回复于 2006-10-01 14:50:15 得分 0

赞同楼上众位饼子,不是设计模式不好,只是被滥用了而以。鄙人刚开始学设计模式,向各位先闻道者致敬。Top

10 楼skyMountain(天山)回复于 2006-10-04 12:17:03 得分 0

“原则上不允许Observers修改Subject的Observers链”  
  但有的时候,的确有这个需要。doc-view方式工作得好,可能是它内部处理了这种迭代子失效的问题。  
   
  请大家注意:  
  不是说设计模式不好,只是说通常的实现方法很容易引起系统崩溃。  
  不是说c++不能实现这些模式,只是说C++要实现这些模式,所要做的工作比较多、难度比较大,初学者不容易掌握。  
   
  另外,前面有人提到说,在没有GC的环境下,我们需要另外的设计模式。这个想法很好。以前我甚至曾给阎宏发过邮件,说在非面向对象的环境下(纯C),也可以总结出一些设计模式出来。可惜的是,目前还没见过相关的研究成果。  
   
  最后:多线程环境下的设计模式,也会跟GOF总结的很不相同,实现也不容易(例如lazy式单例模式,用java甚至不可能有安全的实现)。我最近做了些总结,将会陆续发表在我的blog上,欢迎大家捧场。Top

11 楼longge520(longge520)回复于 2006-10-05 20:28:06 得分 0

markTop

12 楼merlinran(天行者)回复于 2006-10-05 20:44:18 得分 0

[如果用户在Notify函数里不慎修改了m_lstListener,那么这段代码就会崩溃。]  
  Notify函数是不应该访问得到m_lstListener的。  
  维护这个列表是CSubject的责任,而不是由列表的成员。既然成员需要这么做,也得委托CSubject来做。  
  我以前写程序时遇到过这种情况,也是用“观察者增删缓存队列”来实现的。但这是CSubject自己的事。Top

13 楼Polarislee(北极星)(无房无车,飘在北京)回复于 2006-10-09 11:20:04 得分 0

In   software   development,   a   pattern   (or   design   pattern)   is   a   written   document   that   describes   a   general   solution   to   a   design   problem   that   recurs   repeatedly   in   many   projects.   Software   designers   adapt   the   pattern   solution   to   their   specific   project.   Patterns   use   a   formal   approach   to   describing   a   design   problem,   its   proposed   solution,   and   any   other   factors   that   might   affect   the   problem   or   the   solution.   A   successful   pattern   should   have   established   itself   as   leading   to   a   good   solution   in   three   previous   projects   or   situations.    
   
  ......  
   
  Design   patterns   include   the   following   types   of   information:  
        Name   that   describes   the   pattern    
        Problem   to   be   solved   by   the   pattern    
        Context,   or   settings,   in   which   the   problem   occurs    
        Forces   that   could   influence   the   problem   or   its   solution    
        Solution   proposed   to   the   problem    
        Context   for   the   solution    
        Rationale   behind   the   solution   (examples   and   stories   of   past   successes   or   failures   often   go   here)    
        Known   uses   and   related   patterns    
        Author   and   date   information    
        References   and   keywords   used   or   searching    
        Sample   code   related   to   the   solution,   if   it   helpsTop

14 楼BluntBlade(信仰迷离·重构之道,在于Redo/Undo之间)回复于 2006-10-09 11:31:10 得分 0

与其抱怨模式和语言工具,不如重新设计。  
  模式本身存在的风险可以使用一些语言技术、技巧来规避,但是如果还是达不到优雅的程度,那还是要考虑一下设计本身是不是有问题了。Top

15 楼PPower(月亮光光,照地堂)回复于 2006-10-09 13:23:41 得分 0

 
  void   CSubject::OnMsg(CSMSG   *pMsg)  
  {  
      list<IMsgListener*>   DoneList   ;     //已經收到通知的觀察者。  
      m_lstMsgListener   ;   //未收到消息的觀察者。  
  START:  
    for(list<IMsgListener*>::iterator   it   =   m_lstMsgListener.begin();  
  it   !=   m_lstMsgListener.end();   )  
  {  
      ASSERT(   NULL   !=   (*it)   );    
      if(std::find(DoneList.begin(),DoneList.end(),*pos)   ==   DoneList.end())  
      {  
          bool   modified   =   false   ;  
          (*it++)->Notify(pMsg   ,   &modified   );   //用++避免迭代子失效,返回是否修改容器。  
          if(modified   )  
              goto   START   ;     //這裡用了個goto   ,只是臨時寫的代碼,請別以為意。  
          Done.push_back(*pos);  
      }  
      else    
          ++it;  
  }  
  }  
   
  該方式可以支持在Notify函數中修改容器,修改Notify接口就可支持修改容器了。如果不需要支持在Notify中修改容器,那就不用改接口。將Notify接口設計成如下方式:  
  void   Notify(IMsgListener   *sender   ,   bool   modified   ==   false)   ;    
  用默認值就可以兼容不同的使用方式了。使用返回值的接口方式亦可。  
   
    我覺得這不是C++的問題,還是回歸到設計模式及對象間接口的問題上。  
  Top

16 楼OpenHero(开勇)回复于 2006-10-12 09:55:09 得分 0

学习设计模式的时候,java和C#确实比C++要方便,内存的自动释放,方便了很多工作,或许是一些现成提供的库,给了我们开发设计模式的更多的方便;  
  但是这里就不应该说C++对设计模式就不好;  
  在讨论到设计模式的高度的时候,应该不再计较语言本生的缺陷,而是在开发的过程中用的经验避免这些缺陷,设计模式不是循规蹈矩的,就如   Polarislee(北极星)(北京那么大,何处是我家)     那段英文所说,设计模式只是一种设计的概念,针对不同的问题,可能采用不同的方法,灵活的运用,而不是死套某种方法;  
   
  既然都在谈设计模式了,语言,本生来讲,或许就是工作了:)Top

17 楼cwowlonglong(猪都是怎么死的,不是笨死的,是被人杀死的)回复于 2006-10-12 11:02:32 得分 0

领教了Top

18 楼zzw820626(偶要分,偶要星星)回复于 2006-10-15 10:10:34 得分 0

支持Top

19 楼mcs51a(孙二)回复于 2006-10-16 00:54:48 得分 0

list   的insert   ,earse会导致iterator失效?Top

20 楼mcs51a(孙二)回复于 2006-10-16 01:00:49 得分 0

哦,你是在iterator::operator->()里将iterator   删除了吗.  
   
  象  
   
  TestClass::Test()  
  {  
        delete   this;  
        this->MemberFuction;  
  }  
   
  这样吗Top

21 楼FaKeChineseCompany()回复于 2006-10-16 02:14:19 得分 0

呵呵!楼上有很多前辈都说了,模式只是一种思想,你不能用一种语言实现的模式实例代码来套到另一个语言上,并且如果套不合适你就说另一个语言不适合模式。这种肤浅的论调也拿出说,我就不明白中国的程序员或学生怎么就是不能领会思想呢?学设计模式、学算法、学编程语言都有一大批人死记某个例子代码,而对那异常灵活的思想却视若不见,这实在是个学习方法的问题呀……   楼主就是这种人的“典型”代表,而且他是一个极度依懒“库”的人,没有了“库”他什么都不会干了,他还死抠书上的例子,抓住一个缺陷就在那猛吹,楼主大哥,我问你,你手上的哪本书的例子又是写得很好的、能够实用的?我在《Java编程思想》里随便就能找到类似的缺陷,你的脑子是不是有问题呀!书上的例子当然是有侧重点的,你看你那些Java的书,哪个小例子不是缺陷重重,这些你怎么不说了,你就是个SB,以为人家不知道你写这种讨论的目的呀!草……Top

22 楼Polarislee(北极星)(无房无车,飘在北京)回复于 2006-10-16 10:10:36 得分 0

楼上的同志话说得有点过了。我觉得楼主也不过就是遇到了问题发表两句感慨而以。  
   
  不要进行人身攻击,这里不是打架的地方:)Top

23 楼chaosgrass(潦潦草草)回复于 2006-10-16 22:39:49 得分 0

我也遇到了这个问题  
  后来设计了一个提供ForEach方法的安全链表,每次迭代过程对链表加锁  
  删除时判断锁标记,进行伪删除  
  退出最后一次迭代重入时,进行真正的清理工作  
   
  呵呵,在我们的产品上工作的很好,对了,我们产品代码行接近100万。。。Top

24 楼superzjx2000(承桴浮于海)回复于 2006-10-16 23:59:16 得分 0

讲实话,我在4年前第一次看设计模式时,第一个想法是:神阿,设计都有模式了,老子还混根毛阿,娘的像当年你老人家不会是按照设计模式来设计的世界的吧。后来我认为所谓的dp是从经验而来又不得不表示为形式来传达其意义,不管是用还是学还是分析思考,两者都不可缺,我们很多时候该做的只是在一堆约束之间找个平衡点,系统可以跑下去,项目可以做下去,工资可以领下去,就好象那个上帝也在找平衡点使世界可以转下去。。。。有点晕  
  上帝的世界都不完美,设计模式怎么会有上帝的设计完美,按照设计模式卡出来的设计们  
  又怎么会完美的一天,我们还是中庸一点实际一点的好Top

25 楼september_29(RSGIS)回复于 2006-10-17 11:52:10 得分 0

但是如果用户在Notify函数里不慎修改了m_lstListener,那么这段代码就会崩溃。  
  ----------------------------------------------  
  但是如果用户在Notify函数里不慎修改了m_lstListener.着说明既不是设计模式除了问题,也不是C++有问题,而是你编程的人有问题.Notify函数为什么要修改和自己无关的东西?  
  很明显这是作为软件设计者的人有问题.  
   
  不会做家具怪斧头的木匠一个  
  Top

26 楼mmosquito()回复于 2006-10-19 16:14:30 得分 0

〉那么这个问题如何解决呢?并不好解决。在单线程环境下,我建议用“观察者队列缓存”的方式:  
   
  约定Notify不能影响迭代子和listener才是解决之道。  
  Top

27 楼CQZE(Say It Loudly And Proudly - 发颗药)回复于 2006-10-22 13:35:37 得分 0

为什么要约定Notify不能Attach或Detach???????????  
   
  因为是list,所以能!  
   
  for(list<IMsgListener*>::iterator   it   =   m_lstMsgListener.begin();  
  it   !=   m_lstMsgListener.end();)  
  {  
  ASSERT(   NULL   !=   (*it)   );  
  ((*it)++)   ->Notify(pMsg);  
  }Top

28 楼CQZE(Say It Loudly And Proudly - 发颗药)回复于 2006-10-22 13:37:14 得分 0

WACKO...以上纯属垃圾代码  
  下面重新贴  
  --------------------------------------------------  
  为什么要约定Notify不能Attach或Detach???????????  
   
  因为是list,所以能!  
   
  for(list<IMsgListener*>::iterator   it   =   m_lstMsgListener.begin();  
  it   !=   m_lstMsgListener.end();)  
  {  
  ASSERT(   NULL   !=   (*it)   );  
  (*(it++))   ->Notify(pMsg);  
  }Top

29 楼h60009493()回复于 2006-10-22 16:55:43 得分 0

char   ch[1];  
  memcpy(ch   ,   "test"   ,   5);  
  大家认为这样行得通吗?  
  讨论list该不该修改,怎么不看看程序员自己在搞什么.字符串这样赋值,只要懂点程序的就会知道不行,为什么不能约定Notify中不能对list进行修改?  
  要对list进行修改,这样设计的是程序员本身的水平问题.  
  楼上的那段代码也不安全.举个例子:  
  当it   为   m_lstMsgListener的倒数第一值,这时候调用Notify时,将这个it进行了erase操作,那返回后++it,就不知道跑那里去了,循环不会结束,下一次循环照样core   dump.不是吗?Top

30 楼CQZE(Say It Loudly And Proudly - 发颗药)回复于 2006-10-22 21:43:49 得分 0

当it   为   m_lstMsgListener的倒数第一值,这时候调用Notify时,将这个it进行了erase操作,那返回后++it  
  -----  
  拜托,你在Notify里帮我把it   erase掉也...就算你可以erase掉,那也不得不佩服你把过多的细节以裸体般的形式暴露给observer以外的代码Top

31 楼h60009493()回复于 2006-10-23 21:37:56 得分 0

那你修改他的内容不也是祼着吗?Top

32 楼baina()回复于 2006-10-24 14:47:08 得分 0

据说现在微软也在开发一个类似jdk的c++类库Top

33 楼iorilchen()回复于 2006-10-26 17:02:41 得分 0

我也碰到过这个问题,后来是在update(和OnMsg函数一样功能,通知监控者)函数最前面加了锁,等到迭代过程结束后在进入执行.Top

34 楼CQZE(Say It Loudly And Proudly - 发颗药)回复于 2006-10-26 21:50:56 得分 0

回复人:h60009493()   (   一级(初级))   信誉:100   2006-10-23   21:37:57   得分:0  
  ?    
   
  那你修改他的内容不也是祼着吗?  
   
  ------------  
  怎么裸了呢?attach和detch是observer对外的接口呀Top

35 楼fayrount999(夜无痕)回复于 2006-10-27 09:27:35 得分 0

你喜歡用什麽語言就用什麽語言,既然選擇了C/C++那就注定要比使用JAVA多很多工作,生活不會來適應你Top

36 楼h60009493()回复于 2006-10-27 23:31:31 得分 0

detch不就是调用了pop吗?我所说的也没错啊~Top

37 楼h60009493()回复于 2006-10-27 23:32:19 得分 0

这个是模式本身的一点缺陷,需要根据实际情况来进行改进.Top

38 楼iambic()回复于 2006-10-27 23:47:24 得分 0

“甚至在GOF那本名著中,采用的也是这种类似的设计(只不过是用java语言而已)”  
   
  楼主说的是哪本名著?Design   Patterns?Java?Top

39 楼skyMountain(天山)回复于 2006-10-30 16:24:54 得分 0

to楼上:  
  不好意思,一时笔误.GOF那本采用的是C++.其它设计模式书籍,大部分采用的是java,包括《敏捷软件开发》那本。  
   
   
  发现绝大多数讨论者都没弄明白我的意思:  
   
  一、我说的这个缺陷,不是设计模式的问题。模式绝对是很好的。问题只是,C++程序员要实现和使用它,比较困难;新手如果按通常的方法来实现,后果是灾难性的:系统崩溃,而且崩溃原因难以定位。  
   
  二、有人说使用观察者模式时,一定要遵守约定“观察者不得detach和attach”。这是一种解决方法,但非常的不优雅——不觉得这方法不优雅的人,请回去重读设计模式。  
   
  三、有几位朋友贴出了代码,说能避免迭代子失效的。多谢这些热心的朋友,可惜这些代码是错误的。你们对迭代子失效的原因还不大熟悉,建议多深入了解STL,也可以来信和我讨论。  
   
  四、C++在模式面前的困惑主要有两点:  
   
          a、没有基于引用计数的内存回收机制。因此在使用一些模式的时候,如工厂模式、装饰模式等等,不得不违反C++中“谁申请内存,谁负责释放”原则——违反这个的结果就是,很容易导致内存泄漏或者野指针。  
          b、C++没有一个大型的、通用的底层库,因此没有供C++程序员使用的常用设计模式封装。Top

40 楼PPower(月亮光光,照地堂)回复于 2006-11-01 10:57:24 得分 0

“观察者不得detach和attach”可作為一種簡單模式,  
  “观察者detach和attach時要返回修改給調用者”亦可作為一種模式,這與C++或Java沒有關系,只與模式及其實現接口有關。  
   
  這樣的約定才能保障所有"观察者都收到且只收到一次消息",這也是保障迭代子不失效的方式。  
   
  基于引用计数的内存回收机制,   C++目前有智能指針這種方式,另外有強指針auto_ptr這方式傳遞變量的生命周期。說起庫來,目前C++的庫真正算上普及的也只有STL,相對大而全的java要差很多。C++试圖把设计模式通过库來提供的是loki,模板用法也讓使用者對這些庫持保守態度。我沒資格做C++與Java的對比,只把知道的說出來。  
   
  TO:skyMountain  
  我認為在“观察者detach和attach時要返回修改給調用者”   這一約定下是能避免迭代子失效的。我上面寫的代碼是錯誤的,但表達的意思是與下面的代碼一致的。下面重新整理後貼一下,希望你可以告訴我更多的失效細節。  
   
  extern   list<IMsgListener*>   m_lstMsgListener   ;   //觀察者隊列。  
  void   OnMsg(CSMSG   *pMsg)  
  {  
      list<IMsgListener*>   DoneList   ;//已經被通知的觀察者。  
      list<IMsgListener*>::iterator   it   =   m_lstMsgListener.begin();  
      while(it   !=   m_lstMsgListener.end())  
  {  
      if(find(DoneList.begin(),DoneList.end(),*it)   ==   DoneList.end())  
      {  
  bool   modified   =   false   ;  
  (*it++)->Notify(pMsg   ,   &modified   );  
                  /*   這裡用(*it++)是相當於  
                      list<IMsgListener*>::iterator   tmp   =   it   +   1   ;  
                      (*it)->Notify(pMsg   ,   &modified   );  
                      it   =   tmp   ;  
                      避免迭代子在   Notify   中自身更改帶來的問題。  
                  */  
  if(modified   )   //如果观察者已經detach和attach修改隊列  
  {  
        it   =   m_lstMsgListener.begin();//則重新開始傳遞消息  
        continue   ;  
  }  
  DoneList.push_back(*it);   //已經收到消息的观察者不再重發  
      }  
      else  
  ++it;  
  }  
  }  
  Top

41 楼PPower(月亮光光,照地堂)回复于 2006-11-01 11:07:41 得分 0

再看一眼,代碼邏輯還是有誤  
  DoneList.push_back(*it);   //已經收到消息的观察者不再重發  
  這句該放在(*it++)之前。  
  Top

42 楼showlie(想长膘的小猪……)回复于 2006-11-10 21:44:30 得分 0

markTop

43 楼zzpzheng(Aeolus)回复于 2006-11-15 16:17:02 得分 0

模式是好的,但是并不能解决所有问题,而且楼主用的也有问题。不可能奢望GOF给出一个不会让任何程序出错的模式。  
  而且楼主已经提到了C++语言本身并不很完善,那么就更应该考虑如何去处理各种可能的Exception了,而不是出了错就把问题归咎在模式身上。如果大家都这样,GOF早就被大家的口水淹死了。:)  
  个人认为模式仅仅是对某一类问题的一个比较好的解决方案,能不能解决实际问题,还跟程序具体的实现有很大关系。Top

44 楼goodboy1881(积木)(谁都别拦着我在水源升星)回复于 2006-11-15 16:30:08 得分 0

早就说过,如果你再叠代循环里面删除元素的话,你就要为之负责。。。Top

45 楼dananhai(大男孩)回复于 2006-11-16 15:19:12 得分 0

好多人阿,看不过来,只看了楼主的。  
  ************************************************************************  
  迭代失效是序列容器的弊端,而该处则不是,由此引出所有论断都因不符果。  
  ************************************************************************Top

46 楼nule(C/C++编程道长)回复于 2006-11-16 21:46:06 得分 0

这么多对模式有兴趣的,去设计模式板块一起讨论啊  
  Top

47 楼nicknide(封月翔天)回复于 2006-11-18 00:37:26 得分 0

楼主可以尝试一下ID管理,   而不要使用简单的列表之类的结构,   因为列表结构是在是脆弱....Top

48 楼heyuen877()回复于 2006-11-19 23:03:07 得分 0

迭代器本来就有这种问题。。  
  EFFECTIVE   STL,TCPL等书强调了N次()。  
  STL中的解决办法是算法函数返回修正后的ITERATOR给原来的ITERATOR。Top

49 楼mdzhao(读破书万卷)回复于 2006-11-20 22:45:21 得分 0

markTop

50 楼gameboy007(WoHaHa-WaHaHaHaHa)回复于 2006-11-22 00:36:33 得分 0

“叫一个人出去帮我买东西,但是如果我在他回来的路上把他杀掉,那麽我还能等他回来吗?”  
   
  楼主的解决办法是只要换另一个人去买就行了  
   
  Top

51 楼FantasyNES()回复于 2006-11-28 13:07:38 得分 0

markTop

52 楼jiang_nan1981()回复于 2006-11-28 16:28:45 得分 0

markTop

53 楼shan_ghost()回复于 2007-01-17 13:15:41 得分 0

楼主不是真正的CPPer...^_^  
   
  C/C++的哲学是:在不(潜在地)多耗费一个时钟周期的前提下,给用户提供更多的功能;  
   
  而java等语言的哲学则是:在保证功能、保证用户使用体验的前提下,尽量节省更多的时钟周期。  
   
  java认为它的用户都是工程师,C/C++认为它的用户都是算法大师^_^  
   
   
  比如LIST模板,提供一个“任何时间都能保证有效的ITERATOR”很容易;但如果用户只是拿它当普通固定大小的数组用,这个保证就只是在白白浪费时钟周期。  
  这点浪费,java可以不在乎,C/C++在乎。  
   
   
  所以,记住C/C++库的设计原则:如果最终算法是既不牺牲空间、又不牺牲时间的完美实现,提供它;如果最终算法在某方面有所折中,那么,提供它的完美的“零件”但不提供它(或者,既提供这些零件,也提供完美的折中版本);如果最终算法连“完美的零件”都不存在,不提供它。  
   
  所以,C++提供auto_ptr,但不提供通用的垃圾收集机制——虽然写一个也不难。  
   
  对于你的代码:要么自己扩展标准LIST,让它能保证ITERATOR总是有效;要么干脆以const关键字限制用户,使他不可能修改它。  
   
  Top

54 楼redleaves(程序员)回复于 2007-01-17 13:29:46 得分 0

程序的终极目标只不过是"正确,健壮,快速"而已.用适合的方法做适合的事,不要这种无谓的事上花太多力气.Top

55 楼zeusever()回复于 2007-03-05 18:29:34 得分 0

to:shan_ghost  
  好像可选择的垃圾收集已经在0x的列表中了吧。。。  
   
   
  不过我觉得这个问题不应该是c++   才有的吧。。实际上是你的观察者列表被脏读了哦。Top

56 楼heguodong()回复于 2007-03-06 13:21:39 得分 0

作者的意见我不赞同,我觉得设计模式在语言面前都是一样的,除非你不会设计或者不理解语言  
  迭代子失效的问题,是你自己编码的问题,C++语言给了你充分的自由去处理这些问题  
  工厂对象的问题,我认为也不是问题,对象自身删除自己不违背原则吧,那就好好看看COM对象是怎么删除的,这么自由的C++语言机制对你来讲却成了负担,那么我只能说你的C++还有限或者说设计有限Top

57 楼danjiewu(阿丹)回复于 2007-03-06 13:35:49 得分 0

不如每次都用lstMsgListener新生成的一个副本进行迭代,稍微多一些代价,但是一定安全。Top

58 楼holyfire(谁最衰啊你最衰,谁最帅啊我最帅)回复于 2007-03-09 10:05:45 得分 0

attach   deattch前先不要直接加入list,将申请放入另一个Command链表,等到有notify来的时候重新整理链表就可以了  
   
  我觉得这个问题是实用不当,比如说我在Notify里面Attach同一个Handler,那么就会引起死循环,Notify   ->   Attach   ->   Notify   ->   Attach  
   
  那么你觉得这个是模式的缺陷么Top

59 楼fohoo(飞狐)回复于 2007-03-16 11:25:55 得分 0

只是个iterator失效的问题吧,,不要被iterator缚住,不要被C++太多的特性迷住了双眼。  
   
  listener/event,在dispatch时,,取消listener是正当,,应当实现的。。如果换个角度,从C程序员的角度思考,解决方案就一目了然。。  
   
  iterator失效而已,就不让他失效,只做个删除标记就行了。  
   
  void   AddListener(IMsgListener*   listener)  
  {  
      //找到Listener,delete设为true,否则加listener  
  }  
   
   
  void   RemoveListener(IMsgListener*   listener)  
  {  
      //找到并设Listener的delete  
  }  
   
  void   CSubject::OnMsg(CSMSG   *pMsg)  
  {  
  for(map<IMsgListener*,bool>::iterator   it   =   m_lstMsgListener.begin();  
  it   !=   m_lstMsgListener.end();)  
  {  
      ASSERT(   NULL   !=   (*it)   );  
      if(*it.second)  
      {  
          erase(it++);  
      }  
      else  
      {    
            (*it++).first->Notify(pMsg);  
      }  
  }  
  }Top

60 楼boxban(冻酸梨)回复于 2007-03-16 17:40:44 得分 0

shan_ghost()   关于语言哲学的一段论述还是有一定道理的。  
    建议每个试图在比较Java   和   C++   时对某些C++中缺失的特性进行批评时,先去仔细读一读   B.S   的《C++语言的设计和演化》  
  Top

61 楼boxban(冻酸梨)回复于 2007-03-16 17:52:38 得分 0

另外,对于你提出的个案,有一个偷懒的解决办法:  
   
  void   CSubject::OnMsg(CSMSG   *pMsg)  
  {  
          list<IMsgListener*>::iterator   tmp;  
          for(list<IMsgListener*>::iterator   it   =   m_lstMsgListener.begin();  
                  it   !=   m_lstMsgListener.end();   )   //将it++   提前做掉  
          {  
                  tmp   =   it++;  
                  ASSERT(   NULL   !=   (*tmp)   );  
                  (*tmp)   ->Notify(pMsg);  
          }  
  }  
  Top

62 楼Aviyeah()回复于 2007-03-17 21:31:24 得分 0

以下是我的看法:  
      list<int>   l;  
      .....  
      list<int>::iterator   it   =   l.begin();  
      l.erase(i++);//这里的i是一个合法的非未端迭代器,指向l内部,递增一个有郊的迭代器  
  但下面这样写就有问题了  
     
      list<int>   l;  
      .....  
      list<int>::iterator   it   =   l.begin();  
      l.erase(i);  
      i++;//   这时的i不是一个有效的迭代器了。  
              //   大部分的右边++的原型都是这样的  
              //   const   obj   operator++(int);  
              //这说明了,这个运算符函数,i   的新值和旧值都存在于函数数体内,反回的是旧值。Top

63 楼mayflowers(黯然神伤)回复于 2007-03-22 13:42:01 得分 0

对于一个纯粹的   Observer   模式来说,要求   Observer   在被   Notify   的时候不能   Unregister   自身是很有必要的。  
   
  在   Subject   分发通知的时候,复制一份或者两份   Observer   链只能从形式上解决特定应用的问题;并不能解决所有问题。复制   Observer   链只能保证   Observer   将自己删除后,Iterator   依旧可用,就此而已。但是真正的   Observer   对象可能已经不复存在。也就是说   Subject   不能再对它进行任何操作,这只是一个约定,不是一个保证。  
   
  比如说在一个异步的架构下,Subject需要分发的不是一个事件,而是一列事件,当积累了多个事件后逐次发给   Observer:  
   
  list<sharde_ptr<Event>   >   event_list;  
  list<sharde_ptr<Observer>   >   observer_list;  
  for(   list<sharde_ptr<Event>   >iterator   iterObserver   =   ...   )   {  
          for(   list<sharde_ptr<Observer>   >iterator   iterEvent   =   ...   )   {  
                  (*iterObserver)->Notify(*iterEvent);  
          }  
  }  
   
  这种分发方式也许有些怪异,但是“优雅”的Observer难道要依赖于Subject分发事件的方式?  
  如果   Observer   在收到第一个事件后不仅仅   unregister,而且干脆自杀,结果会怎么样?Top

64 楼fohoo(飞狐)回复于 2007-03-23 13:53:43 得分 0

我的方法只做个删除标记,可以解决Observer在notify自杀的问题  
   
  void   AddListener(IMsgListener*   listener)  
  {  
      //找到Listener,delete设为true,否则加listener  
  }  
   
   
  void   RemoveListener(IMsgListener*   listener)  
  {  
      //找到并设Listener的delete  
  }  
   
  void   CSubject::OnMsg(CSMSG   *pMsg)  
  {  
  for(list<pair<IMsgListener*,bool>>::iterator   it   =   m_lstMsgListener.begin();  
  it   !=   m_lstMsgListener.end();)  
  {  
      ASSERT(   NULL   !=   (*it)   );  
      if(*it.second)  
      {  
          erase(it++);  
      }  
      else  
      {    
            (*it++).first->Notify(pMsg);  
      }  
  }  
  }  
  Top

65 楼thinkinnight(逍遥)回复于 2007-04-03 12:14:35 得分 0

大家的讨论真好,对于模式,我感觉也是这样,是否C++中的模式与GOF的有不同呢?Top

66 楼mgphuang(tony)回复于 2007-04-03 20:59:37 得分 0

哥们,你太形而上学了。模式只给你一个思路,任何一个模式的代码对于你的程序都是无效的。对于你的一会儿程可以写出无数不会出错的解法。比如,在观察者的Notify里使用返回值来确定是否要继续传递消息,而Notify中对观察者链的修改使用异步消息的方法。等观察者链处理完成后再集中处理对观察者链的修改。Top

67 楼chon81(当我遇上你…)回复于 2007-04-11 08:32:07 得分 0

我觉得问题不是设计模式或C++的问题.    
  设计模式本来只是提供一个设计思想,   对于具体实现和细节上要看实现者的实现.    
   
  你上面的代码问题是list的实现,   跟Observer模式是没有关系的.    
  Notify时list应该是一个复本,   对于这时加入新的观察者应该是在下次Notify时才有效.    
  如果一定要这次有效,   应该可以在添加时就Notify.Top

68 楼AWolfBoy(龍行江湖)回复于 2007-04-11 13:36:43 得分 0

对于楼主的问题其实这并不是什么Observer的设计模式造成的陷阱,是楼主对使用标准库容器还不够了解,可以去参考一下Effective   STL  
  vector/list/map/set--在遍历迭代子的时候,如果改变容器,只要不超过其最大容量是不会使原有迭代子失效的,但如果超过了,就将重新分配容量,并把原有数据重新拷贝一份到新的容器,这样迭代子肯定是要失效  
  deque--使用这个容器唯一比vector有好处的地方就是在于它的内存管理方式也是连续的,但属于页面式内存管理,这个怎么理解呢,就是说在容量增长的情况下,它会重新申请一块内存,但原有的迭代子指针不会失效  
   
  有一种方法可以解决楼上的问题,而且也允许动态改变lstMsgListener  
  void   CSubject::OnMsg(CSMSG   *pMsg)  
  {  
          int   nIterpos   =   0;  
          while   (nIterpos   <   lstMsgListener.size())  
          {  
                  vector<IMsgListener*>::iterator   it   =   m_lstMsgListener.at(nIterpos++);  
                  ASSERT(   NULL   !=   (*it)   );  
                  (*it)   ->Notify(pMsg);  
          }  
  }  
   
  以上代码需要把容器改为list才行,我觉得list相对vector来说唯一的好处也就在于内存上的消耗  
  Top

69 楼AWolfBoy(龍行江湖)回复于 2007-04-11 13:37:04 得分 0

是改为vector,一时打快了Top

70 楼netren_cn(netren)回复于 2007-04-11 14:27:28 得分 0

这个问题,我在写程序时都出现过,后来就是禁止了detach就没问题了,现在想想,为什么mfc的msg也用了这个模式,但是也没这个问题,其实关键就是list的问题,自己写个特定的list就可以了。Top

71 楼dyf198215()回复于 2007-04-20 14:39:43 得分 0

与设计模式无关哦,自己的用法问题了。  
   
  没有养成良好的习惯而已,对容器迭代循环时,最好不要对容器做增删等操作,不然问题当然多多了。Top

相关问题

关键词

得分解答快速导航

  • 帖主:skyMountain

相关链接

  • C/C++ Blog
  • C/C++类图书
  • C/C++类源码下载

广告也精彩

反馈

请通过下述方式给我们反馈
反馈
提问
网站简介|广告服务|VIP资费标准|银行汇款帐号|网站地图|帮助|联系方式|诚聘英才|English|问题报告
世纪乐知(北京)网络技术有限公司 版权所有, 京 ICP 证 020026 号
北京创新乐知广告有限公司 提供技术支持
Copyright © 2000-2007, CSDN.NET, All Rights Reserved
GongshangLogo