The benefits of using the Model-View-Control (MVC) pattern in our development projects is that we can completely decouple our business and presentation application layers. Furthermore, we will have a completely independent object to control the presentation layer. The independence between the objects/layers in our project that the MVC provides will make maintenance somewhat easier and code reuse very easy (as you'll see below).
As a general practice we know we want to keep the object dependencies in our projects to a minimum so changes are easy and we can reuse the code we've worked so hard on. To accomplish this we will follow a general principle of "programming to the interface, not the class" using the MVC pattern.
Our mission, if we choose to accept it...
We have been commissioned to build a ACME 2000 Sports Car business object and our task is to create a simple windows interface to (1) display the vehicle's current direction and speed, and (2) enable the end user to change direction, accelerate, and decelerate. And of course, there will be scope creep.
There are already rumors at ACME that if our project is successful, we will eventually a need to develop a similar interface for the ACME 2 Pickup Truck and the ACME 1 Tricycle. As developers, we also know that the ACME management team will eventually say "Hey, this is really cool. Can we see it on the company's intranet?" All of this in mind, we want to deliver a product that will be easily scalable so we can be sure to have food on our plates for some time.
So, coincidentally, we think... "This is a perfect situation to use the MVC!!".
Our Architecture Overview
Ok, now we know that we want to use the MVC, we need to figure out what the heck it is. Through our research we come up with the three parts of the MVC: The Model, Control, and View. In our system, the Model would be our car, the View would be the user interface, and the Control is what ties the two together.
To make any changes to the Model (our ACME 2000 sports car), we'll be using our Control. Our Control will make the request to the Model (our ACME 2000 sports car), and update our View, which is our user interface (UI).
This seems simple enough, but here's the first problem we have to solve: What happens when the end user wants to make a change to our ACME 2000 sports car, such as going faster or turning? They are going to have to do it by requesting a change using the Control, through the View (our windows form).
Now we are left with one last problem to solve. What if the View doesn't have the necessary information to display the current state of the Model? We're going to have to add one more arrow to our diagram: The View will be able to request the Model's state in order to get what it needs to display the information about the state of the Model.
Finally, our end user (our driver) will be interacting with our entire ACME Vehicle Control system through the View. If they want to request a change to the system, such as adding a bit of acceleration, the request will be initiated from the View and handled by the Control.
The Control will then ask the Model to change and make any necessary changes to the View. For example if the ACME 2000 Sports Car has a "floor it" request from an unruly driver and is now traveling to fast to make a turn, the Control will know to disable the ability to turn in the View, thus preventing a catastrophic pileup in the middle of rush-hour (whew!).
The Model (the ACME 2000 Sports Car) will notify the View that it's speed has increased and the View will update where appropriate.
So after all that, here's the overview of what we will be building:
Getting Started: Parts... Parts... Parts...
Being developers who always think ahead, we want to be sure our system will have a long and prosperous life. This means being prepared for as many changes at ACME as possible. In order to do this we know to follow two golden rules... "keep your classes loosely coupled" and, in order to accomplish this... "program to the interface".
So we will make three interfaces (as you may have guessed, one for the Model, one for the View, and one for the Control).
After much research and laborious interviews with the folks at ACME, we find out more about the system specifications. We want to be sure that we can set the maximum speeds for traveling forward, backwards ,and turning. We also need to be able to speed up, slow down, and turn left and right. Our "dashboard" view must display the current speed and direction.
It's a tall order to implement all of these requirements, but we're sure we can handle it...
First, let's take care of some preliminary items. We'll need something to represent the direction and turn requests. We'll create two enumerables, AbsoluteDirection and RelativeDirection.
public enum AbsoluteDirection
{
North=0, East, South, West
}
public enum RelativeDirection
{
Right, Left, Back
}
{
North=0, East, South, West
}
public enum RelativeDirection
{
Right, Left, Back
}
public interface IVehicleControl
{
void Accelerate(int paramAmount);
void Decelerate(int paramAmount);
void Turn(RelativeDirection paramDirection);
}
{
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);
}
And Finally, we'll put together the View interface. We know the view should expose some functionality to the Control, such as enabling and disabling acceleration, deceleration, and turn requests.
public class IVehicleView
{
void DisableAcceleration();
void EnableAcceleration();
void DisableDeceleration();
void EnableDeceleration();
void DisableTurning();
void EnableTurning();
}
Now we have to make a few tweaks to our interfaces to allow them to interact. First of all, any Control should be aware of it's View and Model, so we'll add "SetModel" and "SetView" methods to our IvehicleControl interface:
public interface IVehicleControl
{
void RequestAccelerate(int paramAmount);
void RequestDecelerate(int paramAmount);
void RequestTurn(RelativeDirection paramDirection);
void SetModel(IVehicleModel paramAuto);
void SetView(IVehicleView paramView);
}
The next part is a bit tricky. We want the View to be aware of changes in the Model. To do this we'll use a GOF design pattern "Observer".
To implement the Observer pattern, we need to add the following methods to the Model (which will be "observed" by the View): AddObserver, RemoveObserver, and NotifyObservers.
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();
}
...and add the following method to the View (which will be "observing" the Model). What will happen is the Model will have a reference to the View. When the Model changes, it will call the NotifyObservers() method and pass a reference to itself and notify the View of a change by calling the Update() method of the View. (It will become clear as mud when we wire everything up later).
public class IVehicleView
{
void DisableAcceleration();
void EnableAcceleration();
void DisableDeceleration();
void EnableDeceleration();
void DisableTurning();
void EnableTurning();
void Update(IVehicleModel paramModel);
}
So now we have our interfaces put together. We are only going to use references to these interfaces in the rest of our code to ensure we have loose coupling (which we know is a good thing). Any user interface that shows the state of a Vehicle will implement IVehicleView, all of our ACME automobiles will implement IVehicleModel, and we'll make controls for our ACME automobiles with ACME vehicle controls which will implement IVehicleControl.
Next... What things will have things in common?
We know all our vehicles should act the same, so we're going to create a common code base "skeleton" to handle their operation. This is going to be an abstract class because we don't want anyone driving around a skeleton (you can't make an instance of an abstract class). We'll call it Automobile. We'll use an ArrayList (from System.Collections) to keep track of all the interested Views (remember the Observer pattern?). We could have used a plain old array of IVehicleView references, but we're all getting fatigued at this point and want to get through this article. If you're interested, check out the implementation of the AddObserver, RemoveObserver, and NotifyObservers methods to get an idea of how the Observer pattern works by helping our IVehicleModel interact with the IVehicleView. Every time there is a change in speed or direction, the Automobile notifies all IVehicleViews
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}
Last but not least...
Now that our "ACME Framework" is in place, we just have to set up our concrete classes and our interface. Let's take care of the last two class first which will be our Control and our Model...
Here's our concrete AutomobileControl which implements the IVehicleControl interface. Our AutomobileControl will also set the View depending on the state of the Model (check out the SetView method which is called every time there is a request passed to the Model).
Notice, we just have references to the IVehicleModel (not the Automobile abstract class) to keep things loose and IVehicleView (not a specific View).
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 Memberspublic 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();
}
}#endregionpublic 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();
}
}
}
Here's our ACME200SportsCar class (which extends the Automobile abstract class which implements the IVehicleModel interface):
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){}
}
And now for our View...
Now we have to create the last of our three ACME MVC components... the View!
We'll create a AutoView user control and have it implement the IVehicleView interface. The AutoView component will have references to our control and model interfaces:
public class AutoView : System.Windows.Forms.UserControl, IVehicleView
{
private IVehicleControl Control = new ACME.AutomobileControl();
private IVehicleModel Model = new ACME.ACME2000SportsCar("Speedy");
}
We also need to wire everything up in the constructor for the 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);
}
Next, we'll add our buttons, a label to display the status of the ACME2000 Sports Car and a status bar just for kicks and fill out the code for all the buttons.
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);
}
Add a method to update the interface...
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;
}
Finally, we'll wire up the IVehicleView interface methods...
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);
}
And we're off!!!
Now we can go for a test drive in the ACME2000 Sports Car. Everything is going as planned and then we run into an ACME executive who wants to drive a pickup truck instead of a sports car.
Good thing we used the MVC! All we need to do is create a new ACMETruck class, wire it up, and we're in business!
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){}
}
in the AutoView, we just have to build the truck and wire it up!
private void btnBuildNew_Click(object sender, System.EventArgs e)
{this.autoView1.WireUp(new ACME.AutomobileControl(), new ACME.ACME2000Truck(this.txtName.Text));
}
If we wanted a new Control that only allowed us to increase or decrease the speed by a maximum of 5mph, it's a snap! Create a SlowPokeControl (same as our AutoControl, but with limits on how much a Model will be requested to accelerate)
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();
}
}
If we want to make our ACME2000 Truck a SlowPoke, we just wire it up in the AutoView!
private void btnBuildNew_Click(object sender, System.EventArgs e)
{this.autoView1.WireUp(new ACME.SlowPokeControl(), new ACME.ACME2000Truck(this.txtName.Text));
}
And finally, if we wanted a web-enabled interface, all we have to do is create a web project and on the UserControl implement the IVehicleView interface!
In Conclusion...
As you can see, using the MVC to help build code to control interfaces that is very loosely coupled makes life much easier when it comes to change requests. It also makes the impact of changes negligible and you can reuse your interfaces and abstract classes almost anywhere. There are a couple of places where we can build some more flexibility in our project, especially in terms of requesting changes to our Model's state, but that will have to wait for next time.
In the meanwhile, keep the MVC in mind for your next project... You won't regret it.
No comments:
Post a Comment