AsWing入门教程 3.1 模型-视图-控制器设计模式
作者:胡矿
著作权所有,请勿转载
Google Doc
http://docs.google.com/Doc?id=dnp8gdz_33f8dk27
模型-视图-控制器设计模式又被称为MVC。这个模式是将数据模型与显示界面分离,通过一个控制器来分别与数据模型和界面沟通和进行控制。 如图3.1-1.
由于ActionScript3.0是事件驱动的强交互性语言,所以模型-视图-控制器设计模式在ActionScript3.0当中也会与在其他语言中的定义有所不同。
-
数据模型是一个事件源(EventDispatcher)
-
视图是一个事件源(EventDispatcher)
-
控制器里面你有两个通道
1.一个通道监听视图的变化,并且把变化写入到模型
2.一个通道监听模型的变化,并且把变化输出到视图
如图3.2-2。
下面是这个模式的代码表述:
Model
package
{
import flash.events.EventDispatcher;public class Model extends EventDispatcher
{
}
}
Viewpackage
{
import flash.events.EventDispatcher;public class View extends EventDispatcher
{
}
}
Controllerpackage
{
public class Controller
{
private var v2mChannel:Channel;
private var m2vChannel:Channel;
public function Controller(view:View, model:Model) {
this.v2mChannel = new ViewToModelChannel(view, model);
this.m2vChannel = new ModelToViewChannel(view, model);
}
}
}class Channel {
protected var view:View;
protected var model:Model;
public function Channel(view:View, model:Model) {
this.view = view;
this.model = model;
}
}class ViewToModelChannel extends Channel {
public function ViewToModelChannel(view:View, model:Model) {
super(view, model);
//监听来自视图的事件
this.view.addEventListener(……..
}
}class ModelToViewChannel extends Channel {
public function ModelToViewChannel(view:View, model:Model) {
super(view, model);
//监听来自模型的事件
this.model.addEventListener(…….
}
}
下面,我们用一个简单的例子来介绍这个设计模式如何使用。在后面的章节当中,我们会大量使用这里所介绍的模型-视图-控制器设计模式。
如图3.2-5和3.2-6,这个程序有一个输出框架和一个输入框架。在输入框架增加或者减少模型的顶点,输出框架根据模型当中的顶点数绘制正多边形。
(图3.2-5)
(图3.2-6)
在这个例子当中,模型定义了一个vectorChanged事件(VECTOR_CHANGED)和一个正整数(vectors)。定义了两个公开的方法来操纵这个正整数(getVectors、setVectors)。如图3.2-7。
(图3.2-7)
模型对于外面发生的事情一无所知,它只知道在vectors改变的时候发布vectorChanged事件(VECTOR_CHANGED)。
笔记:
一个对象是由数据和方法所构成的,数据定义的是对象的状态,所以有的时候,数据又被叫做属性,比如例子当中的Model,数据域vectors表示这个Model目前是处在3个顶点的状态,或者是4个顶点的状态。
我们要强调的是对象当中的方法。对象当中的方法是对本对象自身的数据的操作。所以方法(Method)有时候又被叫做操作(Operation)。
记住这句话:数据(属性)是对象的状态,方法是对数据(属性)的操作。
笔记:
面向对象的一个基本概念是服务(Service)。
当一个对象要访问另一个对象的时候(即改变或者读取另一个对象的状态),它只能通过那个对象所提供的服务来进行操作,在对象这个层面,服务就是方法(Method)。而一个对象要让别的对象可以操作自己的状态,就要定义操作自己数据的方法。
注意:一个对象的方法只应该操作自己的数据,而不应该去操作其他对象的对象的数据。要操作别的对象,应该通过别的对象所提供的服务(Method)
视图是一个有两个框架(JFrame)的界面,它只提供了一个公开的方法update,这个方法接受一个正整数作为参数,并且将根据这个正整数来绘制一个正多边形。如图3.2-8。
(图3.2-8)
视图对于模型同样一无所知。它只知道代表顶点数目的一个正整数,而对这个数字从何而来,代表什么含义一无所知。在“添加顶点”(Add Vector)和“移除顶点”(Remove Vector)按钮被按下的时候,视图将发布addVector事件(ADD_VECTOR)和removeVector事件(REMOVE_VECTOR)。
控制器负责间监听分别来自模型和视图的事件。如图3.2-9。
(图3.2-9)
控制器有两个通道(Channel),一个负责监听和处理来自模型的事件(ModelToViewChannel),另一个负责监听和处理来自视图的事件(ViewToModelChannel)。控制器对于视图将如何显示以及模型将如何保存数据一无所知,它只知道在收到特定的事件的时候对数据进行逻辑处理,并且在需要的时候通过模型或者视图所提供的方法改变他们的状态。
我们看到,模型-视图-控制器设计模式当中,每一个层面都只负责自己层面的处理逻辑,对于其他层面是如何进行处理的一无所知。不同层面之间只是通过一些特定的接口(Method)进行通信。并且在这个设计模式当中视图和模型是被隔开的,他们之间不能直接访问,而是要通过一个控制器来来处理模型和视图之间的逻辑。这样的好处在于
- 当我们需要对某一层进行修改的时候,只要保证接口的定义不发生变化,这个修改就不会影响到其他的层面。
- 界面显示部分的逻辑、数据的处理逻辑、数据的存储逻辑被彻底分离。每一个层面都只关注自己层面的逻辑而不用关心别的层面具体是如何进行处理的。
注意!
这里提到的接口是广义的接口,而不是指用关键字interface所定义的接口类。
接口是连接两个模块的规范(在面向对象当中,最基本的模块就是对象)。在对象这个层面,接口就是一个方法定义,这个定义除了方法的返回类型和参数以外,还包括方法的前置条件(Pre-condition)和后置条件(Post-condition)。
接口方法:提供给其它对象使用,用来操纵自身对象的方法。
前置条件:要让这个方法得到正确的结果,需要具备一些什么样的先决条件。(比如参数不能为null)
后置条件:这个方法结束之后将会有怎样的后果。
前置条件和后置条件并不是ActionScript3.0用语法约束硬性规定的,它可以是一种口头约定,或者是被写在方法的注释当中或者文档当中的描述性文本。并不是所有的方法都需要注明前置条件和后置条件,但是在一些关键的接口方法的定义当中,应该写明前置条件和后置条件。
关键的接口方法的前置条件和后置条件往往被忽略,这是造成将来维护困难的一个重要原因。
例3.2-1是完整的程序代码。
例3.2-1
Model
package
{
import flash.events.EventDispatcher;
import flash.events.Event;[Event (name=”vectorChanged”, type=”flash.events.Event”)]
public class Model extends EventDispatcher
{
public static const VECTOR_CHANGED:String = “vector changed”;
private var vectors:uint = 1; //默认是1个顶点
/**
* Post-condition:返回vector的数目
*/
public function getVectors() :uint {
return this.vectors;
}
/**
* pre-condition: n 不能为负数
*
* post-condition: vector 的数目将会被设为 n
*/
public function setVectors(n:uint) :void {
this.vectors = Math.max(n, 1); //确保至少有一个顶点
//发布更新事件
var event:Event = new Event(Model.VECTOR_CHANGED);
this.dispatchEvent(event);
}
}
}
View
package
{
import flash.display.DisplayObjectContainer;
import flash.display.Shape;
import flash.events.Event;
import flash.events.EventDispatcher;
import flash.events.MouseEvent;
import org.aswing.ASColor;
import org.aswing.AsWingManager;
import org.aswing.EmptyLayout;
import org.aswing.FlowLayout;
import org.aswing.JButton;
import org.aswing.JFrame;
import org.aswing.JPanel;
import org.aswing.geom.IntPoint;
import org.aswing.graphics.Graphics2D;
import org.aswing.graphics.Pen;
[Event (name=”addVector”, type=”flash.events.Event”)]
[Event (name=”removeVector”, type=”flash.events.Event”)]
public class View extends EventDispatcher
{
public static const ADD_VECTOR:String = “add vector”;
public static const REMOVE_VECTOR:String = “remove vector”;
private var owner:DisplayObjectContainer;
private var canvas:JPanel;
private var shape:Shape;
/**
* 构造函数
* pre-condition: owner 对象不能为空,而且必须已经被添加到Stage的显示列表当中
*
* post-condition:将构造视图的界面
*/
public function View(owner:DisplayObjectContainer) {
this.owner = owner;
AsWingManager.initAsStandard(this.owner);
//创建显示多边形的框架
var outputFrame:JFrame = new JFrame(this.owner, “Output”);this.canvas = new JPanel(new EmptyLayout()); //用空布局
this.canvas.setSizeWH(400, 400);this.shape = new Shape();
//将Shape的坐标原点移动到JPanel的中央
shape.x = 200;
shape.y = 200;
this.canvas.addChild(this.shape);outputFrame.getContentPane().append(this.canvas);
outputFrame.pack(); //根据内容的尺寸自动计算框架的尺寸
//创建输入用的框架
var inputFrame:JFrame = new JFrame(this.owner, “Input”);inputFrame.setLocationXY(450, 0);
inputFrame.getContentPane().setLayout(new FlowLayout());var addVectorBtn:JButton = new JButton(”Add a Vector”);
addVectorBtn.addEventListener(MouseEvent.CLICK, this.__onAddVector);
inputFrame.getContentPane().append(addVectorBtn);var removeVectorBtn:JButton = new JButton(”Remove a Vector”);
removeVectorBtn.addEventListener(MouseEvent.CLICK, this.__onRemoveVector);
inputFrame.getContentPane().append(removeVectorBtn);
inputFrame.pack(); //根据内容的尺寸自动计算输入框架的尺寸
outputFrame.show();
inputFrame.show();
}
private function __onAddVector(e:Event) :void {
var event:Event = new Event(View.ADD_VECTOR);
this.dispatchEvent(event);
}
private function __onRemoveVector(e:Event) :void {
var event:Event = new Event(View.REMOVE_VECTOR);
this.dispatchEvent(event);
}
/**
* pre-condition: n 是一个uint类型的正整数,如果传进来的参数是一个负数,那么这个负数经过uint类型
* 转换之后将会变成一个巨大的正数,这将会造成严重的运行错误。
*
* post-condition: 将绘制一个正n边形
*/
public function update(n:uint) :void {
var g:Graphics2D = new Graphics2D(shape.graphics);
var pen:Pen = new Pen(new ASColor(0×666666), 3);
g.clear();
g.drawPolygon(pen, this.calculateVectors(n));
g.endDraw();
}
private function calculateVectors(n:uint) :Array {
var theta:Number = 2*Math.PI/n;
var radius:uint = 150; //半径150像素
var array:Array = new Array(n); //初始化一个长度为n的数组
var point:IntPoint;
var x:int;
var y:int;
var angle:Number;
for (var i:uint=0; i<n; i++) {
angle = i*theta;
angle -= Math.PI; //将角度调整到 [-PI, PI] 区间
x = (radius*Math.cos(angle));
y = (radius*Math.sin(angle));
point = new IntPoint(x, y);
array[i] = point;
}
return array;
}
}
}
Controllerpackage
{
public class Controller
{
private var v2mChannel:Channel;
private var m2vChannel:Channel;
/**
* Pre-condition:view 不为null;model不为null
*
* Post-condition:将开始控制 view 和 model 之间的数据
*/
public function Controller(view:View, model:Model) {
this.v2mChannel = new ViewToModelChannel(view, model);
this.m2vChannel = new ModelToViewChannel(view, model);
}
}
}
//Inner classes
import flash.events.Event;class Channel {
protected var view:View;
protected var model:Model;
public function Channel(view:View, model:Model) {
this.view = view;
this.model = model;
}
}class ViewToModelChannel extends Channel {
public function ViewToModelChannel(view:View, model:Model) {
super(view, model);
//监听来自视图的事件
this.view.addEventListener(View.ADD_VECTOR, this.__onAddVector);
this.view.addEventListener(View.REMOVE_VECTOR, this.__onRevoveVector);
}
private function __onAddVector(e:Event) :void {
this.model.setVectors(this.model.getVectors()+1);
}
private function __onRevoveVector(e:Event) :void {
this.model.setVectors(this.model.getVectors()-1);
}
}class ModelToViewChannel extends Channel {
public function ModelToViewChannel(view:View, model:Model) {
super(view, model);
//监听来自模型的事件
this.model.addEventListener(Model.VECTOR_CHANGED, this.__onVectorChanged);
}
private function __onVectorChanged(e:Event) :void {
this.view.update(this.model.getVectors());
}
}
API:org.aswing.JFrame
-
setLocationXY(x:int, y:int):void
设置框架的位置,x,y参数分别表示框架左上角定点的坐标
-
pack():void
根据所包含组件的默认大小来自动调整框架的尺寸。
API:flash.events.EventDispatcher
-
dispatchEvent(event:Event):Boolean
发布事件。所发布事件的event.target指向发布事件的对象
API:org.aswing.graphics.Graphics2D
-
drawPolygon(pen:Pen, points:Array):void
绘制多边形。多边形是封闭的,第一个顶点和最后一个顶点之间有连线。
API:Math
-
Math.PI
圆周率
-
Math.cos(angle:Number):Number
计算余弦。angle的单位是弧度。angle位于[-PI, PI]区间
-
Math.sin(angle:Number):Number
计算正弦。angle的单位是弧度。angle位于[-PI, PI]区间
-
Math.max(value1:Number, value2:Number):Number
返回两个数当中较大的数
标记:Event
语法:[Event (name=”eventName”, type=”package.eventType“)]
这个标记在MXML当中很有用,对于普通的ActionScript项目来说,这个标记可以在FlexBuilder2(或者FlexBuilder3)当中触发代码提示。
一般来说,如果某一个类要发布事件,应该用Event标记指出它将会发布哪些事件。
-
eventName
其所关联的类当中的属性为 EVENT_NAME,即,在标记当中,事件名称的首字母小写,之后每一个单词的第一个字母大写来区分其中的单词。而其所对应的类当中属性的名称全部用大写,单词之间用下划线分开。
-
type
这个事件的类型,由于例子当中用的就是Event,所以这里写的就是flash.events.Event。当然,它也可以是flash.event.MouseEvent或者是你自己定义的事件类型。