MVC模式的介紹(C#)
Benefits
在開發項目中使用“模型-視圖-控制器(MVC)”模式的好處在於可以完全消除商業流程和應用表達層之間的相互影響。此外,還可以獲得一個完全獨立的對象來控制表達層。本文項目里的這種獨立性使代碼的重用非常簡單,代碼的維護也稍微容易了一些(下面就會看到)。
通常我們都知道要讓對象盡量減少之間的依賴關系,這樣,我們努力編寫的代碼才容易修改。為了達到這種目的,需要遵循一個通行的原則采用MVC模式“在接口上編程,而不應該在類上編程”("programming to the interface, not the class" )。
我們的任務就是……
我們接了個任務——ACME 2000 賽車項目,任務就是編個簡單的交互界面,實現以下功能:1、顯示車輛的當前方向和速度;2、讓最終用戶可以改變方向、加速、減速。當然,這些功能都是有一定范圍限制的。
據說如果我們在這個項目上成功了,我們最后還要開發一個接口,實現類似的程序:ACME 2 皮卡和 ACME 1 三輪車 ACME 1 。做為程序員,我們了解ACME 的管理層最后會這樣說“啊,非常棒!可以放到公司的網站上嗎?”考慮到這些要求,我們要開發一個容易升級的產品,這樣我們才能讓顧客滿意,才能有飯吃,哈哈。:)
我想,正好…… “這個機會正好用來實現一下MVC模式!”
架構概述
現在我們知道要用MVC了,我們就需要知道MVC到底是什么東西。我們的項目要實現MVC模式的三部分:模型、控制器和視圖。在我們的項目中,汽車就是模型,用戶界面就是視圖,連接這兩部分的就是控制器。
我們使用控制器來操縱模型(ACME2000運動型汽車),控制器將向模型發送請求,並更新用戶接口——視圖。這樣看上去很簡單。我們需要解決的第一個問題是:用戶想讓汽車跑得更快或者是轉彎的話,要做些什么?答案就是通過視圖(我們的程序窗口),借助控制器發送一個請求。
我們還有一個問題需要解決:視圖沒有足夠的信息來顯示當前模型的狀態。解決辦法就是在圖里面增加一個箭頭——視圖能夠請求獲得足夠的模型狀態信息來顯示模型狀態。
這樣,用戶(司機)就可以通過視圖使用整個ACME汽車控制系統。如何用戶想操縱這個系統,比如說加速,視圖就會發出請求,並且由控制器來處理這個請求。控制器將把請求告訴模型,由模型來做出相應的動作,並且,控制器還將會更新試圖。
如果一個不守規矩的司機發出加速到底的指令,汽車將高速運行,這時候,司機再發出轉彎的指令,控制器就會在試圖中取消轉彎的功能,這樣就可以防止車禍的發生。
模型(汽車)會告訴視圖:速度已經升上去了,視圖就會做出相應的顯示。
總結一下,我們就可以預覽一下下面的架構了。
開始編寫我們的程序:
在動手之前,程序員應該做如下思考。我們的系統要足夠健壯,要想到盡量多的系統可能發生的變化。我們要牢記兩條黃金准則:類的松耦合,以及實現松耦合而使用的對接口編程。
所以,先增加三個接口(正如你猜到的,一個是模型的接口,一個是視圖的接口,還有一個就是控制器的接口)。
在和ACME的人打過充分的交道后,我們獲得了系統需求:汽車要能夠前進、倒車、轉彎,要設定前進、倒車、轉彎時的最大速度,儀表板(視圖)要能夠顯示當前的速度和方向。
需求很多,但我們能夠搞定它……
首先來做些准備工作。我們需要創建來個枚舉來表達方向和轉向請求,絕對方向(AbsoluteDirection)和相對方向(RelativeDirection)。
public enum AbsoluteDirection
{
North=0, East, South, West
}
public enum RelativeDirection
{
Right, Left, Back
}
然后,處理控制器接口。控制器要告訴模型如下請求:加速、減速、轉向。我們增加一個包含合適的方法的汽車控制接口(IVehicleControl)。
public interface IVehicleControl
{
void Accelerate(int paramAmount);
void Decelerate(int paramAmount);
void Turn(RelativeDirection paramDirection);
}
接下來,處理模型接口。我們需要知道汽車的名字、速度、最大前進速度、最大倒車速車、最大轉彎速度、方向。我們還需要如下方法:加速、減速、轉向。
public interface IVehicleModel
{
string Name{ get; set;}
int Speed{ get; set;}
int MaxSpeed{ get;}
int MaxTurnSpeed{ get;}
int MaxReverseSpeed { get;}
AbsoluteDirection Direction{get; set;}
void Turn(RelativeDirection paramDirection);
void Accelerate(int paramAmount);
void Decelerate(int paramAmount);
}
最后,處理視圖接口。我們知道,視圖需要向控制器暴露一些功能調用:允許/禁止加速/減速/轉向。
public class IVehicleView
{
void DisableAcceleration();
void EnableAcceleration();
void DisableDeceleration();
void EnableDeceleration();
void DisableTurning();
void EnableTurning();
}
現在,我們要調整一些這些接口,讓它們可以交互。首先,控制器要知道屬於它的視圖和模型,所以我們在汽車
public interface IVehicleControl
{
void RequestAccelerate(int paramAmount);
void RequestDecelerate(int paramAmount);
void RequestTurn(RelativeDirection paramDirection);
void SetModel(IVehicleModel paramAuto);
void SetView(IVehicleView paramView);
}
下一步使用了點技巧,我們使用GOF設計模式——觀察器(Observer)以便視圖得知模型的變化。
為了讓視圖能夠得知模型的變化,我們需要在模型里增加如下方法來實現這個設計模式:增加觀察其(AddObserver)、移除觀察器(RemoveObserver)、通知觀察者(NotifyObserver)。
public interface IVehicleModel
{
string Name{ get; set;}
int Speed{ get; set;}
int MaxSpeed{ get;}
int MaxTurnSpeed{ get;}
int MaxReverseSpeed { get;}
AbsoluteDirection Direction{get; set;}
void Turn(RelativeDirection paramDirection);
void Accelerate(int paramAmount);
void Decelerate(int paramAmount);
void AddObserver(IVehicleView paramView);
void RemoveObserver(IVehicleView paramView);
void NotifyObservers();
}
在視圖中增加如下的方法(用來觀察模型)。這樣,模型就會有個指向視圖的引用。當模型發生變化時,將會使
public class IVehicleView
{
void DisableAcceleration();
void EnableAcceleration();
void DisableDeceleration();
void EnableDeceleration();
void DisableTurning();
void EnableTurning();
void Update(IVehicleModel paramModel);
}
現在我們可以把這些接口放在一起了。只需要在剩余的代碼里面使用這些接口,就可以保證松耦合(好事一件)
public abstract class Automobile: IVehicleModel
{
#region "Declarations "
private ArrayList aList = new ArrayList();
private int mintSpeed = 0;
private int mintMaxSpeed = 0;
private int mintMaxTurnSpeed = 0;
private int mintMaxReverseSpeed = 0;
private AbsoluteDirection mDirection = AbsoluteDirection.North;
private string mstrName = "";
#endregion
#region "Constructor"
public Automobile(int paramMaxSpeed, int paramMaxTurnSpeed, int paramMaxReverseSpeed, string paramName)
{
this.mintMaxSpeed = paramMaxSpeed;
this.mintMaxTurnSpeed = paramMaxTurnSpeed;
this.mintMaxReverseSpeed = paramMaxReverseSpeed;
this.mstrName = paramName;
}
#endregion
#region "IVehicleModel Members"
public void AddObserver(IVehicleView paramView)
{
aList.Add(paramView);
}
public void RemoveObserver(IVehicleView paramView)
{
aList.Remove(paramView);
}
public void NotifyObservers()
{
foreach(IVehicleView view in aList)
{
view.Update(this);
}
}
public string Name
{
get
{
return this.mstrName;
}
set
{
this.mstrName = value;
}
}
public int Speed
{
get
{
return this.mintSpeed;
}
}
public int MaxSpeed
{
get
{
return this.mintMaxSpeed;
}
}
public int MaxTurnSpeed
{
get
{
return this.mintMaxTurnSpeed;
}
}
public int MaxReverseSpeed
{
get
{
return this.mintMaxReverseSpeed;
}
}
public AbsoluteDirection Direction
{
get
{
return this.mDirection;
}
}
public void Turn(RelativeDirection paramDirection)
{
AbsoluteDirection newDirection;
switch(paramDirection)
{
case RelativeDirection.Right:
newDirection = (AbsoluteDirection)((int)(this.mDirection + 1) %4);
break;
case RelativeDirection.Left:
newDirection = (AbsoluteDirection)((int)(this.mDirection + 3) %4);
break;
case RelativeDirection.Back:
newDirection = (AbsoluteDirection)((int)(this.mDirection + 2) %4);
break;
default:
newDirection = AbsoluteDirection.North;
break;
}
this.mDirection = newDirection;
this.NotifyObservers();
}
public void Accelerate(int paramAmount)
{
this.mintSpeed += paramAmount;
if(mintSpeed >= this.mintMaxSpeed) mintSpeed = mintMaxSpeed;
this.NotifyObservers();
}
public void Decelerate(int paramAmount)
{
this.mintSpeed -= paramAmount;
if(mintSpeed <= this.mintMaxReverseSpeed) mintSpeed = mintMaxReverseSpeed;
this.NotifyObservers();
}
#endregion
}
最后……
汽車框架有了,現在要實現剩下的兩個接口:控制器和模型。
現在通過實現汽車控制器接口來生成具體的汽車控制器(AutomobileControl )。汽車控制器基於模型狀態來設置
public class AutomobileControl: IVehicleControl
{
private IVehicleModel Model;
private IVehicleView View;
public AutomobileControl(IVehicleModel paramModel, IVehicleView paramView)
{
this.Model = paramModel;
this.View = paramView;
}
public AutomobileControl()
{
}
#region IVehicleControl Members
public void SetModel(IVehicleModel paramModel)
{
this.Model = paramModel;
}
public void SetView(IVehicleView paramView)
{
this.View = paramView;
}
public void RequestAccelerate(int paramAmount)
{
if(Model != null)
{
Model.Accelerate(paramAmount);
if(View != null) SetView();
}
}
public void RequestDecelerate(int paramAmount)
{
if(Model != null)
{
Model.Decelerate(paramAmount);
if(View != null) SetView();
}
}
public void RequestTurn(RelativeDirection paramDirection)
{
if(Model != null)
{
Model.Turn(paramDirection);
if(View != null) SetView();
}
}
#endregion
public void SetView()
{
if(Model.Speed >= Model.MaxSpeed)
{
View.DisableAcceleration();
View.EnableDeceleration();
}
else if(Model.Speed <= Model.MaxReverseSpeed)
{
View.DisableDeceleration();
View.EnableAcceleration();
}
else
{
View.EnableAcceleration();
View.EnableDeceleration();
}
if(Model.Speed >= Model.MaxTurnSpeed)
{
View.DisableTurning();
}
else
{
View.EnableTurning();
}
}
}
接下來是ACME200運動鞋汽車類(繼承了汽車抽象類,而汽車抽象類實現了汽車模型接口):
public class ACME2000SportsCar:Automobile
{
public ACME2000SportsCar(string paramName):base(250, 40, -20, paramName){}
public ACME2000SportsCar(string paramName, int paramMaxSpeed, int paramMaxTurnSpeed, int paramMaxReverseSpeed):
base(paramMaxSpeed, paramMaxTurnSpeed, paramMaxReverseSpeed, paramName){}
}
現在輪到視圖了……
我們來創建MVC三個組件中的最后一個——視圖。
創建一個汽車視圖來實現汽車視圖接口。汽車視圖包含了指向控制器接口和模型接口的引用:
public class AutoView : System.Windows.Forms.UserControl, IVehicleView
{
private IVehicleControl Control = new ACME.AutomobileControl();
private IVehicleModel Model = new ACME.ACME2000SportsCar("Speedy");
}
同樣需要將所有東西都連接到用戶控制器(UserControl)的構造器里。
public AutoView()
{
// This call is required by the Windows.Forms Form Designer.
InitializeComponent();
WireUp(Control, Model);
}
public void WireUp(IVehicleControl paramControl, IVehicleModel paramModel)
{
// If we're switching Models, don't keep watching
// the old one!
if(Model != null)
{
Model.RemoveObserver(this);
}
Model = paramModel;
Control = paramControl;
Control.SetModel(Model);
Control.SetView(this);
Model.AddObserver(this);
}
接下來,增加按鈕、標簽來顯示ACME2000運動型汽車的狀態,還有一個狀態欄用作填充按鈕的代碼。
private void btnAccelerate_Click(object sender, System.EventArgs e)
{
Control.RequestAccelerate(int.Parse(this.txtAmount.Text));
}
private void btnDecelerate_Click(object sender, System.EventArgs e)
{
Control.RequestDecelerate(int.Parse(this.txtAmount.Text));
}
private void btnLeft_Click(object sender, System.EventArgs e)
{
Control.RequestTurn(RelativeDirection.Left);
}
private void btnRight_Click(object sender, System.EventArgs e)
{
Control.RequestTurn(RelativeDirection.Right);
}
增加一個方法來更新用戶接口……
public void UpdateInterface(IVehicleModel auto)
{
this.label1.Text = auto.Name + " heading " + auto.Direction.ToString() + " at speed: " + auto.Speed.ToString();
this.pBar.Value = (auto.Speed>0)? auto.Speed*100/auto.MaxSpeed : auto.Speed*100/auto.MaxReverseSpeed;
}
最后,實現汽車視圖接口的方法……
public void DisableAcceleration()
{
this.btnAccelerate.Enabled = false;
}
public void EnableAcceleration()
{
this.btnAccelerate.Enabled = true;
}
public void DisableDeceleration()
{
this.btnDecelerate.Enabled = false;
}
public void EnableDeceleration()
{
this.btnDecelerate.Enabled = true;
}
public void DisableTurning()
{
this.btnRight.Enabled = this.btnLeft.Enabled = false;
}
public void EnableTurning()
{
this.btnRight.Enabled = this.btnLeft.Enabled = true;
}
public void Update(IVehicleModel paramModel)
{
this.UpdateInterface(paramModel);
}
搞定了!!!
我們來測試一下ACME2000運動型汽車。一切都按照計划那樣運行。接着,ACME 想要一輛皮卡。
好在我們使用了MVC!我們僅僅需要創建一個ACMETruck類,再把它連接上去,就可以用了。
public class ACME2000Truck: Automobile
{
public ACME2000Truck(string paramName):base(80, 25, -12, paramName){}
public ACME2000Truck(string paramName, int paramMaxSpeed, int paramMaxTurnSpeed, int paramMaxReverseSpeed):
base(paramMaxSpeed, paramMaxTurnSpeed, paramMaxReverseSpeed, paramName){}
}
在汽車視圖里,我們只需要創建皮卡,並把它連接上去。
private void btnBuildNew_Click(object sender, System.EventArgs e)
{
this.autoView1.WireUp(new ACME.AutomobileControl(), new ACME.ACME2000Truck(this.txtName.Text));
}
如何我們要創建新的控制器,這個控制器只允許最大5mph的加減速,好辦!創建一個限定加減速控制器(SlowPokeControl,和汽車控制器一樣,只多了個最大加減速的限制)。
public void RequestAccelerate(int paramAmount)
{
if(Model != null)
{
int amount = paramAmount;
if(amount > 5) amount = 5;
Model.Accelerate(amount);
if(View != null) SetView();
}
}
public void RequestDecelerate(int paramAmount)
{
if(Model != null)
{
int amount = paramAmount;
if(amount > 5) amount = 5;
Model.Accelerate(amount);
Model.Decelerate(amount);
if(View != null) SetView();
}
}
如果我們想給ACME2000皮卡加上限定加減速功能,再連接到汽車視圖里。
private void btnBuildNew_Click(object sender, System.EventArgs e)
{
this.autoView1.WireUp(new ACME.SlowPokeControl(), new ACME.ACME2000Truck(this.txtName.Text));
}
最后,我們想創建一個基於web的界面,需要做的就是創建一個web工程,並且在用戶控制器實現汽車視圖接口。
總結一下……
正如我們看到的那樣,使用MVC創建控制接口能夠很好地實現松耦合,而且能夠輕松地應對需求變化,減少需求變化帶來的影響。我們還可以隨處重用這些接口和抽象類。
在我們的項目里,還有幾個地方可以做得更加柔性可變,特別是請求改變模型狀態的實現,這些都將在下次討論。
請在你的項目里記住MVC,你不會后悔。
駕駛快樂!
注意,使用指向汽車模型接口的引用(不是汽車抽象類)來保持松耦合,汽車視圖也是這樣。 。通過實現汽車視圖接口(IVehicleView)可以生成任何顯示汽車狀態的人機界面,而我們的汽車ACME可以通過實現汽車模型接口(IVehicleModel)來生成,再通過實現汽車控制器接口(IVehicleControl)來生成汽車的操縱器。
接下來……哪些是我們通常要做的?
我們制作的汽車都是一樣的,所以,創建一個共同的框架來處理各種操作。因為我們不想讓別人駕駛一個框架, 所以就要使用一個抽象類(不允許創建抽象類的實例)。我們把這個抽象類叫做汽車(Automobile)。可以使用隊列列表(System.Collections中的ArrayList)來跟蹤所有的視圖(還記得觀察者模式嗎?)。當然也可以使用一個舊的汽車視圖平面隊列,但我們有太多的文章將這方面的東西可供參考。如果有興趣研究汽車模型接口和汽車視圖接口是如何交互的,可以研究一下如下方法的實現:AddObserver, RemoveObserver, and NotifyObservers。每當速度或方向發生變化時,模型將通知視圖。 用NotifyObservers方法,把指向視圖的引用傳給自己,並調用視圖的Update方法來通知視圖進行相應的變化。 控制接口中增加如下方法:設置模型(SetModel)和設置視圖(SetView)。