[转]学习可以是一件很快乐的事,特别是当你发现以前所学的点点滴滴慢慢地能够串起来或者变成了一个环,这种感觉真好。这篇文章就这么来的。
从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控制器的循环内,等待新的事件。在控制器中控制视图的数据,和模型的数据。而视图的显示,则需要另外的一套机制来管理了吧。