第 1 章:导论。
模式能够:
- 利用一个经过验证可行的解决方案;
- 提供一套通用词汇;
- 约束解决方案的空间。
第 2 章:表现层设计考虑和不佳实践。
客户端验证:基于表单的验证、基于抽象类型的验证。
曾经在 JSP 中滥用过的助手类,通过助手类在页面和业务逻辑之间传递数据,有点类似于如今 Struts 中的 Action 作为传值模型时的情况。
表现层不佳实践:
- 多个视图中都包含控制代码;
- 表现层数据结构暴露给业务层或者业务领域对象,比如:暴露 HTTPServletRequest;
- 重复提交表单;
- 敏感资源暴露给客户端直接访问,有个原则,敏感的东西不能放在 WEB-INF 之外;
- 胖控制器;
……
怎么区分后台视图层和前台页面层?或者说,怎么划分哪些事情 JSP 或者模板做,哪些事情 JavaScript 做?首先,根据模型驱动的原则,通常送到 JSP 或者模板上的都是通用模型的对象或者对象集,JSP 或者模板根据需要选择展示出来,但是后续可抽取为不需和服务端交互状态下响应用户的行为,应当划分为 JavaScript 的工作。
第 3 章:业务层设计考虑和不佳实践:
session bean:根据 EJB 规范,每个 session bean 专门服务于一个客户端或者用户,生命时间等于客户端会话时间;在服务器崩溃后无法存活、无法持久化、会超时、可以涉及事务;支持构造有状态或无状态的对话模型。
不过现在的容器会话大多可以持久化了,会话复制和会话持久化应当是会话管理中重要的两个分支,通常情况下会话不需考虑完整的事务性,保证线程独立性即可。
至于无状态的 session bean,可以被池化,以高效利用(EJB 容器管理)。
entity bean:实体 bean 是否应该包含业务逻辑?按照下面三个原则去判定,还是比较清晰的:
- 这样的业务逻辑是否会引入实体之间的关系?比如处理 UserInfo 的时候,是否引入了 AccountInfo,这样应当考虑根据模型驱动的原则,放置到专门的 User 或者 Account 相关的业务无状态 bean 中去;
- 是否要负责管理用户交互的工作流?
- 是否会担负起本该属于其他业务组件的责任?
有一个 “是”,就说明不该包含这段业务逻辑。
尤其提一句,如果使用远程实体 bean,就更应该减少实体 bean 之间的依赖关系,以提高性能和可用性。
业务层和集成层不佳实践:
对象模型或关系模型或每个用例直接映射成实体 bean:导致粒度过细,EJB 就给网络传输带来太多的负担;
通过 getter、setter 暴露 EJB 所有属性:这也是不好的,提供少量和可控的方法调用,减少远程方法调用的开销;
客户端中包括服务寻址代码:寻址这件事情应当从单纯的客户端抽离出来,把不同的寻址策略和复杂度封装起来,真正做到透明传输(扩展到 without EJB 的系统中也一样,集群环境中也一样,把寻址的行为隐藏于业务逻辑之下)。
EJB 用户长时间持续的事务:会锁住其他 EJB 需要的资源;
……
第 4 章:J2EE 重构:
对业务层隐藏表现细节:对用户请求的处理和通信协议相关的数据不应当被业务层获取,最简单的例子就是 HttpServletRequest 对象。
用 session bean 包装 entity bean:现在这里说的问题一般不会出现,一般也不会有人直接把 Action 对象扔给后面的业务逻辑去处理,原文说的解决办法是引入业务代表,涉及到此的还有两条:减少 entity bean 之间的通信;将业务逻辑移至 session bean。
分离数据访问代码:DAO。
按层重构系统架构(这里也正好归纳一下现在 J2EE 系统中常涉及到的 Action、Service 和 EJB 中的几种 bean 的内在联系),例如:
- 客户端层:浏览器
- 表现层:JSP、模板、业务代表
- 业务层:entity bean(Action)、session bean(各种粒度的 Service)
- 集成层:DAO
- 资源层:DB
……
表现层模式:
拦截过滤器:Intercepting Filter。正如图中的 “Apply zero or more” 和 Servlet 规范所述一样,应当具备一个链式结构。这个链式结构中的每个 filter,互相之间应当是一个互不依赖的松耦合关系,以便于容易地组合。
前端控制器:Front Controller。给表现层请求安排一个集中访问点。集中了控制逻辑,一定程度避免了重复代码。和拦截过滤器的区别:拦截过滤器使用的是松耦合的,结合成链式的处理器逻辑,适合进行强大的预处理、后处理的策略分布;而前端控制器则专注于集中控制,减少视图中的业务和处理逻辑,提高重用度。
在常用的 Struts 网站构架中,N 个拦截器都是可以自由组合的,也可以自定义合适的拦截器栈来继承某个通用的基础拦截器栈,一些通用的拦截逻辑变放置在基础拦截器栈中,这里是一个拦截过滤器和前端控制器结合实现的例子。
Context 对象:不想在与协议无关的环境上下文中使用针对特定协议的系统信息。就是说系统信息,比如请求、配置和安全数据等等,这些东西,通常应当被隐藏起来,不能被业务逻辑看到;但是在某些情形下,业务组件可能又必须用到这些信息,例如,进行终端适配的组件,需要用到 HTTP 报文中的一些 header,那么直接使用会导致组件的灵活性和可重用性的下降,导致前一篇我提到的表现层数据结构和业务层的紧耦合。
解决方法就是制定一个特定的 API,将业务组件需要的部分通过 API 来包装和筛选,而不是直接把表现层数据结构直接暴露给它。这样一来,对于表现层的一些改变,比如协议等方面的改变,不会直接影响到业务组件的接口和运行,只需要修正 API 的实现逻辑。
还有个什么好处?如果我需要测试业务层的逻辑,因为有了这样一层特殊的 API,我可以把整个表现层 mock 掉。
这里有一个应用例子就是 RequestContext 对象,API 只传输这个,而不是具体的某一 Request 对象,对于复杂的请求层面被隔离掉,留下一个包装好的上下文给后面的逻辑。
应用控制器:集中地、模块化地进行操作管理和视图管理。
- 操作管理:把输入请求解析到一个操作(action),让它处理该请求。
- 视图管理:选定返回给客户端的视图,并把请求分派到这个视图。
这两点的应用例子其实就是在 struts-xxx.xml 里面定义的配置,如同一个路标,对于出入视图层的数据进行方向上的导航。
举例来说,它的实现经常采用的策略是 command 对象的策略,命令对象的说法具体可见 GoF 的那本经典设计模式的书。
效果:把操作管理和视图管理分离开了,提高了模块化程度;再一个这个导航的逻辑被抽取成为一处独立的配置单独维护,方便扩展。
视图助手:View Helper。把视图和相关处理逻辑分离开。
这里需要先提及两个重要的阶段:视图准备阶段:这是指请求被分配到一个具体的视图上面;视图创建阶段:视图根据从模型中取得的内容来实例化自己。
因此使用视图封装显示格式的代码,而使用助手封装视图处理逻辑。助手在视图和模型之间充当了一个适配器的角色,同时也会做一些格式逻辑相关的处理。
一个很好的例子就是各种标签,包括自定义标签,比如一个时间格式化的标签,对于一个时间,在不同的环境下以不同的格式展示。
视图助手终究是 “视图” 的助手,它的核心始终是视图,对于已经生成了的成熟的具备一定模型的数据,试图助手协助将它们以某种合适的方式展示出来,而不应当做复杂或具体的业务逻辑。
于是这里提出第一点需要注意,怎样把视图助手和后端的逻辑区分开?视图助手得到的数据应当是已经成熟的一定模型化的数据,需要做的是仅仅是做一些格式的处理,对展示效果的修正和增强,并不做任何业务逻辑的相关事宜。
第二点需要注意,应当把视图助手和 JavaScript 区分开来,前者在服务端完成,后者在客户端完成:把处理逻辑从页面中抽取出来,一个重要原因就是要减少在页面中直接暴露的实现细节。在实际开发中,这二者之间的区分,常常带来困扰。比如在模板或者 JSP 中使用 if 标签,还是在客户端使用 JavaScript 来控制逻辑?我建议这里应当有一个区分的原则:这些逻辑是否属于客户端才能决策的页面展示细节?如果是,就使用 JavaScript 来完成,反之还是应当隐藏到页面助手中。
复合视图:Composite View。使用由多个原子化的子视图构成的复合视图。特点是组合是可以动态的,而页面布局又可以整体控制,和页面内容互相独立。
有这么几个常见的例子:Portlet 就是一个复合视图结合的最好例子,主题可以影响到所有视图的呈现,又是和展示的具体内容没有关系的,Portlet 可以在服务端做到视图的聚合,而不把事情遗留到客户端完成,不涉及浏览器跨域的安全性问题;SiteMesh 是一个很适合对页眉、页脚等页面通用元素拼装的框架,比 jsp:include 标签优雅;更小维度上,标签的引用也可以认为是视图的复合。
视图的复合增进了视图模块化和重用能力,这方面来看是增加了可维护能力;但是另一方面,一个完整的直观的页面被拆得七零八落,又降低了可维护性,为了解决这个问题,我觉得对于一个大型 Web 应用,一个好的思路是提供一种工具,至少是一个简易的指导方法,从页面的某一部分元素快速定位到具体的最小视图上;另外,视图的复合带来了服务端拆解和部署的灵活性,但一定也带来性能损耗,Portlet 聚合尤为明显。
还有一个重要的事项是,页面布局需要和页面内容相独立。一个较大的视图拆解成若干个小的子视图,这些小的子视图应当具备独立的展示内容,但是页面的布局不应当有其中的任一子视图控制,而可以落到某一个整体的主题定义中去。
服务到工作者:Service To Worker。集中控制权管理和请求的处理,再把控制权交给视图之前获取表现模型。视图则根据获得的表现模型生成一个动态响应。这个模式是由前端控制器、应用控制器和视图助手组合而成的。具体说:前端控制器集中了访问视图的逻辑,然后应用控制器完成了视图导航,最后由视图助手协助准备了视图所使用的模型数据。
分配器视图:Dispatcher View。把视图本身作为请求的最初访问点,把业务处理的逻辑交由视图完成。
服务到工作者和分配器视图是非常类似的两种模式,前者以进视图前的逻辑处理为核心,后者才真正以视图为核心。当业务处理比较简单,或者不能合适地通过视图之外的逻辑来控制时,可以采用分配器视图模式,把控制逻辑放到视图中。在这种方式下,不代表分配器视图做了所有的业务逻辑,对于数据的准备完全可以在进视图之前完成,毕竟视图中完成大量的业务逻辑通常不是一个优秀的解决方案。
一个很好的例子就是页面集成,进入集成页之前准备好集成的子页面的 URL,到了集成的父页面中再执行拼装操作,这个行为,甚至可能被到客户端才完成。这种情形下,尽管页面去做了聚合视图的事,但这恰恰是页面最擅长的行为,比进页面之前把数据准备好、拼装好再一并写入页面要可见和可接受得多。
业务层模式:
业务代表:Business Delegate。封装对业务服务的访问,隐藏服务层具体实现细节,主要为降低客户端和服务层之间的耦合。除了隐藏服务细节、处理服务异常等基础功能以外,还可以做服务的缓存。业务代表是客户端的直接客户,起到客户端业务抽象层的作用,而业务代表的另一头,常常连接着会话门面。
比较常用的情况就是在某种远程连接和业务处理的基础上,使用业务代表把这些细节统统包装起来,给内部提供的模型也好 API 也好,都是和外部接口相异的。比如一个系统中对于展现的内容数据的同步,以及订购、使用等业务流程,都由 SOAP 消息载体来协助完成,那么封装起 SOAP 消息这种底层行为的 PCMP 模块,对其上内部组件暴露的都是系统中通用的模型和 API,将 SOAP 定义的模型和相应的同步、校验和通知行为等隔离开了。
服务定位器:Service Locator。封装对服务和组件的寻址。在系统,尤其是分布式系统中,服务通常被设置为可插接的,通过某种方式挂在服务总线上,寻求某服务的行为应当对服务的使用者来说透明。
某个大型解决方案中,某一组件充当 SOA 中的 ESB,承担了服务定位的角色,派发往各个服务不同协议的请求,皆可以统一的协议收拢到该组件中,再由该组件负责以各种方式分发给不同的服务。
会话门面:Session Facade。目的有二:控制客户端对业务对象的访问;降低客户端和细粒度业务组件访问的网络负载。只暴露必要的、粗粒度的服务,并且可以对之在门面内部做好事务、安全、寻址和记录等等切面辅助工作。
多数情况下使用无状态的会话门面,对于客户端要求也较低,通常只需要单次调用就能完成功能;但也可能需要使用有状态的会话门面,通常比较复杂,需要涉及会话事务、会话资源的管理和释放。
和业务代表的关系:业务代表在客户端提供了对会话门面的抽象,把客户端的请求分别代理给专门提供特定服务的会话门面。
应用服务:集中、聚合特定功能,提供一个统一的服务层,其接口粒度比服务门面细。服务门面通常包括很少,甚至不包括业务逻辑,仅仅提供一个简单和粗粒度的接口。而一些相关的操作,之间具有内聚性,这就需要某个角色把它们聚合起来。
Facade 成为粗粒度的门面的时候,内部就由多个细粒度的 Service 组成,这就是会话门面和应用服务之间的关系。举一个更具体的例子,一个短信息发送的会话门面,提供了消息发送的一系列功能,内部则包含了若干个应用服务:拼装消息报文、消息事务信息持久化、发送消息。合理地分割和规划应用服务是降低会话门面复杂度的有效途径。
业务对象:利用对象模型把业务数据和业务逻辑分离开来。业务对象在最前端(客户端)和最后端(数据资源)都会进行业务数据形式的转化。业务对象的实现通常有两种方式:POJO + JDO 或者 Entity Bean + BMP/CMP。业务对象包含业务逻辑和业务状态。
J2EE 系统中面向过程向面向对象转变有时甚至仅仅区别于最初的一念之差。没有什么是绝对的事情,如果业务非常简单,客户端通过浅浅的显示层,直接访问持久层、甚至数据资源存储中业务数据,整个过程中,其结构都是依据客户端所需数据的获取过程来完成的,是典型的面向过程的实现方式,没有什么不合理;但一旦情况复杂了,你也许希望在系统中设定一些核心的业务模型,让它们来驱动整个服务的提供和流程的运转,而不再是客户端无任何包装的需求,这时候兴许就变成了模型驱动下的面向对象行为。
复合实体:Composite Entity。结合本地 entity bean 和 POJO,实现业务对象持久化。复合实体能够把一组相互关联的业务对象聚合为粗粒度的 entity bean 实现。业务对象被实现为父对象和从属对象,从属对象紧耦合与父对象,且无法独立存在或独立被访问、识别和管理。无论使用远程对象还是本地对象实现复合实体,都不应该直接把 entity bean 暴露给客户端,而应当封装在门面里面。
实际我们的项目中,给内容超市部分,封装了核心的 API,而 API 的调用传值,都是通过复合实体——各种 Event 完成的。这是一个很好的例子,就算日后将 API 扩展成可远程调用的方法,性质并未改变。
脏数据标示器策略:对复合实体持久化的时候,如果能判断哪些从属对象是脏的,就可以提高持久化性能。
传输策略可以考虑单次传输整个复合实体,减少网络交互;可以结合脏数据标示器,只传输变化的部分;可以结合懒加载策略,只传输需要的部分。
传输对象:Transfer Object。跨层次传输多种数据元素。有一种简化远程对象和远程接口的方法是,把众多 get/set 方法合并成粗粒度的 getDate 和 setData 方法。我们通常希望传输对象是简单和可控的,因此粒度不应过细,细节应尽量屏蔽,对于接口的定义,应该尽少约束。
日后我们系统的 API 扩展必然面临着复合实体传输的情境,API 的远程调用已渐渐变得广泛,比如 JavaEye 支持 API 调用,使用 JSON 作为数据形式;我们常用的 Blog 客户端也是遵从的简约的 API 规范开发的。
传输对象组装器:Transfer Object Assembler。复合传输对象的形式构建应用模型。从各种不同的业务组件和业务服务中聚合多个传输对象,并且最后把复合对象返回给客户端。最大的好处:减少了客户端和应用模型之间的耦合。对于不同的传输形式,就并不需要应用模型做任何的改变,搭配不同的传输对象组装器和传输策略即可。
系统的页面集成中涉及到的会话信息的传递,提供了几种策略,就涉及到 SpringHTTPInvoker 传输、OSCache 传输、本地传输和 void 传输等相应的对象组装器。
值列表处理器:Value List Handler。执行查询、缓存结果,并让客户端遍历、选择查询结果。基本上相当于封装了一个游标,共客户端遍历操作,但是如果这个游标是远程的,注意可能造成巨大的性能消耗。
我们的系统中设计了一个类:PaginationSupport,用于分页,其角色便类似于值列表处理器。需要查询哪一块区域的数据集合,就传递相应的游标指令。
集成层模式:
数据访问对象:Data Access Object。提炼和封装对持久化存储介质的访问。DAO 封装了数据源的实现细节,总是面向 API 调用者提供统一的接口。DAO 应当被实现为无状态的对象,这样就可以成为轻量的对象,不需要考虑线程、同步、缓存等问题,而把这些问题下沉到数据层去完成。
以我参与的项目的缓存的使用举例,模型 DAO 并不做任何的缓存行为,数据库使用自身的缓存能力,并且在必要时冗余字段,这是基于数据粒度的基础缓存;到了调用 DAO 的业务层面,比如 Service 层,才进行业务模型粒度的缓存,比如缓存某些用户对象等;而 DAO 层实现了基础 DAO 的约束,继承了 Spring 给 DAO 封装的基础能力,比如事务控制的能力等,所有方法都不使用类状态变量,找不到任何对用户会话对象访问的逻辑,也看不到任何 java.sql 包内的类和对象(尤其是异常)。
服务激活器:Service Activator。用于接收异步请求,由异步请求来触发业务。JMS 监听器是一个常用的实现者,JMS 目标通常有两种,一种是主题,即 Topic,用于点对面的通知;一种是队列,即 Queue,用于点对点的通知。
解决方案中的请求通常都由界面侧触发,因此服务激活器通常被放置在内部部件(例如计费部件)上,计费业务的请求由展现部件经过总线转发给计费部件触发计费服务流程。
业务领域存储:将持久化逻辑从对象模型中分离出去。比如最常用的 BMP 和 CMP,无需根据不同的业务对象类型建立不同的数据库脚本,只需要维护好业务领域侧的模型配置,存储事件是透明的。
业务领域存储的实现有很多种方式,比如 Grails 内部使用规约配置和 Hibernate 的持久化管理能力,让存储的逻辑完全透明,映射关系的配置和映射表建表和 CRUD 的 sql 语句都可以由规约代替,于是可以不进行任何的映射配置来实现存储,真正做到透明存储。
再比如:上述的关系型数据库下,数据库表和业务模型是有映射关系的,也就是常说的横表;但是也可以使用纵表,实现数据模型的任意扩展,这就是一个通过改变存储方式来实现持久化逻辑完全不依赖于对象模型的例子。
使用 nosql,海量数据的存储可以是稀疏的,水平扩展性、查询性能优异,它减弱了数据之间在存储层面上相互之间的约束。
Web Service 中转:暴露可通过 XML 和 web 协议访问的服务,并将对服务的请求转发给真实的服务组件。通常有许多 Web Service 是不希望暴露出来的,有时有一些服务又需要聚合起来使用,这时候就需要 Web Service 中转。在使用中转前的 Web Service 需要被改造,以支持中转的接口(例如一个本地接口)。这个模式和 Facade 很类似,只不过它的定位放在了远程接口上。
微架构:一组被同时使用的模式,用于实现系统中的一个特定部分(子系统)。每一个微架构是独立和内聚的积木块,由它们构成整个系统的架构。微架构有多种形式,比如 Web Worker 工作流。
工作流可以把业务逻辑和过程逻辑分离开,使得关注点和阶段清晰和分散。本人当前参与的项目是一个较大的 Web 项目,处于整个解决方案的前端,但是里面并未明确提及工作流(尽管在解决方案的后端,计费部件和内容管理部件中明确定义和使用了)。首先要说的是,作为一个展现部件,对于用户操作的过程中个,并不适合具备过多的用户交互途径,通常也不会有特别繁杂的业务逻辑;但是倘若整个项目的内容使用部分流程过于复杂,完全可以引入工作流的思想解决问题。
以一个展现系统中播放的内容使用的流程为例,第一次交互行为 prePlay,发起播放行为,系统给用户返回一个产品确认页面;用户确认并发起第二次交互行为 play,系统给用户完成订购操作并生成播放的 rtsp 链接回送给用户;第三次用户使用此 rtsp 链接和播放系统交互。这三次主流程的交互中,所有交互行为都由用户出发(这是通常是展现系统的工作流步骤中的特点),并且只有前两步和展现系统相关。每一个步骤都具备独立的拦截器栈,相应的 Action-Service-DAO 方法。在某些业务复杂的系统中,工作流的步骤是可以自定义的,即用户可以自行组装工作流——这样的定制属于纵向业务流程的定制,与横向的 API 调用的定制相异。
通常我们使用 request-session-application 这样三类容器管理当前数据,但是在 request scope(一次交互的数据存放)和 session scope(用户会话数据存放)对象之间,还可以引入 flash scope(N 次交互的数据存放,由拦截器管理)和 work flow scope(一个完整业务流程的数据存放,由工作流引起管理)对象。
文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《四火的唠叨》