MVC

博客园首页新随笔联系订阅管理随笔 - 16 文章 - 7 评论 - 1
[概念理解] MVC模式和C++的实现
[转]学习可以是一件很快乐的事,特别是当你发现以前所学的点点滴滴慢慢地能够串起来或者变成了一个环,这种感觉真好。这篇文章就这么来的。

从MVC架构开始说起吧。这两天系统了解了一下MVC架构的内容,主要参考于文献【1】。

MVC在这几年应该被非常多的人所熟悉了,因为相当多的web框架采用的是这套架构,此外,早在MFC横行的年代,MFC所采用的document/view架构也是MVC架构的变种。包括QT,它的model/view亦是如此。只不过它们都将MVC中的view和controller的功能整合到了一起。

MVC的全称是model-view-controller architecture,最早被用在了smalltalk语言中。MVC最适合用在交互式的应用程序中。

我个人认为,理解MVC架构最重要的是两点:

  1. MVC将数据的维护和数据的呈现,与用户的交互割裂了。Model负责的是数据的维护,就好比是DB和文件要保存数据一样,可以认为它是process。而view负责的是数据的呈现,把数据通过某种形式在用户面前展现,把它看做是output。model和view的关系就像下面这幅图一样。

而controller负责的是处理用户的输入。它提供一个窗口或者是控件供用户输入数据,它就是input。所以,一个MVC架构,就是将交互式程序的process,input和output解耦合。

  1. 这一点更为重要,就是model与view和controller之间的联系。任何一个MVC框架都必须提供一个“change-propagation mechenism”(变更-传播机制)。而这个变更-传播机制是MVC框架中model和view以及controller之间的唯一的联系(The change-propagation mechanism is the only link between the model and the views and controllers)。比如说,一个用户通过controller改变了model中的数据,那么model会使用变更-传播机制来通知和它有关的view和controller,使得view去刷新数据和重新显示。

有很多人总是对于MVC架构不能够熟练掌握,核心问题就在于他在使用MVC架构的时候是看不到变更-传播系统的,所以对于MVC架构的内在运行机制无法了解。

完整过程如图所示:

在这里插入图片描述

  1. 用户操作controller,相当于调用controller的handleEvent函数;

  2. handleEvent函数实际上调用的是model中的成员函数,对model中的数据进行操作;

  3. model中的数据被调用完成后,model会执行自身的notify函数;

  4. notify函数会调用和这个model有关的所有view和controller的update函数;

  5. update函数会调用各自的display函数和getData函数;

  6. 当这一切都完成时,handleEvent才返回;

更多的关于MVC的内容就不在这篇文章中详述了,毕竟俺写这文章不是光为了MVC。有兴趣的可以查看网络文档或者参考文献。

下面的重点在于讨论这个change-propagation mechenism的实现。

其实一个简单MVC架构的变更-传播机制采用observer模式+多态就可以搞定了。model维护一个基类view(和controller)的指针队列,将所有和这个model相关的派生view的指针放在这个队列中。那么model的notify函数就是依次调用队列中的指针的update成员函数。

但是,在实际的C++的MVC系统中,比如MFC,或者QT,都没有采用这种方法来实现变更-传播机制,事实上,他们在实现这个机制的时候都没用到多态。MFC采用的是消息映射的机制,基本概念是建了一个消息查找表,将消息和对应函数的映射关系存储下来。每次处理一个消息的时候,都去表中查找到对应的函数,然后回调。而QT采用的signal-slot机制(具体的实现机制我不清楚,但肯定不是用的多态)。

为什么MFC和QT都不采用多态呢?我相信有很多的原因,比如QT的signal-slot要求是能够跨进程的,这肯定不是用多态能做到的。在这我只讨论一个原因。

讨论之前先说一说C++的多态机制的实现(我更推荐你看参考文献【2】而不是我的这段话,【2】中把这个问题解释得非常清楚)。很多人都知道vtable,这里放着某个类的一个虚函数指针数组,某个类的指针如果要调用虚函数,先会通过vptr找到vtable,然后查找到对应的函数。这个机制本身没有问题。但关键是,C++在vtable中保存的是所有虚函数的指针,也就是说,如果一个基类有1000个虚函数,但它的继承类只改写了其中的5个,那么这个继承类的vtable中仍然有1000项,表中的995项被浪费了。正是由于这个原因,MFC和QT都没有采用C++的多态机制来实现变更-传播机制。因为在MFC和QT中,它的每个基类都有着大量的虚函数,而在实际应用当中,继承类可能只是改写其中的很少的几项,如果采用多态实现,那么会浪费大量的内存空间。

借用文献【2】的一段话,“也正 是因为这个原因,从OWL 到VCL,… 从MFC到Qt,以至于近几年出现的GUI和游戏开发框架,所有涉及大量事件行为的C++ GUI Framework没有一家使用标准的C++多态技术来构造窗口类层次,而是各自为战,发明出五花八门的技术来绕过这个暗礁。其中比较经典的解决方案有 三,分别以VCL 的动态方法、MFC的全局事件查找表和Qt 的Signal/Slot为代表。而其背后的思想是一致的,用Grady Booch的一句话来总结,就是:“当你发现系统中需要大量相似的小型类的时候,应当用大量相似的小型对象解决之。” 也就是说,将一些本来会导致需要派生新类来解决的问题,用实例化新的对象来解决。这种思路几乎必然导致类似C#中delegate那样的机制成为必需品。 可惜的是,标准C++ 不支持delegate。虽然C++社群里有很多人做了各种努力,应用了诸如template、functor等高级技巧,但是在效果上距离真正的 delegate还有差距。因此,为了保持解决方案的简单,Borland C++Builder扩展了__closure关键字,MFC发明出一大堆怪模怪样的宏,Qt搞了一个moc前处理器,八仙过海,各显神通。”

结语

#include<iostream>
#include<vector>

//get namespace related stuff
using std::cin;
using std::cout;
using std::endl;
using std::flush;
using std::string;
using std::vector;

//struct Observer, modeled after java.utils.Observer
struct Observer
/*
 * AK: This could be a template (C++) or generic (Java 5),
 * however the original Smalltalk MVC didn't do that.
 */
{
   //update
   virtual void update(void*)=0;
};

 //struct Observable, modeled after java.utils.Observable
struct Observable
{
   //observers
   vector<Observer*>observers;

   //addObserver
   void addObserver(Observer*a){observers.push_back(a);}

   //notifyObservers
   void notifyObservers()
   {
    for (vector<Observer*>::const_iterator observer_iterator=observers.begin();observer_iterator!=observers.end();observer_iterator++)
     (*observer_iterator)->update(this);
   }

  /*
  AK: If you had a method which takes an extra "ARG" argument like this
  notifyObservers(void* ARG), you can pass that arg to each Observer via
  the call (*observer_iterator)->update(this,ARG);


  This can significantly increase your View's reusablity down the track.
  I'll explain why below in the View.
  */

};


 //struct Model, contains string-data and methods to set and get the data
struct Model:Observable
{
   //data members title_caption, version_caption, credits_caption
   string title_caption;
   string version_caption;
   string credits_caption;

   //data members title, version, credits
   string title;
   string version;
   string credits;

   //constructor
   Model() :
    title_caption("Title: "),
    version_caption("Version: "),
    credits_caption("Credits: "),
    title("Simple Model-View-Controller Implementation"),
    version("0.2"),
    credits("(put your name here)")
    { }

   //getCredits_Caption, getTitle_Caption, getVersion_Caption
   string getCredits_Caption(){return credits_caption;}
   string getTitle_Caption(){return title_caption;}
   string getVersion_Caption(){return version_caption;}

   //getCredits, getTitle, getVersion
   string getCredits(){return credits;}
   string getTitle(){return title;}
   string getVersion(){return version;}

   //setCredits, setTitle, setVersion
   void setCredits(string a){credits=a;notifyObservers();}
   void setTitle(string a){title=a;notifyObservers();}
   void setVersion(string a){version=a;notifyObservers();}
  /*
   * AK notifyObservers(a) for credit, title and version.
   * All as per discussion in View and Observer *
   */
};


/*
AK:
Great stuff ;-) This satisfies a major principle of the MVC
architecture, the separation of model and view.

The model now has NO View material in it, this model can now be used in
other applications.
You can use it with command line apps (batch, testing, reports, ...),
web, gui, etc.

Mind you "MVC with Passive Model" is a variation of MVC where the model
doesn't get even involved with the Observer pattern.

In that case the Controller would trigger a model update *and it* could
also supply the latest info do the Views. This is a fairly common MVC
variation, especially with we apps.
*/



 //struct TitleView, specialized Observer
struct TitleView:Observer
{
/*
 * AK:
 * I like to get a reference to the model via a constructor to avoid
 * a static_cast in update and to avoid creating zombie objects.
 *
 * A zombie object is instantiated but is unusable because it
 * is missing vital elements. Dangerous. Getting model via the
 * constructor solves this problem.

 Model model;
 // Cons.
 TitleView (Model* m) ....

RE-USABILITY.
Some views are better off working with the full Model, yet others are
better off being dumber.

I like to have two kinds of Views. Those that work with full Model (A)
and those that only work with a limited more abstract data type (B).

Type A.
Complex application specific views are better off getting the full
model, they can then just pick and choose what they need from the full
model without missing something all the time. Convenient.

Type B.
These only require abstract or generic data types.

Consider a PieChartView, it doesn't really need to know about the full
Model of a particular application, it can get by with just float
*values[] or vector<float>;

By avoiding Model you can then reuse PieChartView in other applications
with different models.

For this to be possible you must use the 2 argument version of
notifyObservers. See comments on Observer class.

See my Java example NameView. That view only knows about a String, not
the full Model.
*/


   //update
   void update(void*a)
  /*
   *AK:void update(void*a, void*arg) is often better. As per discussion
  above.
   */
   {
   cout<<static_cast<Model*>(a)->getTitle_Caption();
   cout<<static_cast<Model*>(a)->getTitle();
   cout<<endl;
   }
};


 //struct VersionView, specialized Observer
struct VersionView:Observer
{

 //update
 void update(void*a)
 {
 cout<<static_cast<Model*>(a)->getVersion_Caption();
 cout<<static_cast<Model*>(a)->getVersion();
 cout<<endl;
 }
};


 //struct CreditsView, specialized Observer
struct CreditsView:Observer
{

 //update
 void update(void*a)
 {
 cout<<static_cast<Model*>(a)->getCredits_Caption();
 cout<<static_cast<Model*>(a)->getCredits();
 cout<<endl;
 }
};


 //struct Views, pack all Observers together in yet another Observer
struct Views:Observer
{
 //data members titleview, versionview, creditsview
 TitleView titleview;
 VersionView versionview;
 CreditsView creditsview;
/*
 * AK:
 * Views are often hierarchical and composed of other Views. See
Composite pattern.
 * vector<View*> views;
 *
 * Views often manage (create and own) a Controller.
 *
 * Views may include their own Controller code (Delegate).
 *
*/
 //setModel
 void setModel(Observable&a)
 {
 a.addObserver(&titleview);
 a.addObserver(&versionview);
 a.addObserver(&creditsview);
 a.addObserver(this);
 }

 //update
 void update(void*a)
 {
 cout<<"_____________________________";
 cout<<"\nType t to edit Title, ";
 cout<<"v to edit Version, ";
 cout<<"c to edit Credits. ";
 cout<<"Type q to quit./n>>";
 }
};


 //struct Controller, wait for keystroke and change Model
 struct Controller
/*
 * AK: Controller can also be an Observer.
 *
 * There is much to say about Controller but IMHO we should defer
 * that to another version.
 */
{
   //data member model
   Model*model;

   //setModel
   void setModel(Model&a){model=&a;}

   //MessageLoop
   void MessageLoop()
   {
    char c=' ';
    string s;
    while(c!='q')
    {
      cin>>c;
      cin.ignore(256,'\n');
      cin.clear();
      switch(c)
      {
       case 'c':
       case 't':
       case 'v':
       getline(cin,s);
       break;
      }
      switch(c)
      {
       case 'c':model->setCredits(s);break;
       case 't':model->setTitle(s);break;
       case 'v':model->setVersion(s);break;
      }
    }
   }
};


 //struct Application, get Model, Views and Controller together
struct Application
{

   //data member model
   Model model;

   //data member views
   Views views;

   //data member controller
   Controller controller;

   //constructor
   Application()
   {
   views.setModel(model);
   controller.setModel(model);
   model.notifyObservers();
   }

   //run
   void run(){controller.MessageLoop();}
};


 //main
int main()
{
  Application().run();
  return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值