互動式系統(MVC模式) 虛擬碼實現
上一篇博文簡要概述了MVC的基本框架流程 https://blog.csdn.net/yangfahe1/article/details/84075227
下面將利用虛擬碼編寫MVC建立流程,其中(1)到(6)是編寫MVC軟體框架的基本步驟,第(7)~第(10)步驟是可選的,可以提高靈活性,適合用於打造高度靈活的應用程式或者應用程式框架。
(1)將人機互動與核心功能分離。分析應用領域,將核心功能與所需的輸入輸出行為進行分離。設計應用程式的模型元件,使其封裝所需的核心資料和功能,並提供函式用於訪問要顯示的資料。確定要通過控制器將模型的那些功能暴露給客戶,進而給模型新增相應的介面。
在我們的例項中,模型在兩個等長的列表中分別儲存選舉人和得票數,並提供兩個返回迭代器的方法,用於訪問這些列表。該模型還提供了修改投票資料的介面。
class Model
{
List<long> votes;
List<string> parties;
public:
Model(List<string> partyNames);
void clearVotes();
void changeVotes(string party, long vote);
Iterator<long> makeVoteIterator()
{
return Iterator<long>(votes);
}
Iterator<string> makePartyIterator()
{
return Iterator<string>(partyNames);
}
//未完待續
};
(2)實現變更傳播機制。為此,可採用Publisher-Subscriber設計模式,並讓模型充當釋出者(publisher)。在模型中新增一個用來儲存觀察物件的容器,並提供讓檢視和控制器能夠向變更傳播機制註冊和解除安裝的過程。模型的通知過程呼叫所有觀察物件的更新過程。模型中每個修改模型狀態的過程都在執行修改後呼叫通知過程。
根據C++使用指南,應定義一個提供更新介面的抽象類Observer,並讓檢視和控制器都繼承Observe。對第(1)步的Model類進行擴充套件,使其包含一個觀察者的引用集合以及讓觀察者能夠註冊和解除安裝attach()和detach()介面。介面notify()被修改模型狀態的介面呼叫。
class Observer
{
public:
/*介面 用來更新檢視和控制器
*/
virtual void update();
}
class Model
{
//待續
public:
void attach(Observer *obs)
{
registry.add(s);
}
void detach(Observer *obs)
{
registry.remove(obs);
}
protected:
virtual void notify();
private:
List<Observer *> registry;
};
我們的notify()介面實現了遍歷所有已經註冊的Observe物件並呼叫其更新介面。由於儲存註冊物件的List僅供內部使用,因此我們沒有提供建立註冊物件的迭代器物件。
void Model::notify()
{
Iterator<Observer *>iter(registry);
while(iter.next()) iter.curr()->update();
}
介面changeVote和clearVotes在修改投票資料後呼叫notify();
(3)設計並實現檢視。設計每個檢視的外觀,規範並實現在螢幕上顯示檢視的繪圖(draw)過程。這個過程首先從模型那裡獲取要顯示的資料,餘下的部分與使用者介面平臺無關,如呼叫繪製線或渲染文字的過程。
實現反映模型變化的更新過程,為此最簡單的方法是呼叫繪圖過程,讓它取回檢視所需的資料。對於需要頻繁更新的複雜檢視,這種簡單的更新可能效率低下。在這種情況下,有多種優化策略可供使用。一種方法是給更新過程提供額外的引數,讓檢視能夠判斷是否需要重繪;另一種解決方案,在後面還有要求重繪檢視的事件時,不馬上繪製檢視,而等到沒有未處理的事件時再重繪檢視。
除了更新和繪圖過程外,每個檢視都需要包含一個初始化過程。初始化過程註冊模型的變更傳播機制,並關聯到控制器,如第(5)步所示。初始化控制器後,檢視將自己顯示到螢幕上。平臺或控制器可能要求檢視提供其他功能,如調整試圖視窗大小的過程。
在投票系統中,我們定義了基類View,所有檢視都從它派生而來。這個基類包含兩個成員變數(用於儲存於檢視相關聯的模型和控制器),並提供了獲取它們的介面。View的建構函式通過註冊變更傳播機制來與模型建立關係,而解構函式通過解除安裝來中斷這種關係。View還提供了一個update(),這個方法很簡單,未經優化。
class View
: public Observer
{
public:
View(Model *m)
: model(m)
, controller(0)
{
model->attach(this);
}
virtual ~View()
{
model->detach(this);
}
virtual void update()
{
this->draw();
}
virtual void initialize();
virtual void draw();
//未完待續
Model *getModel()
{
return model;
}
Controller *getController()
{
return controller;
}
protected:
Model *model;
Controller *controller;
};
class BarChartView
: public View
{
public:
BarChartView(Model *m)
: View(m)
{
}
virtual void draw();
}
void BarChartView::draw()
{
Iterator<string> party = model->makePartyIterator();
Iterator<long> vote = model->makeVoteIterator();
List<long> dl; //儲存充滿整個螢幕時的縮放比例
long max = 1;
while(vote.next())
{
if(vote.curr() > max) max = vote.curr();
}
vote.reset();
while(vote.next())
{
dl.append((MAXBARSIZE * vote.curr()) / max);
}
vote = dl;
vote.reset();
while(party.next() && vote.next())
{
//繪製文字
//繪製矩形
}
}
類BarChartview的定義演示了該系統的一個具體檢視,它重寫了方法draw(),使其使用柱狀顯示投票資料。
(4)設計並實現控制器。對於應用程式的每個檢視,確定系統如何響應使用者操作。我們規定底層平圖以事件的方法傳遞使用者執行的每項操作。控制器接受這些事件,並使用一個專用過程進行解讀。對於重要的控制起來說,這種解讀依賴於模型的狀態。
初始化控制器時應關聯到模型和檢視,並啟動事件處理。如何完成這些工作取決去使用者介面平臺,例如,控制器可向視窗系統註冊,並將事件處理過程指定為回撥函式。
在我們的示例中,大部分檢視都用於顯示結果,不需要處理事件,因此我們定義了基類Controller,它包含了一個空的handleEvent()介面。其中的建構函式將控制其關聯到模型,而解構函式斷開關聯。
class Controller
: public Observer
{
public:
virtual void handleEvent(Event *)
{
}
Controller(View *v)
: view(v)
{
model = view->getModel();
model->attach(this);
}
virtual ~Controller()
{
model->detach(this);
}
virtual void update()
{
}
protected:
Model *model;
View *view;
}
這裡沒有提供獨立的控制器初始化方法,因為已經在建構函式中關聯到了檢視和模型。
呼叫功能核心導致控制器與模型聯絡緊密,因為控制器依賴於應用程式特定的模型介面。如果你打算修改功能或希望控制器可重用,必須讓控制器獨立於特定的介面,為此可使用Command Processor設計模式。這種情況下,MVC模型將扮演Command Processor供應者(supplier)角色;在模型和控制器之間新增了命令類和命令處理器元件;而MVC控制器將扮演Command Processor控制器角色。
(5)設計並實現檢視-控制器關係。檢視通常在初始化期間建立與之相關聯的控制器。建立檢視和控制器類層次結構遵循Factory Method設計模式,在檢視類中定義介面makeController()。如果檢視所需的控制器與其超類所需的控制器不同,就必須重寫工廠介面makeController()。
在下面的C++示例程式碼中,基類View實現了介面initialize(),而這個方法呼叫了工廠介面makeController()。不能在View類的建構函式中呼叫makeController(),否則將不會呼叫子類中重寫的makeController()。在View的子類中,只有TableView需要的特殊控制器,因此他重寫了makeController(),使其返回一個可接受使用者輸入的TableController。
class View
: public Observer
{
public:
virtual void initialize()
{
controller = makeController();
}
virtual Controller *makeController()
{
return new Controller(this);
}
};
class TableController
: public Controller
{
public:
TableController(TableView *tv)
: Controller(tv)
{
}
virtual void handleEvent(Event *e)
{
//釋放事件e
//如更新等票數
if(vote && party)
{
model->changeVotes(party, vote);
}
}
};
class TableView
: public View
{
public:
TableView(Model *m)
: model(m)
{
}
virtual void draw();
virtual Controller *makeController()
{
return new TableController(this);
}
};
(6)實現搭建MVC的程式碼。搭建MVC的程式碼首先初始化模型,在建立並初始化檢視。完成初始化後,啟動事件處理,這通常是一個迴圈,也可能是一個包含迴圈的過程。由於模型應獨立於檢視和控制器,這種搭建程式碼應位於模型外部,如主程式中。
在下面的簡單程式碼示例中,函式main()初始化模型和多個檢視。事件處理將事件交給表檢視的控制器,讓使用者能夠輸入和修改投票資料。
main()
{
List<string> partys;
partys.append("black");
partys.append("red ");
partys.append("oth. ");
partys.append("blue ");
partys.append("green");
Model m(parties);
//初始化檢視
TableView *vl = new TableView(&m);
vl->initialize();
BarChartView *v2 = new BarChartView(&m);
v2->initialize();
//開始處理事件
}
(7)動態建立檢視。如果應用程式允許動態地開啟和關閉檢視,最好提供一個負責對開啟的試圖進行管理的元件。該元件還可以負責在最後一個試圖關閉後儲存資料和終止應用程式。要實現這種管理元件可以使用View Handle設計模式。
(8)“可插入式”的控制器。通過將控制方面與檢視分離,可將試圖與不同的控制器組合。可以利用這種靈活性實現不同的執行模式(如提供新使用者和專家級使用者使用的模式),還可以使用忽略所有輸入的控制器來構造只讀檢視。這種靈活性的另一種用途是,整合新的輸入和輸出裝置。例如:提供殘障人士使用的眼睛跟蹤裝置的控制器可利用既有模型和檢視的功能,很容易整合到系統中。
在我們示例程式碼中,只有TableView類支援多種控制器,其預設控制器TableController讓使用者能夠輸入投票資料。如果TableView只用來顯示資訊,那就可以給它配置一個忽略所有使用者輸入的控制。下面的程式碼演示瞭如果更換控制器。請注意,setController返回以前使用的控制器物件,這裡不再需要改控制器物件,因此馬上將它刪除掉。
class View
: public Observer
{
public:
virtual Controller *setController(Controller *ctrll);
};
main()
{
//*****
//更換控制器
delete v1->setController(new Controller(v1));
//*****
//開啟另一個只讀的檢視
TableView *v3 = new TableView(&m);
v3->initialize();
delete v3-setController(new Controller(v3));
//繼續事件處理
//*****
};
(9)建立檢視和建立控制器層次結構。基於MVC的框架實現了可重用的檢視類和控制器類,其中的檢視類通常表示常用的使用者介面元素,如按鈕、選單和文字編輯器。這樣,建立應用程式的使用者介面時,主要工作是組合預先定義好的試圖物件。可使用Composite模式來建立層次型組合檢視。如果有多個檢視處於活動狀態,這時可能同時有多個控制器對事件感興趣。例如,對話方塊中的按鈕響應滑鼠單擊,但不響應鍵盤輸入。如果該對話方塊還包含了一個文字框,鍵盤輸入將被髮送這個文字框的控制器。事件按指定順序依次傳遞給所有活動控制器的事件處理過程。要管理這種事件委託,可以使用Chain of Responsibility模式。如果正確地設定了責任鏈,控制器將把未處理的事件傳遞給夫檢視或者兄弟檢視的控制器。
(10)進一步降低系統的依賴性。打造框架需精心製作一系列檢視和控制器類,代價不菲。因此,你可能希望這些類獨立與平臺,有些系統就是這樣做得。為此可使用Bridge模式在系統和平臺之間再新增一層:讓檢視使用表示視窗display類,讓控制器使用負責處理事件的sensor類。
抽象類display定義了用於建立視窗、繪製線條和文字、修改滑鼠外觀等操作的方法;抽象類sensor定義了獨立於平臺的事件,每個sensor子類都將系統特定的市價對映到獨立於平臺的事件。為支援的每個平臺實現display和sensor子類。他們封裝了與系統相關的細節。
抽象類display和sensor的設計關係重大,因為他們將影響最終程式碼效率,還將影響在不同平臺上可實現的具體類的效率。一種方法是抽象類sensor和display只定義所有使用者介面平臺都提供的基本功能。另一個極端是讓display和sensor提供更高階的抽象,這種類使用的使用者介面平臺原生程式碼更多,移至工作量更大。採用第一種方法時,應用程式的外觀在不同平臺上看起來類似;採用第二種方法時,應用程式更嚴格地遵循了平臺特定的指導原則。