[概念理解] 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解耦合。

 

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结构的一个小例子.

  1 #include<iostream> 
  2 #include<vector>
  3 
  4 //get namespace related stuff 
  5 using std::cin; 
  6 using std::cout; 
  7 using std::endl; 
  8 using std::flush; 
  9 using std::string; 
 10 using std::vector; 
 11  
 12 //struct Observer, modeled after java.utils.Observer 
 13 struct Observer 
 14 /* 
 15  * AK: This could be a template (C++) or generic (Java 5), 
 16  * however the original Smalltalk MVC didn't do that. 
 17  */ 
 18 { 
 19    //update 
 20    virtual void update(void*)=0; 
 21 }; 
 22  
 23  //struct Observable, modeled after java.utils.Observable 
 24 struct Observable 
 25 { 
 26    //observers 
 27    vector<Observer*>observers; 
 28    
 29    //addObserver 
 30    void addObserver(Observer*a){observers.push_back(a);} 
 31    
 32    //notifyObservers 
 33    void notifyObservers() 
 34    { 
 35     for (vector<Observer*>::const_iterator observer_iterator=observers.begin();observer_iterator!=observers.end();observer_iterator++) 
 36      (*observer_iterator)->update(this); 
 37    } 
 38   
 39   /* 
 40   AK: If you had a method which takes an extra "ARG" argument like this 
 41   notifyObservers(void* ARG), you can pass that arg to each Observer via 
 42   the call (*observer_iterator)->update(this,ARG); 
 43   
 44   
 45   This can significantly increase your View's reusablity down the track. 
 46   I'll explain why below in the View. 
 47   */
 48 
 49 }; 
 50  
 51  
 52  //struct Model, contains string-data and methods to set and get the data 
 53 struct Model:Observable 
 54 { 
 55    //data members title_caption, version_caption, credits_caption 
 56    string title_caption; 
 57    string version_caption; 
 58    string credits_caption; 
 59    
 60    //data members title, version, credits 
 61    string title; 
 62    string version; 
 63    string credits; 
 64    
 65    //constructor 
 66    Model() : 
 67     title_caption("Title: "), 
 68     version_caption("Version: "), 
 69     credits_caption("Credits: "), 
 70     title("Simple Model-View-Controller Implementation"), 
 71     version("0.2"), 
 72     credits("(put your name here)") 
 73     { } 
 74    
 75    //getCredits_Caption, getTitle_Caption, getVersion_Caption 
 76    string getCredits_Caption(){return credits_caption;} 
 77    string getTitle_Caption(){return title_caption;} 
 78    string getVersion_Caption(){return version_caption;} 
 79    
 80    //getCredits, getTitle, getVersion 
 81    string getCredits(){return credits;} 
 82    string getTitle(){return title;} 
 83    string getVersion(){return version;} 
 84    
 85    //setCredits, setTitle, setVersion 
 86    void setCredits(string a){credits=a;notifyObservers();} 
 87    void setTitle(string a){title=a;notifyObservers();} 
 88    void setVersion(string a){version=a;notifyObservers();} 
 89   /* 
 90    * AK notifyObservers(a) for credit, title and version. 
 91    * All as per discussion in View and Observer *  
 92    */ 
 93 };
 94 
 95 
 96 /* 
 97 AK: 
 98 Great stuff ;-) This satisfies a major principle of the MVC 
 99 architecture, the separation of model and view.
100 
101 The model now has NO View material in it, this model can now be used in 
102 other applications. 
103 You can use it with command line apps (batch, testing, reports, ...), 
104 web, gui, etc.
105 
106 Mind you "MVC with Passive Model" is a variation of MVC where the model 
107 doesn't get even involved with the Observer pattern.
108 
109 In that case the Controller would trigger a model update *and it* could 
110 also supply the latest info do the Views. This is a fairly common MVC 
111 variation, especially with we apps. 
112 */
113 
114  
115 
116  //struct TitleView, specialized Observer 
117 struct TitleView:Observer 
118 { 
119 /* 
120  * AK: 
121  * I like to get a reference to the model via a constructor to avoid 
122  * a static_cast in update and to avoid creating zombie objects. 
123  * 
124  * A zombie object is instantiated but is unusable because it 
125  * is missing vital elements. Dangerous. Getting model via the 
126  * constructor solves this problem.
127 
128  Model model; 
129  // Cons. 
130  TitleView (Model* m) ....
131 
132 RE-USABILITY. 
133 Some views are better off working with the full Model, yet others are 
134 better off being dumber.
135 
136 I like to have two kinds of Views. Those that work with full Model (A) 
137 and those that only work with a limited more abstract data type (B).
138 
139 Type A. 
140 Complex application specific views are better off getting the full 
141 model, they can then just pick and choose what they need from the full 
142 model without missing something all the time. Convenient.
143 
144 Type B. 
145 These only require abstract or generic data types.
146 
147 Consider a PieChartView, it doesn't really need to know about the full 
148 Model of a particular application, it can get by with just float 
149 *values[] or vector<float>;
150 
151 By avoiding Model you can then reuse PieChartView in other applications 
152 with different models.
153 
154 For this to be possible you must use the 2 argument version of 
155 notifyObservers. See comments on Observer class.
156 
157 See my Java example NameView. That view only knows about a String, not 
158 the full Model. 
159 */
160 
161 
162    //update 
163    void update(void*a) 
164   /* 
165    *AK:void update(void*a, void*arg) is often better. As per discussion 
166   above. 
167    */ 
168    { 
169    cout<<static_cast<Model*>(a)->getTitle_Caption(); 
170    cout<<static_cast<Model*>(a)->getTitle(); 
171    cout<<endl; 
172    } 
173 }; 
174  
175  
176  //struct VersionView, specialized Observer 
177 struct VersionView:Observer 
178 { 
179  
180  //update 
181  void update(void*a) 
182  { 
183  cout<<static_cast<Model*>(a)->getVersion_Caption(); 
184  cout<<static_cast<Model*>(a)->getVersion(); 
185  cout<<endl; 
186  } 
187 }; 
188  
189  
190  //struct CreditsView, specialized Observer 
191 struct CreditsView:Observer 
192 { 
193  
194  //update 
195  void update(void*a) 
196  { 
197  cout<<static_cast<Model*>(a)->getCredits_Caption(); 
198  cout<<static_cast<Model*>(a)->getCredits(); 
199  cout<<endl; 
200  } 
201 }; 
202  
203  
204  //struct Views, pack all Observers together in yet another Observer 
205 struct Views:Observer 
206 { 
207  //data members titleview, versionview, creditsview 
208  TitleView titleview; 
209  VersionView versionview; 
210  CreditsView creditsview; 
211 /* 
212  * AK: 
213  * Views are often hierarchical and composed of other Views. See 
214 Composite pattern. 
215  * vector<View*> views; 
216  * 
217  * Views often manage (create and own) a Controller. 
218  * 
219  * Views may include their own Controller code (Delegate). 
220  * 
221 */ 
222  //setModel 
223  void setModel(Observable&a) 
224  { 
225  a.addObserver(&titleview); 
226  a.addObserver(&versionview); 
227  a.addObserver(&creditsview); 
228  a.addObserver(this); 
229  } 
230  
231  //update 
232  void update(void*a) 
233  { 
234  cout<<"_____________________________"; 
235  cout<<"\nType t to edit Title, "; 
236  cout<<"v to edit Version, "; 
237  cout<<"c to edit Credits. "; 
238  cout<<"Type q to quit./n>>"; 
239  } 
240 }; 
241  
242  
243  //struct Controller, wait for keystroke and change Model 
244  struct Controller 
245 /* 
246  * AK: Controller can also be an Observer. 
247  * 
248  * There is much to say about Controller but IMHO we should defer 
249  * that to another version. 
250  */ 
251 { 
252    //data member model 
253    Model*model; 
254    
255    //setModel 
256    void setModel(Model&a){model=&a;} 
257    
258    //MessageLoop 
259    void MessageLoop() 
260    { 
261     char c=' '; 
262     string s; 
263     while(c!='q') 
264     { 
265       cin>>c; 
266       cin.ignore(256,'\n'); 
267       cin.clear(); 
268       switch(c) 
269       { 
270        case 'c': 
271        case 't': 
272        case 'v': 
273        getline(cin,s); 
274        break; 
275       } 
276       switch(c) 
277       { 
278        case 'c':model->setCredits(s);break; 
279        case 't':model->setTitle(s);break; 
280        case 'v':model->setVersion(s);break; 
281       } 
282     } 
283    } 
284 }; 
285  
286  
287  //struct Application, get Model, Views and Controller together 
288 struct Application 
289 { 
290  
291    //data member model 
292    Model model; 
293    
294    //data member views 
295    Views views; 
296    
297    //data member controller 
298    Controller controller; 
299    
300    //constructor 
301    Application() 
302    { 
303    views.setModel(model); 
304    controller.setModel(model); 
305    model.notifyObservers(); 
306    } 
307    
308    //run 
309    void run(){controller.MessageLoop();} 
310 }; 
311  
312  
313  //main 
314 int main() 
315 { 
316   Application().run(); 
317   return 0; 
318 }

 

 

one simple figure.

 

*感觉和观察者模式的UML非常类似,MVC有好多种实现形式,*MFC中不是用这种虚函数多态实现的。 MFC中添加了一个文档模板类,来管理多个文档。

Views类对象在最后,在所有前面的View类更新完数据后,负责标识出一个消息跟新过程的结束。最后再转入到Controler控制器的循环内,等待新的事件。在控制器中控制视图的数据,和模型的数据。而视图的显示,则需要另外的一套机制来管理了吧。

posted @ 2014-04-04 09:43 tt-player 阅读(...) 评论(...) 编辑 收藏