Jan 8 2006
Table of Contents
1. 问题的产生
2. 开发模型
3. 结论
中国 北京 陈建
<james...@gmail.com>
struts是开放源码的企业级web应用开发框架。它非常好地解决了服务器端业务处理之前的诸多问题。在业务处理上让开发人员自由发挥。由于技术的多样性,可以提供业务处理的开发框架或者开发模式非常多。虽然使用哪种方式都没有对错之分,但是对于软件系统——尤其是来源于项目的软件系统来说,在开发成本的“紧箍”之下就有了差别。在小团队、简单项目的环境中,使用复杂的开发框架,除了能够把开发人员累死或者延误开发进度之外没有其他任何好处;同样,在复杂项目的环境中,使用简单的开发框架,往往会产生一个弱不禁风的系统,在诸多可变因素的影响下软件系统有如摇摇欲坠的茅屋。本文地目的正是希望讨论一下如何能够快速、低成本地开发具有适当灵活性、可扩展的简单应用。
在讨论问题之前我们首先明确,什么是简单应用?
业务逻辑几近于无,整个系统几乎可以抽象为对数据的CRUD(增查改删),几乎不存在需要在内存中维护其关系和长时间占用资源处理的对象。简单应用的特点是规模小、资源少、升级改造约等于重新开发。基于这些特点,开发团队必须在质量和成本之间寻求平衡。
那么如何才能达到平衡呢?
非常明确的是,以牺牲质量来降低成本是绝对错误的,必然会带来恶梦般的维护期。为了回答这个问题,我们首先来了解一下常用的企业级复杂应用开发的模式。此类应用往往会被纵向分从展现层到持久化层的n层来实现。对于简单应用来讲,同样也少不了展现层和持久化层。只是中间的n-2层是否需要值得商榷。显而易见,在简单应用中保留展现层和持久化层就为应用从简单到复杂的成长提供了机会,同时也最大程度地重用原有资源。那么被删节的n-2层的业务逻辑该怎么办呢?由于这些逻辑在简单应用中可能根本不存在,即使存在也非常薄弱,所以把它们放在展现层的后端是非常好的选择。这样一来,应用即成为展现层加上持久化层的系统。
如何实现持久化层?
大凡实现持久化层有两种方案:第一种使用成熟的复杂的持久化框架(譬如:Hibernate);第二种自行开发。对于尽量简化开发过程降低开发成本的初衷来讲,第一种方案显然稍有违背——引入了复杂性。自行开发的持久化层只需要提供简单的CRUD功能,仅此而已,多则无益。从简单性效率上来讲自行开发略胜一筹。
需要DAO工厂吗?
DAO工厂的主要职责是封装DAO的创建过程,为应用提供动态变更持久化层的能力。要揭示这个问题,首先得回答这样一个问题——简单应用需要多个不同的持久化层实现吗?答案是显然的,绝大多数情况不需要。既然如此,那么DAO工厂自然可以省略。
需要持久化层“外观(Facade)”吗?
许多持久化框架都使用外观来封装内部实现,用于提供灵活的、可配置的持久化功能。在复杂应用环境中,持久化层为上面许多层提供服务,他的变化将是致命的。然而在简单应用环境中,对持久化层的变化往往意味着整个应用的重新开发。从变动几率上来讲展现层的变化比持久化层的变化要多得多。所以持久化层外观也可以省略。
需要为每个实体创建与之对应的TO吗?
在各个层之间以松耦合的方式传递数据是个好主意。出于简单的目的,显然为每个实体创建一个与之对应的TO略显多余。但是又不能因此而违背常理完全抛弃松耦合的优势。那么最好的办法就是找一个可以适用于所有实体的替代品。Map是个不错的选择。
如何解决字典型数据的使用问题?
字典型数据的特点是使用频率高、变更频率低。那么最好的策略就是在应用启动的时候加载之后作为属性放在ServletContext
中。在JSP或者Action中随时取用。那么更新了怎么办呢?对于简单应用来讲总会有重新启动一次的机会的,而且又不花多少时间。所以改变之后重新启动一次是个不错的主意。
如上所述,简单应用开发模型可以归纳为:
· 建立良好的概念模型,并通过概念模型设计出稳定的数据模型。
· 使用struts解决展现层问题。
· 为每个实体创建一个DAO。
· Action中执行简单的逻辑之后,直接调用对应的DAO访问数据。
· Action与Dao之间使用Map传递数据。
·
创建一个ServletContextListener
负责在应用启动的时候初始化(从数据库中读取等)字典类型数据,并将字典类型数据作为属性存放在context中备用。以两种格式存放同一字典数据。它们分别是Collection型和Map型。Collection型字典数据负责提供有序列表;Map型字典数据负责根据字典值获得与该值对应的名称。
对于“简单应用”,其实现方式也必然突出简单的特点。象事务处理、缓存等复杂问题也就无须过分深究了。以至于开发一个小的平台来配置纯粹的CRUD应用都是很有可能的。试问这种平台的价值,那就要看有多少所谓的“企业级应用”能够超脱CRUD而真正名副其实了。
不好意思,我对Facade,DAO概念一直不太明白,有哪位朋友能讲讲或者推荐本书么?谢谢。
Façade的详细介绍可以参看《设计模式》。这些设计模式并没有实现语言的限制。更直白的应用场景,欢迎大家讨论。
发件人: program...@googlegroups.com
[mailto:program...@googlegroups.com] 代表 Yong Chen
发送时间: 2006年1月11日 12:41
收件人: program...@googlegroups.com
主题: [Programmer Cafe] Re: [tips]--基于struts的简单应用开发模式
是的。
Façade模式被翻译为外观模式。其目的是让内部复杂的系统更加易用;将客户程序对易变的复杂系统的依赖进化为对稳定接口的依赖。譬如:持久化层的Façade可能仅提供带有参数的CRUD等少数方法。客户程序在使用持久化层时,只需要调用Façade的方法即可,而不必关心持久化层是如何设计的或者使用了什么技术(hibernate 或自定制的)。
发件人: program...@googlegroups.com
[mailto:program...@googlegroups.com]
代表
Yong
Chen
发送时间: 2006年1月11日 14:37
收件人: program...@googlegroups.com
主题: [Programmer Cafe] Re: 答复: [Programmer Cafe] Re: [tips]--基于struts的简单应用开发模式
是《设计模式--可复用面向对象软件的基础》么?
要回答文中提到的问题,我们首先来界定一下讨论基础--"简单应用"。所谓简单应用就是没有复杂业务逻辑的纯粹提供对数据CRUD功能的应用。那么肯定有人会说哪里有这样的"简单应用"呢?其实最明显例子就是为一个大的应用提供数据字典维护之类的功能。讨论基础明确之后,可以开始讨论问题了。
1。随着request请求一起提交的参数都是以String类型提交的,它必然与实际的类型不符。那么form与to(文中使用map替代)之间的映射也就是不可避免的了。有人写过一些工具,以便于让这种映射能够自动完成。对于此,我的个人观点是,有则拿来用,没有则老老实实自己写映射代码。但是当奉行拿来主义的时候要注意,这种自动映射工具在to中有多个重载方法时一般都会出错,因为它不知道要用哪个方法,所以"随机地"选择一个就用上了,结果就可想而知了。Apache的commons工具包的BeanUtil就是一个例子。综合考虑,自己写映射代码是不错的选择。当然对于简单应用来说,这种映射就只好在action中完成了。
2。在Map接口的声明中,无论是key还是value都可以是Object。既然可以从任何对象中获得其类型,那么保存在Map中的值自然也就携带了其类型。
3。如果要求这样的"简单应用"担负起更大的担子,那么只好又action来全权代劳了。一般情况下,象exception、message、log、redirect、authentication
和authorization等功能都会由置于action前面的控制器来担当。这么说来,action中所要做的工作其实也不太多了。
public class Util {
public static void copyProperties(Object desObj, Object resObj) {
PropertyDescriptor descriptor = null;
try {
BeanInfo beanInfo =
Introspector.getBeanInfo(resObj.getClass());
PropertyDescriptor[] descriptors
= beanInfo.getPropertyDescriptors();
String[] names = new String[descriptors.length];
int i = 0;
for (/**/; i < names.length; i++) {
String name = descriptors[i].getName();
Class type = descriptors[i].getPropertyType();
if (descriptors[i].getReadMethod() != null) {
Object value
=
descriptors[i].getReadMethod().invoke(resObj);
try {
descriptor
= new PropertyDescriptor(name,
desObj.getClass());
} catch (IntrospectionException ex) {
continue;
}
try {
if
(!descriptor.getPropertyType().isPrimitive()) {
Object obj
= convertValue(value,
descriptor.getPropertyType(),
desObj);
descriptor.getWriteMethod()
.invoke(desObj, new Object[] { obj });
}
} catch (Exception ex) {
System.out
.println("the function name is ----->"
+ name);
ex.printStackTrace();
}
}
}
} catch (Exception e) {
System.out.println(descriptor.getName());
System.out.println(descriptor.getPropertyType().getName());
e.printStackTrace();
}
}
private static Object convertValue(Object resValue, Class desType,
Object desObj) {
if (resValue == null)
return resValue;
if
(resValue.getClass().getName().equalsIgnoreCase(desType.getName()))
return resValue;
if (resValue.getClass().getSuperclass().getName()
.equalsIgnoreCase(desType.getName()))
return resValue;
if ((resValue instanceof Boolean || resValue instanceof Integer
|| resValue instanceof Long || resValue instanceof
Float
|| resValue instanceof Double)
&&
desType.getName().equalsIgnoreCase("java.lang.String"))
return resValue.toString();
if (desType.getName().equalsIgnoreCase("java.lang.Boolean")
&& resValue instanceof String) {
Boolean var_boolean;
try {
var_boolean = new Boolean((String) resValue);
} catch (Exception ex) {
return null;
}
return var_boolean;
}
if (desType.getName().equalsIgnoreCase("java.lang.Integer")
&& resValue instanceof String) {
Integer integer;
try {
integer = new Integer((String) resValue);
} catch (NumberFormatException ex) {
return null;
}
return integer;
}
if (desType.getName().equalsIgnoreCase("java.lang.Long")
&& resValue instanceof String) {
Long var_long;
try {
var_long = new Long((String) resValue);
} catch (NumberFormatException ex) {
return null;
}
return var_long;
}
if (desType.getName().equalsIgnoreCase("java.lang.Float")
&& resValue instanceof String) {
Float var_float;
try {
var_float = new Float((String) resValue);
} catch (NumberFormatException ex) {
return null;
}
return var_float;
}
if (desType.getName().equalsIgnoreCase("java.lang.Double")
&& resValue instanceof String) {
Double var_double;
try {
var_double = new Double((String) resValue);
} catch (NumberFormatException ex) {
return null;
}
return var_double;
}
return null;
}
private Util() {
}
}
不知道这个Util是不是和Apach-e的commons工具包的BeanUtil相同,写在这里大家有兴趣可以研究一下,可能存在关于陈哥说的重载问题,不过我看了java.lang.class中的getMethod(String
name, Class...
parameterTypes)方法可以返回一个确定的参数类型的方法,配合Field根据Field产生getMethod方法中的name参数,应该可以解决这个问题,不过要注意,field一定要记得setAccessible(true).说的远了^^完成这个工具后,只要在别的类中import它,然后就可以用:
Util.copyProperties(formBean, toBean);
Util.copyProperties(toBean, formBean);
来自动进行映射了.如果把这个加入到这个简单框架中,不知道可行不可行.
2有个问题,刚才看那个OA的代码才弄明白,它也是通过类反射机制来实现数据库操作的.比如DBProcessor中的static方法query(String
sql,String
className)中根据传入的sql得到ResultSet,再通过ResultSetMetaData
rsmd =
rs.getMetaData();得到列信息,然后循环自动创建并付值类名为className的对象,以ArrayList或数组的方式返回,大概流程转来网上一段程序:
public ArrayList query(String sql,String className){
ArrayList paraList=new ArrayList();
//传入的sql得到ResultSet
try{
if (conn == null){
Connection();
}
PreparedStatement stmt = conn.prepareStatement(sql);
ResultSet rs = stmt.executeQuery();
String recordValue="";
Object c1=null;
paraList=new ArrayList();
//再通过ResultSetMetaData rsmd =
rs.getMetaData();得到列信息
ResultSetMetaData rsmd = rs.getMetaData();
int columnCount = rsmd.getColumnCount();
//循环自动创建并付值类名为className的对象,以ArrayList或数组的方式返回
//循环rs每一行
while (rs.next()){
//创建一个类名为className的对象
c1=Class.forName(className).newInstance();
//循环每一行中的每一列
for (int i=1; i<=columnCount; i++) {
if(rs.getString(rsmd.getColumnName(i))!=null){
recordValue=rs.getString(rsmd.getColumnName(i));
}else{
recordValue="";
}
//得到类中与表当前列名相同的属性的set方法
Method
m=c1.getClass().getMethod(getSetMethodName(rsmd.getColumnName(i))
,new
Class[]{recordValue.getClass()});
//执行set方法,将表当前行当前列下的数据set进对象中
m.invoke (c1, new Object[]{recordValue});
}
paraList.add(c1);
}
}catch(SQLException ex){
}catch(ClassNotFoundException e){
}catch(NoSuchMethodException e) {
}catch(InvocationTargetException e){
}catch (IllegalAccessException e){
}catch(InstantiationException e){
} finaly{
closeConnection();
return paraList;
}
}
这样就不用为每个表建一个dao了都,因为在这种简单的对表的CRUD中,完全可以将FORMBEAN建的与数据库中的字段对应,这样得到的所有不用的formBean对象就可以直接或间接的被query(或是insert,update,delete)方法调用,完成数据库操作:
ArrayList forms=
query(sql,formBean.getClass());//先执行sql,然后循环创建formBean对象,利用类反射机制为每个formBean付值返回
insert((ActionForm)formBean);//根据类反射机制找出formBean的所有属性和对应的值,拼成sql执行
update((ActionForm)formBean);//同上
del((ActionForm)formBean);//同上
不知道表达是否正确清楚.这样只有一个类的几个静态方法就可以完成一堆DAO的任务,当然是在这种简单的对表的CRUD中,完全可以将每一个formBean的属性名称建的与数据库中一个表字段名称完全对应的情况下.而这种情况应该在本文讨论的语境范围内,不知道这种方式是否可行?