现在开发应用程序经常使用一些所见即所得的开发环境,使得用户界面的制作非常方便。然而,用户界面是最轻易发生需求变更的部分,用户界面发生变化,经常对业务模块产生影响。并且,用户界面是不利于自动测试的。一旦某些代码依靠用户界面,这样的代码就很难在别的模块中调用了,因此业务逻辑不能在界面层次中进行,否则会造成不能复用,不能复用自然会增加复制粘贴的代码,造成错误的扩散,放大需求变更的影响。在程序设计中,应该尽量做到用户界面和底层的业务模型分离。
用户界面和业务模块的互动方式,在程序设计中经常采用MVC模式。MVC模式并不是一个非凡的模式,而是一些特定模式的组合。基本上包括三个对象:业务模块(Model)、用户界面(View)和控制器(Controller),关系如下:
图中实线表示高耦合的依靠关系,虚线表示低耦合的消息关系。业务模块是不依靠用户界面的,这样就隔离了用户界面的变更对业务程序的影响。用户界面负责收集用户的输入,显示用户需要的数据;控制器负责将用户的请求调用到实际的业务程序,也将业务程序处理的结果回送给用户界面;业务程序具体处理业务操作。同时业务模块可能主动发送消息到用户界面,通知界面显示数据。
在具体的环境下,这些因素可能发生一些变化。比如,在web开发中,由于web应用程序的性质,用户界面是在浏览器上运行的,而界面的控制和业务模块在浏览器上运行,所以在web应用中通常采用这种典型的MVC模式。并且在Web应用中,不存在服务器主动向客户端“推”数据,因此从Model到View之间的虚线也是不存在的。在windows窗体程序中,控制器和界面经常是合并在一起的,比如MFC框架中使用的Document-View模式,其中的Document对应MVC中的Model,负责保存业务数据,处理业务逻辑,View相当于MVC中的View+Controller,负责用户界面的显示、用户输入的收集和画面的跳转控制。
好的设计和坏的设计有时候需要写的代码是一样多的,但是这些代码放的位置不一样。MVC中最重要的一点就是清楚Controller应该处于什么样的地位,应该完成什么样的功能。下面用一个web应用程序的例子来说明一下。
jsp编程有一些MVC的框架,比如Struts,Struts控制器的工作如下:首先是一个请求分派机制,负责监听请求和分配请求,然后是一个Command模式的实现,负责处理请求。首先收到服务器收到客户端的http请求,交给控制器分析其中的地址,在一个配置文件中寻找对应的处理者(一个Action的子类),建立这个类的实例,随后执行其execute方法,Action类中调用业务模块进行实际业务的处理(在处理之前进行必要的预备,比如分析请求的参数,将其转化为业务模型了解的对象),得到处理结果,根据处理的结果决定需要显示的View。这个需要显示的View在Struts框架中也是在文件中配置的。
这是一种集中式的控制器,应用程序使用一个统一的Controller。不仅使业务和界面分离开,并且界面的流程完全由同一个对象来控制。最重要的是,使得功能的修改和追加变得比较方便,控制器成为业务模块的缓冲,减轻了需求变化对业务模块的影响。
很多windows窗体程序也采用这样的控制器。有一个开放源码的.Net开发工具,叫做SharpDev,本身也是用c#开发的,采用的就是这样的集中控制方式。SharpDev是用add-in的方式进行增量开发的,程序中的功能,如打开文件、保存文件、运行某个向导等功能都是一个个独立的add-in,使用了Command模式。程序运行过程大致如下:应用程序初始化的时候,读取配置文件中所有名称为*.add-in的文件,得到程序中所有的add-in,可以把这些add-in看作一个ICommand接口的实现。根据配置文件建立这些ICommand的实例,绑定在对应的菜单项和工具栏按钮上。当用户点击这些菜单项和工具栏按钮的时候,由一个任务分派的对象将请求定位到一个Command上,执行其Run方法。Command执行的时候可能要调用业务程序,业务程序是通过一系列的Service对外提供功能的,不直接向外界暴露。Controller就是负责定向用户操作到具体Command的分派器。
窗体应用程序还有一个特点:有时候业务改变的时候,需要用户界面作出相应的变化。比如:当代码编辑器中的文字发生变更的时候,工具栏上的保存按钮要置为可用状态,当保存后,保存按钮又要置为灰色。这样的功能是通过一个Observeor模式来实现的,这就避免了业务模块对用户界面的依靠,并且这样的模式也便于同时将消息发送给多个对象,比如保存按钮不仅要在工具栏上出现,也要在菜单上出现,这样的变化是不会影响业务模块的。在SharpDev中,这个交互的过程也是在业务模块对外提供的Service中通过delegate来实现的。
很多应用程序采用的是另一种控制模式:每个画面和窗口使用自己的控制器。在窗体程序中,这样的方式实际上就将用户界面和控制器融合在一起了,比如MFC中的Document-View,View不仅实现用户数据的展示和输入数据的收集,还要将用户的输入进行基本的处理,转变为业务模块了解的类型,调用业务模块进行处理,最后跳转到别的窗口。
在asp.net中使用code behide的编程框架,实际上也是为每一个用户界面采用了一个独立的Controller,ASPx文件就是用户界面,对应的code behide代码就是他的控制器。这样的框架减少了程序的灵活性,但是在一般情况下可以使应用程序的框架变得简单和直接。