UML软件工程组织

MVC模式在j2me项目中的应用
文章出处:www.j2medev.com 作者:FavoYang

内容提要:
 本文简要的介绍了MVC模式的思想,并分析了MVC模式的利弊,最后结合MIDP平台给出几种常见的MVC模式实践。相信此文对任何一个使用midp平台的商务程序开发者都或多或少的有所帮助。

 版权声明:
 本文同时发表在www.j2medev.com和我的Blog(blog.csdn.net/alikeboy)上,如果需要转载,有三个途径:1)联系我并经我同意;2)和www.j2medev.com有转载文章合作协议的 3)通过Rss聚合我的Blog。另外网上转载需要全文转发(包括文章的头部的声明),不要断章取义。

 正文:

 初识MVC模式

 第一次认识到MVC模式是从Microsoft MFC框架所采用的“文档-视图”模型开始的。第一次接触到这个概念让我兴奋不已,很长时间困扰我的程序框架问题似乎迎刃而解了。而后我翻阅了GOF一书中对MVC模式的描述,增进了对这个模式的一些理解。应该说MVC框架是程序设计领域的常青树,也是GOF模式中最为重要的模式之一。这一经典的模式被广泛的使用,有太多的程序构架在这一框架之下,从早期的卓面Application到现在流行的Web。并因各自的需求不同,MVC有了很多的变种。了解MVC是每个程序设计人员的必修课,最好能够达到熟练运用的程度。

 我并不打算详细介绍这一模式,因为细节比较复杂,我口舌拙笨也不容易说清楚,大家应该参阅一下有关模式的书籍,任何一本都比我要讲的清楚。所以此处就一带而过。MVC模式是Model-View-Controller的缩写,中文译为“模型-视图-控制器”。MVC的核心思想是分离。Model就是对实体类的抽象;View就是Model在屏幕上的表示;Controller就是协调者。可能有朋友发现Controller的描述多少有些含糊,不要着急,这个一会还要谈到。大概因为太过有名,MVC模式的每个实现都出处很大,但他们却都叫做MVC!!搞得初学者一头雾水。往往滥用,最后搞得M.V.C.三者之间的协调很混乱。这其实并不是他们的错,理清思路的关键还是刚刚提到的一个词“分离”。尽管MVC实现不同,但是思想是一致的。

 MVC模式的利与弊
 先谈优点:
 1)将M.V.C.分离可以让不同的专家负责不同的模块,一般情况下,M部分由熟悉数据库,网络传输的专家来负责;V则交给对UI有研究的专家。这对于项目的管理者而言是多么的诱人,分工意味着可以提高效率并可以按照传统的责任划分来处理软件开发过程。对开发者而言也可以专心于一个领域。这样做的前提是接口要明确,MVC的分离思想正为其提供了基础。

 2)一旦V的部分发生变化,可以迅速的重构而不必引起整个工程的返工。如今的软件表现层的部分变化实在是太快了…

 3)M的部分,因为足够抽象,可以方便的重复利用,符合OO的思想。另一方面我们可以利用JUnit等单元测试工具对M进行测试,保证工程质量。

 谈完了优点再来看看缺点:
 1)利用MVC模式(也包括近代的其他一些模式)暗示我们通过多产生一些类,来提高程序的可读性与健壮性。附带来的缺点就是类的数量的膨胀。说句笑话,MVC就好像是发面时用的速效粉一样,是最为方便的代码膨胀剂,相信大家都深有体会:)

 2)MVC虽然定义了M.V.C.个个部件的含义,但并不具体,而且没有非常明确的固定三者之间的联系。所以一直以来除了View没有争论外,其他方面都有很多争论,大家都想把自己的理解作为正解。尤其是“Model到底是屏幕数据的集合还是实体数据”、“控制器的作用”是两个经常争论的问题。前面提过MVC变种很多,这也给初学者留下了不少的陷阱。后面结合实例将会分析几种常见的做法。

 3)MVC的实现成本偏高。但请注意是这是相对的,一般而言项目越大,越可以看出其优势。


 常见的MVC模式实践
 下面将会介绍在midp平台几种常见的实践,最后是我习惯的做法

 M—V形式(或者MC—V、M—VC)

 这也是在j2me中一种惯用的方法,精炼的说这种方法是以屏幕为组织单位的,因而很适合RAD工具的开发思路。一个屏幕及其控制被抽象成一个VC类,而这个类中有一个私有的Model对象来代表屏幕上要用到的数据元素。屏幕对象并不保存任何的实体数据,这些数据被组织在了Model对象中。大概因为屏幕对象很直观,控制器的作用也不明晰(它绝大部分的功能被view或是model取代,具体取决于你的实现),所以也常常称呼为model-view模式。形式如下:

class MyFrame extend Frame{
private Model model;
private StringItem name;
MyFrame(Model model){
this.model=model;
name=new StringItem(model.getName());//请求模型的数据
append(name);
}
}

class Model{
private String name="M-C pattern";
public String getName(){//这是一个服务接口
return name;
}
}

上面看到的是个典型的M—V模型,我们可以理解这种以屏幕为核心的分离的含义。Model组织起屏幕的数据,view向Model索要其希望显示的数据,注意这一操作一定要通过预先协商好的接口访问,而不是直接操作。如果出现复杂的事务逻辑(用户选择的某种操作),有人将其放在Model端,也有人放在View端,但一般上放在Model端,这时Model带有严重的Controller的色彩。

 这种形式的优点是非常的直观,也有限的分离了显示和数据。如果常看j2medev.com站长Mingjava的文章,可以看到大部分他写的例子都是这种模式。并且这种模式也常常用于RAD工具。

 这种模式的缺点是它与RAD工具一样鼓励你从屏幕开始思考问题,这往往让你陷入RAD的陷阱——不先考虑事务的流程,而是从用户接口直接下手去分析问题,这往往扼杀了你的全局构思。

 Sun blueprints: Smart Ticket中使用的MVC模式

 著名的蓝图程序Smart Ticket中使用了MVC模式,并且这一模式帮助Sun的程序员在MIDP2发布时,快速的将Smart Ticket的view部分从MIDP1.0 更新到MIDP2.0。

 Sun针对MIDP的特点,设计并改进了这一模式,在SUN的解决方法中是一个很标准的方法,只是 Controller变成了一个巨大的事务处理器,所有由UI对象收集到的用户的需求都转发给Controller处理。Controller内部保存了一组常量。在一个dispose(int id)形式的方法里一个巨大的switch case语句根据比较不同的常量,处理不同的请求。这种技术有时也将Controller称为处理器,或者屏幕导航器。这种模式的提出者主要是要集中处理j2me里频繁的画面导航。

 很多人都觉得,在j2me中将Controller改造成巨大的事务处理器是一个很好的方法。我对此持保留意见。

 iFeedback 中简化的MVC

 为了大大减少类的数量,iFeedback的作者,将MVC封装到一个类中,用不同的方法来代表对这三者的分离,这种举动证明对减少类的数量又很大帮助。

public abstract class MVCComponent implements CommandListener {
// Set from outside at beginning
public static Display display;

// Returns the screen object from the derived class
public abstract Displayable getScreen();

public Displayable prepareScreen () throws Exception {
if ( getScreen() == null ) {
initModel();
createView();
} else {
updateView();
}
getScreen().setCommandListener ( (CommandListener) this );
return getScreen ();
}

public void showScreen() {
try {
display.setCurrent( prepareScreen() );
} catch (Exception e) {
e.printStackTrace();
Alert a = new Alert("Error in showing screen");
a.setTimeout(Alert.FOREVER);
display.setCurrent(a);
}
}

// Initialize. If a data member is not backed by RMS, make sure
// it is uninitilzed (null) before you put in values.
protected abstract void initModel () throws Exception;

protected abstract void createView () throws Exception;

protected abstract void updateView () throws Exception;

public abstract void commandAction(Command c, Displayable s);

}

因为都在一个类里面,你在也不必被MVC三者之间的关系操心了,这种退化的做法,是对MIDP有限资源的妥协。

 我的习惯做法

 下面结合我对MVC的理解和大家交流一下。我使用的是一种UML标准的做法,最大程度上对的体现分离的思想。首先和大家交流一下词汇表:

 View代表屏幕。
 View通过预先商定好的接口向Controller索要数据,View同时收集用户的输入,View并不处理这些输入,而是根据不同的输入回调  Controller不同的方法。通常View的子类使用UI后缀。

 Controller 控制器
 提供View调用的接口,负责和model交流。控制器和View共同担负起和用户交流的作用。

 Model 泛指一系列的实体对象
 需要注意的是我理解的Model并不是屏幕数据的组织单位。Model代表一系列的实体对象。由Controller跟Model交流。我觉得RAD工具中常常将Model代表屏幕数据的集合正式导致MVC概念混乱的一个原因。RAD工具中Model,大体相当于这里的Controller所起的作用。

控制器并不总是联系着Model,有时只是依赖关系。并且Controller往往通过Model的对应的生命期类来获得Model对象。在这种形式中,层层隔离,View与Controller紧密相连,而Model有很高的独立性,可以很好的重用。

 一般的结合UML设计的过程,对MVC的各个类有相应的命名习惯。
 View 称为Boundary类(边界类) 以UI结尾
 Controller 称为 Controller类(控制类) 以Workflow结尾
 Model 称为Entity类(实体类) 以Entity结尾或者没有尾缀
 Model对应的Lifecycle类(生命周期类) 以Locator结尾

边界类和控制类的基础类如下

BaseView.java
/**
* @author Favo
*
* 视图类
*/
public abstract class BaseView {

public abstract Display getDisplay();

/**
* 简单的返回包装的屏幕对象,不要做任何准备屏幕的操作!
*/
public abstract Displayable getScreen();

/**
* 创建屏幕
*/
protected abstract void createView() throws Exception;

/**
* 更新屏幕
*/
public abstract void updateView() throws Exception;

/**
* 返回控制器
*/
public abstract BaseController getController();

/**
* 准备屏幕
* 返回准备好的屏幕对象
*/
public Displayable prepareScreen() throws Exception {
if(getScreen()==null){
createView();
} else {
updateView();
}
return getScreen();
}

/**
* 显示当前屏幕
*/
public void displayScreen(){
try{
getDisplay().setCurrent(prepareScreen());
} catch (Exception e) {
e.printStackTrace();
Alert al=new Alert("Error",e.toString()+'\n'+e.getMessage(),null,AlertType.ERROR);
al.setTimeout(Alert.FOREVER);
getDisplay().setCurrent(al);
}
}

}

BaseController.java
/**
* @author Favo
*
* 控制类
*/
public abstract class BaseController {
public abstract BaseView getView();
public abstract void setView(BaseView view);
}

注意到这些基础的类并没有向MFC框架那样产生完整的框架,而是设计成了抽象类,一来希望强迫大家实现抽象类(防止出错);二来希望增加一点灵活性。所以两个类之间的通信就要靠大家撰写的子类的构造函数了。一般我的习惯是,初始化好控制器,然后将控制器作为参数传给边界类的构造函数,由边界类的构造函数来回调控制器的setView()来实现的。这些步骤是一定要有的,不然会NULLpointerExcpetion哦。

 尽管理论上可能很清晰,但实践带来的复杂性是惊人的。这正是软件开发的问题,太多的细节困扰这开发者对大局的把握。本文接下来,将结合最后这种设计思想,给出一个完整的设计实例。帮助大家从实践的角度理解运用这一模式。敬请大家期待。

Note项目描述
 手机是一个真正随身携带的数字终端,我们除了利用手机打电话、发信息外,往往让他帮助我们记录文字性的信息。PDA有较大的屏幕和手写输入的功能,而手机上的记事功能有限,记录的内容很简单。可以说利用手机记录信息是一种被视为理所应当的功能,也造就了一个一系列的应用——将手机作为随时随地的信息收集器。不难设想此类应用会用一定的市场,我们今天选择的Note记事本项目,正是此类应用的单机版的原形。

Note是很多手机的内建应用程序,一般叫做记事本或便签,说明这是一个非常常用的服务。也许朋友们可能认为开发一个已有的程序没有挑战性。是的,在开发上最忌讳的就是重作车轮,但对于教学则可两说着。毕竟一个简单的原型程序不会让我们陷入太多的细节,我的主旨是向大家介绍MVC模式的应用方法。

Note为手机用户提供记录一些简短信息的功能,用户可以添加记录,打开浏览记录,并可以随时修改已经保存在手机上的记录,当然也可以删除它们。就好象Windows下的记事本一样,只不过多了管理的功能。

用例分析
 首先设想一下谁在使用这个程序:手机用户。好,我们以后就称这个参与者为用户(user)。

然后设想一下,用户都利用我们的NOTE(中文我们叫做记录好了)干些什么呢?很显然,用户可以添加新的记录,浏览他添加的记录,修改他所添加的记录,并且他还可以删除记录。一条记录应该简单的包括用户对记录起的名字,记录的创建或修改时间,以及最重要的记录的内容。

一般的情况开发人员是很反感这种文字性的描述的,往往是因为开发人员习惯于对待硬梆梆的PC机,而不愿意去面对客户,收集这种需求。其实文字性的东西,既是一个对系统的概述,又是我们发现开发要素的土壤。试想如果你的软件要发布了,你却无法组织起语言让用户恰当的理解软件的功能与使用对象,是多么的让人烦恼。

精练用户的需求(其实是我的教学需求哈哈)。很显然添加记录与修改记录同属于对记录进行编辑操作,就叫做编辑记录用例(NoteEdit)好了。浏览记录也是一个很明显的用例,就叫做浏览用例(Notepad)。删除是对记录进行的一种管理,叫做管理记录用例(NoteManager)。到此,我们已发现并精练了三个主要用例,还不错,系统正一步步变的清晰。在这里提醒大家,这个阶段是站在客户的观点(这里是用户的观点)想问题的,你的工作是发现并系统化客户的想法,不必站在开发者的角度思考任何细节。

编辑记录用例(NoteEdit)

事件流1:

1) 显示用户Note的内容

2) 用户编辑内容

3) 用户放弃修改,note内容不变,正常退出

事件流2:

1) 显示用户Note的内容

2) 用户编辑内容

3) 用户save,退回主菜单

事件流3:

1) 显示用户Note的内容

2) 用户编辑内容

3) 用户save As,提示让用户输入新的文件名

4) Save,退回主菜单

浏览用例(Notepad)

事件流1:

1) 显示用户的Note的标题、创建时间、内容

2) 用户选择退出,返回主菜单

事件流2:

1) 显示用户的Note的标题、创建时间、内容

2) 用户显示编辑,转向编辑用例

管理记录用例(NoteManager)

事件流1:

1) 显示用户的Note列表

2) 用户打开选择的Note,转向浏览用例

事件流2:

1) 显示用户的Note列表

2) 用户编辑选择的Note,转向编辑用例

事件流3:

1) 显示用户的Note列表

2) 用户新建一个Note,转向编辑用例

事件流4:

1) 显示用户的Note列表

2) 用户删除选择的Note

3) 出现确认提示

4) 用户确认,删除Note

5) 更新显示,回到Note列表

三个用例的事件流一经被分析出来了,很显然应该在第一次迭代全部完成。
 
 寻找类(oo分析)
 首先是实体类(Entity),只需要从事件流中提取名字就可以缩小范围。

Note,显然是个对象。

内容(content)、时间(datetime)、标题(title),恩,应该是Note的元素。

Note的方法包括对域成员的操作set/get。因为要保存,所以需要序列化反序列化方法。

一般实体类都是由一个对应的生命周期类(lifecycle)用于他的产生、存储、消亡等等操作,一般把这样的操作独立出来大大有利用实体类的重用。不过此阶段还用不着分析他,一会儿画顺序图时,自然就会发现它。

习惯上为了高效的画顺序图,边界类和控制类的方法都需一一列出。不过我们省了,大家只知道每个用例都对应着一个边界类就好了。

设计实践
 我举三个用例中管理记录用例(NoteManager)的一部分和浏览用例(Notepad)的一部分来介绍详细的设计过程。在这里,我们试图从一个客户的角度转化到一个开发者角度。要面对很多的挑战,可能包括一部分细节。应该学习从分离的角度思考整个系统。MVC的精华就在这里。

记录用例(NoteManager)事件流1:

NoteManagerUI并不知道Note列表的具体组织形式,它通过预先商定好的接口getNoteTitleList向控制类NoteManagerWorkflow所要数据,控制类返回一个String[]数组。

同样,NoteManagerWorkflow需要向生命周期类NoteLocator所要数据,不过NoteManagerWorkflow知道数据的细节。为了能够识别数据,除了返回记录的Title这一信息外,还要同时返回一个唯一识别的ID作为整个系统内识别Note的方法。所以NoteManagerWorkflow就有了两个域一个是TitleList、一个是IdList。

这里有几个细节:

1) NoteManagerUI、NoteManagerWorkflow如何通信,这不成问题,我们有理由相信他们是紧密相关的。

2) NoteManagerWorkflow如何找到NoteLocator,一般情况下,NoteLoator都是单件Singlton。

3) 当NoteManagerUI的showNote(index),调用的时候,他会调用NoteManager的showNoteDispose(index),而showNoteDispose会根据内部的实现,将这一Index转化为id用于识别Note

浏览用例(Notepad)事件流1:

NotepadUI向控制类所要标题,控制类有域note、和noteid,但是控制类通过getNode,这里很明显的可以使用惰性初始化技术,向生命周期类所要Note。取得记录对象的引用后,你可以方便的像实体类请求数据了。

经验分享
当然了,打好骨架后你就可以开始时coding了,画图的好处是强迫你在设计阶段做好各个部件之间的接口设计。这可以有效地减少你返工的几率,但是往往我们在设计阶段过多的思考了细节,比如NoteLocator是如何和Rms交流的等等。这都是很不好的习惯,但是不太容易改正。因为无论是学校里,还是陪训等等都是训练,反复的训练我们对coding的敏感。我们太依赖于从代码的角度思考问题了,这阻碍了我们从大局思考问题,发现更通用的模式。

如果要开始coding了,也不要一开始就全面铺开,一般实体类具有很强的独立性。可以独立开发,而开发其它类的时候可以从边界类开始,如果想一边开发一边测试一下,大可把控制类、生命周期类的方法暂时用fade data(伪支撑数据),这都是大大降低复杂性的好办法。

有条件的话请进行单元测试,不然测试效率实在是低。(Ps,有人写篇j2me下使用单元测试的文章好吗,我好想让测试自动化)

惰性初始化是我使用的最为频繁的技术,我觉得它可以大大降低代码混乱的程度。

整个系统的UML

有人说看不懂UML,但如果给我这么大的代码,我肯定看不懂。UML吗,有可能看懂:)

屏幕快照
 开始后的画面:(管理用例)

新建后的画面:(编辑用例)

 

按下save,输入title名字

更新显示:

选择你喜欢的浏览,比如标题是easy的记录:(浏览用例)

屏幕导航:

代码种种

有朋友不愿意公开自己的代码,但是对于java来说,反编译太容易了。你没有什么秘密可言。相对于设计来说,代码是个很细碎的东西。如果有人愿意为你修改代码,共同完善的话,那是打着灯笼也找不到的呀,我用eclipse开发,就是在使用开放源码的结晶呀。当然这是对文章的附加代码而言。真正的项目如果开源一定要在GPL下进行。谁也不想再出现divx那样的事了。转载的朋友,再说一遍免费不但等于 not copyright。


版权所有:UML软件工程组织