Enbiting 2020-06-03
[转]学习可以是一件很快乐的事,特别是当你发现以前所学的点点滴滴慢慢地能够串起来或者变成了一个环,这种感觉真好。这篇文章就这么来的。
从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解耦合。
2. 这一点更为重要,就是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前处理器,八仙过海,各显神通。”
结语
以上论点其实我并没有十足的把握,因为正如我所说的,实际中不采用多态可能有方方面面的考虑,而我所提到的这个原因也许微不足道。
为了反驳我自己的观点,可以计算一下,当MFC最开始诞生的时候,那个时候计算机的内存很小,所以大家很节约,但现在,计算机的内存非常大,一个vtable就算是上千个函数指针,那也是可以忽略不计的。
//------------------------------------------
//一个用C++多态实现的MVC(分离式)的结构。DEVC++编译通过.
//一个用C++写的MVC结构的一个小例子.
#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; }
one simple figure.
*感觉和观察者模式的UML非常类似,MVC有好多种实现形式,*MFC中不是用这种虚函数多态实现的。 MFC中添加了一个文档模板类,来管理多个文档。
Views类对象在最后,在所有前面的View类更新完数据后,负责标识出一个消息跟新过程的结束。最后再转入到Controler控制器的循环内,等待新的事件。在控制器中控制视图的数据,和模型的数据。而视图的显示,则需要另外的一套机制来管理了吧。