原文:Introduction to Model View Control (MVC) Pattern using 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,你不会后悔。
驾驶快乐!
联系客服