设计模式之调查者(Observer)模式与其C++通用实现(中)
当前位置:以往代写 > C/C++ 教程 >设计模式之调查者(Observer)模式与其C++通用实现(中)
2019-06-13

设计模式之调查者(Observer)模式与其C++通用实现(中)

设计模式之调查者(Observer)模式与其C++通用实现(中)

副标题#e#

通过上篇的先容我们知道了调查者模式的根基特点、利用场所以及如何故C++语言实现。有过多次编写调查者模式代码履历的你也许会发明,险些所有的案例存在为数相当可观的反复性代码:界说一个调查者接口;界说一个主题并实现其诸如注册一/多个调查者,移除一/多个调查者,广播至所注册的调查者等根基行为。既然如此,我们有没有大概为所有调查者模式抽象出共有的接口与行为,以便日后复用呢?

此篇文章即是探讨如何实现一个通用或称为万能的调查者模式库。

我们为所有的调查者/订阅者抽象出一个共有的接口IObserver:

struct IObserver{
  virtualvoidupdate()=0;
  virtual~Observer(){}
};

当主题状态产生改变时IObserver工具的update要了解被自动挪用。IObserver的子类会实现update要领,以便具有其特定的行为。思量到update要领的详细实现,大部门环境下我们需要查询主题的状态,从而做出回响。这有多种实现方案:一是生玉成局或雷同全局性(如Singleton技能)的主题工具:

Subjectg_subject;
...
structConcreteObserver:public IObjserver{
  virtualvoidupdate(){
    if(g_subject.getStatus()==xxx){
      ...
    }
};

因为“尽大概的不要利用全局工具”缘故,这种方法不常用。二是为update要领增加一个参数,以便奉告update某些须要的信息,为具有普遍性,我以

Event代表此类,界说如下:

struct Event{
  Event(Subject &subject);
  BasicSubject*getSubject();
  virtual~Event(){}
};

很明明,这应该是个基类,所以具有需析构要领,另外,Event还提供一个获取主题的要领。BasicSubject类是我们随后要说到的主题基类。这样,IObserver接口的界说看起来应该是这样:

struct IObserver{
  virtualvoidupdate(Event&event)=0;
  virtual~IObserver(){}
};

接下来处理惩罚我们的主题,按照前面所提到的它应该具有的行为,它的界说应该大抵像这样:

class BasicSubject{
public:
  virtual ~BasicSubject() {}

  voidaddObserver(IObserver&observer);
  voidremoveObserver(IObserver&observer);
protected:
  voidnotifyAll(Event&event);
protected:
  std::list<Observer*>observers_;
};


#p#副标题#e#

BasicSubject基类有三个要领,别离是增加一个调查者,移除一个调查者以及通知已注册调查者。至于其实现,我留给读者,看成操练。

此刻让我们通过以上三个基类(Event、IObserver及BasicSubject)来从头实此刻上篇中所给出的例子:

structMMEvent:publicEvent{
  MMEvent(MMInteligenceAgent &sub):Event(sub){}
};

structMMInteligenceAgent:public BasicSubject{
  MMStatusgetStatus()const{returnstatus_;}
  voidtrace(){notifyAll(MMEvent(this));} // for demonstrating how to use nofifyAll method.
private:
  MMStatusstatus_;
};

structLarcener:public IObserver{
  virtualvoidupdate(MMStatusstatus){
    if(status==Sleeping){
      ...
    }
  }
};

此刻是不是简朴了很多?

不要遏制你的脚步,更不要兴奋的过早。

我们事先界说了三个接口让我们的客户遵循,约束太多了。

主题Subject与调查者Observer之间固然已是抽象耦合(彼此认识对方的接口基类),但仍可改造,使两者间的耦合度更低。

思量到UI中的窗口设计,需要监督的窗口事件大概有:

windowOpened
windowClosing
windowIconified
windowDeiconified
windowActivated
windowActivated
windowDeactivated

倘若代码全由你一人设计,你大可将以上7个事件归并为一个粗事件并通过窗口(也就是这里的Subject了)提供一个符号表白今朝产生的是这7其中的哪一个事件,这没什么问题。可是,我相信并不是所有代码都由你一人包揽,设想你的同事或是客户将WindowEventListener(也就是这里的Observer)设计成几个独立的更新要领的环境吧(java即是如此)。糟糕,我们今朝界说的IObserver接口只支持单一更新要领。

是时候将我们的设计改造了。

事实上,在我们界说的三个基类傍边最没有意义的即是IObserver接口,它什么也没帮我们实现,仅是个Tag标志,以便我们能为BasicSubject类指明addObserver及removeObserver要领的参数。通过模板技能,我们不愿界说IObserver接口:

#p#分页标题#e#

template<
  classObserverT,
  classContainerT=std::list<ObserverT*>
>
classBasicSubject
{
public:
  inline voidaddObserver(ObserverT&observer);
  inline voidremoveObserver(ObserverT&observer);
protected:
  ContainerTobservers_;
};

#p#副标题#e#

BasicSubject不需要虚析构函数,因为客户不需要知道BasicSubject类的存在。类模板参数ContainerT的存在是为了让客户可以选择容器范例,默认容器范例是std::list,也许你的客户更喜欢std::vector,于是他便可这样利用:

class MyBasicSubject : public BasicSubject<MyObserver, std::vector<MyObserver*> { ...};

当BasicSubject状态改变时需要通知调查者,所以notifyAll要领仍不行缺少。思量到调查者大概具有多个更新要领,我们可以通过notifyAll要领的参数来指定要更新的要领。是的,就是函数指针了。所以nofifyAll要领大概是这样的:

template<typenameReturnT,typenameArg1T>
voidBasicSubject::notifyAll(ReturnT(ObserverT::*pfn)(Arg1T),Arg1Targ1){
  for(ContainerT::iteratorit=observers_.begin(),itEnd=observers_.end();it!=itEnd;++it){
    ((*it)->*pfn)(arg1);
  }
}

个中pfn是指向ObserverT类的、具有ReturnT返回范例的、吸收一个范例为Arg1T参数的函数的指针。

此刻连Event基类都不需要了,其脚色完全由模板参数范例Arg1T所代替。

问题远没有竣事。

仔细想想Arg1T参数范例的推导,编译器既可选择从pfn函数所声明的形参范例中推导也可选择从arg1实参推导,当实参(arg1)范例可独一推导且与pfn函数声明的形参范例完全匹配时没问题。当实参范例与形参范例不匹配时编译器报错。如:

structMyObserver{
  voidincrement(int &val){++val;}
};
structMySubject:publicBasicSubject<MyObserver>{
  voidtrigger(){
    inti=10;
    notifyAll(&MyObserver::increment,i);
  }
};

我的编译器上的报错信息大抵是:"template parameter ‘Arg1T’ is ambiguous" … "could be ‘int’ or ‘int &’"。编译器不知道Arg1T是int(从实参i推导)照旧int&(从函数increment形参val推导)。编译器真傻。

此问题的来源是模板参数

多渠道推导的不匹配性所致。为制止多渠道推导,智慧的你大概想到这样界说notifyAll要领:

template<typenameMemFunT,typename Arg1T>
void BasicSubject::notifyAll(constMemFunT&pfn, Arg1T &arg1);

值得表彰。

设想pfn所声明的形参范例是const引用范例(如const int&)而用户把常量(如10)直接用作实参的景象吧:

structMyObserver{
  voidincrement(const int&val){}
};
structMySubject:publicBasicSubject<MyObserver>{
  voidtrigger(){
    notifyAll(&MyObserver::increment, 10);
  }
};

#p#副标题#e#

编译器会诉苦不能把实参(10)范例(int)转换到形参(val)范例(const int&)。

那可否将arg1声明为const引用范例呢,即:

template<typenameMemFunT,typenameArg1T>
void BasicSubject::notifyAll(constMemFunT&pfn, const Arg1T &arg1);

这会限制调查者更新要领对参数举办任何修改,不行接管。

按着你的思路,我可以给你一种办理方案,不外要将notifyAll要领声明为:

template<typenameMemFunT,typenameArg1T>
inlinevoid notifyAll(constMemFunT&pfn,Arg1Targ1) ;

是的,arg1前少个引用(&)标记。当调查者更新要领的形参范例为非引用范例时没任何问题,仅仅是多了一次拷贝而使效率稍微低下罢了:

structMyObserver{
  voidincrement(int val){}
};
structMySubject:publicBasicSubject<MyObserver>{
  voidtrigger(){
    notifyAll(&MyObserver::increment, 10); // OK
  }
};

可是当形参范例为引用范例时直接利用的功效与预期行为不符:

structMyObserver{
  voidincrement(int&val){++val;}
};
structMySubject:publicBasicSubject<MyObserver>{
  voidtrigger(){
    int i = 10;
    notifyAll(&MyObserver::increment, i);
    cout << i << endl; // 输出10,但我们期望是11
  }
};

我们可以通过一个特另外帮助类将其办理:

template<typenameT>
classref_holder
{
  T&ref_;
public:
  inlineref_holder(T&ref):ref_(ref){}
  inlineoperatorT&()const{returnref_;}
};

template<typenameT>
inlineref_holder<T>ByRef(T&t){
  returnref_holder<T>(t);
}

函数ByRef的存在仅仅是为了利便生成ref_holder工具(雷同STL中的make_pair)。当需要引用通报时以ByRef函数浸染到实参上:

#p#分页标题#e#

structMyObserver{
  voidincrement(int&val){++val;}
};
structMySubject:publicBasicSubject<MyObserver>{
  voidtrigger(){
    int i = 10;
    notifyAll(&MyObserver::increment, ByRef(i));
    cout << i << endl; // 输出11,OK
  }
};

#p#副标题#e#

此刻没问题了,前提是能正确利用。可是,我敢赌博,你的客户会常常健忘ByRef函数的存在,乃至最终放弃你所提供的办理方案。

我会给出别的一种更完美的方案。

实际上,此处的notfiyAll要领是个转发函数,对其的挪用会转发给已向BasicSubject注册了的所有调查者工具的相应更新要领(我称之为目标函数)。为了具有正确的转刊行为以及较高的效率,转发函数的形参范例声明与目标函数的形参范例声明必需遵循必然的对应法则。篇幅所限,这里直接给出结论(以下将“转发函数形参”简称为“转发形参 ”,将“目标挪用函数形参”简称为“目标形参”。):

目标形参范例为const引用范例时,转发形参范例也是const引用范例;

目标形参范例为non-const引用范例时,转发形参范例也是non-const引用范例;

目标形参范例为其它范例时,转发形参范例是const引用范例。

我们通过模板traits技能可实现上面所提的转发——目标函数形参范例对应法则:

template<typenameT>
structarg_type_traits {
  typedefconstT&result;
};

template<typenameT>
structarg_type_traits<T&> {
  typedefT&result;
};

template<typenameT>
structarg_type_traits<constT&> {
  typedefconstT&result;
};

最后一个traits的存在是必需的,因为引用引用范例(如int&&)在C++中是不正当的。此刻我们可以界说我们的notifyAll要领了:

template<typenameReturnT,typenameArg1T>
inlinevoid BasicSubject::notifyAll(ReturnT(ObserverT::*pfn)(Arg1T),
  typenamearg_type_traits<Arg1T>::resultarg1) {
  for(ContainerT::iteratorit=observers_.begin(),itEnd=observers_.end();it!=itEnd;++it)
    ((*it)->*pfn)(arg1);
}

#p#副标题#e#

智慧的你大概会问,万一调查者的更新要领参数不是一个呢?说真的,我也很想确定到底具有几个参数,令我哀痛的是我的客户常常这样答复:“我也不知道有几个。”

我利用了一种较量简朴、鸠拙却行之有效的手段办理了这一问题。我通过重载notifyAll要领,使其别离对应更新要领是0、1、2、3……个参数的环境。

template<typenameReturnT>
inlinevoid BasicSubject::notifyAll(ReturnT(ObserverT::*pfn)()) {
  for(ContainerT::iteratorit=observers_.begin(),itEnd=observers_.end();it!=itEnd;++it)
    ((*it)->*pfn)();
}

template<typenameReturnT,typenameArg1T>
inlinevoid BasicSubject::notifyAll(ReturnT(ObserverT::*pfn)(Arg1T),
  typenamearg_type_traits<Arg1T>::resultarg1) {
  for(ContainerT::iteratorit=observers_.begin(),itEnd=observers_.end();it!=itEnd;++it)
    ((*it)->*pfn)(arg1);
}

template<typenameReturnT,typenameArg1T,typenameArg2T>
inlinevoid BasicSubject::notifyAll(ReturnT(ObserverT::*pfn)(Arg1T,Arg2T),
  typenamearg_type_traits<Arg1T>::resultarg1,
  typenamearg_type_traits<Arg2T>::resultarg2) {
  for(ContainerT::iteratorit=observers_.begin(),itEnd=observers_.end();it!=itEnd;++it)
    ((*it)->*pfn)(arg1,arg2);
}
...
template<typenameReturnT,typenameArg1T,typenameArg2T,typenameArg3T,typenameArg4T,typenameArg5T>
inlinevoid BasicSubject::notifyAll(ReturnT(ObserverT::*pfn)(Arg1T,Arg2T,Arg3T,Arg4T,Arg5T),
  typenamearg_type_traits<Arg1T>::resultarg1,
  typenamearg_type_traits<Arg2T>::resultarg2,
  typenamearg_type_traits<Arg3T>::resultarg3,
  typenamearg_type_traits<Arg4T>::resultarg4,
  typenamearg_type_traits<Arg5T>::resultarg5){
  for(ContainerT::iteratorit=observers_.begin(),itEnd=observers_.end();it!=itEnd;++it)
    ((*it)->*pfn)(arg1,arg2,arg3,arg4,arg5);
}

#p#分页标题#e#

按我的履历,高出5个参数的类要领不常见,要是你真的有幸碰着了,你大可让实现作者与你共进晚餐,虽然,账单由他付。你也大可再为notifyAll增加几个重载要领。

代码看起来有点巨大,但你的客户却很利便:

structMyObserver{
  voidcopy(intsrc,int&dest){dest=src;}
};
structMySubject:publicBasicSubject<MyObserver>{
  voidtrigger(){ // demonstrate how to use notifyAll method.
    int i= 0;
    notifyAll(&MyObserver::copy, 100, i);
    assert(i == 100);
  }
};
intmain(){
  MyObserverobs;
  MySubjectsub;
  sub.addObserver(obs);
  sub.trigger(); 
}

以上即是我所实现的通用调查者模式库的骨架。之所以称为骨架,是因为尚有很多诸如多线程等现实问题没有思量,我将在下篇中与读者一起探讨现实世界中大概碰着的问题。

<未完,待续>

    关键字:

在线提交作业