[概念理解] 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控制器的循環內,等待新的事件。在控制器中控制視圖的數據,和模型的數據。而視圖的顯示,則需要另外的一套機制來管理了吧。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM