电脑和手机都有配套的离线软件。不知是我的网络问题还是什么问题,这时中国博客打不开。下面是Google中的简介:您将安装在自己手机中的MRabo程序与您在中国博客网的用户名及密码进行绑定后,便可随时随地通过手机发表日志或上传手机图片 ... Rabo是中国博客网客户端产品中的一种,它是安装在用户计算机端的管理自己博客的软件。它不但结合了RSS阅读器浏览日志的方便...
分类:开源
提高学习日记导航能力的思路
下面是我对导航系统提高的思路,希望大家能够指出其中的问题并告诉我,让我少走弯路,您有什么意见和建议和问题也可以提出来。
我会持续把我对导航系统的思考过程记录在这里。
以前曾经就这个问题有个设想如下(http://www.learndiary.com/disDiaryContentAction.do?goalID=558):
“4、改善系统的导航功能:
关于保持访问路径的方法,我想了一点方法,仅供参考:用一个LinkedList对象保存访问路径字符串,其中的每一个元素是当前路径的字符串,在LinkedList中通过添加元素、查找元素、删除元素可以实现路径的跳转和回溯。”
然后,我前些天作了一些思考,已记录在开源社区的cvs库中,其中的想法我已经初步认识是不可行的,但为了记录思路的过程,我也把它贴在这里:
1.The key of this modifing is to keep the parents information of the current page.
1).keep the information of where the goal come from:
For example,a goal,it is maybe in "All Goals List(indexAction.do?pageNum=1),the abbreviation is 'AGL'",
"Processing Goals List(processGoalAction.do?pageNum=1,the abbreviation is 'PGL')",
"Quited Goals List(quitedGoalAction.do?pageNum=1,the abbreviation is 'QGL')",
"Finished Goals List(finishedGoalAction.do?pageNum=1,the abbreviation is 'FGL')",
"Searched Goals List(goalResultAction.do;jsessionid=4A629DA49B39A0C426F31B6DAB2FC0A3?pageNum=1),the abbreviation is 'SGL'".
So,when we click to view the content or the diaries list of a goal,we should record where
this goal come from,the item is among AGL,PGL,QGL,FGL and SGL;and,the page number "pageNum" is needed to record too.
2).how to return to goals list when open different low level items in different windows:
For example,when we click to view the goal content(disGoalContentAction.do?goalID=36) in
page "indexAction.do?pageNum=1",then,we click to view the same goal content(disGoalContentAction.do?goalID=36) in
page "processGoalAction.do?pageNum=1".When we want return to goals list in page "disGoalContentAction.do?goalID=36",
which goals list should we return?AGL or PGL?My answer is:return the lastest goals list,for example,if you visit
AGL before PGL,we will return to page PGL.
3.ok,I have got a new idea,I should build a Navigator class,it will keep the all needed information concerning
navigation,and it will be transfered to it's low level page as a implicit parameter.
4.All paths list:
1)all goals
2)all goals->display goal's content
3)all goals->display goal's content->comment goal
昨天,我把动网的导航系统看了一下,其中的搜索帖子功能也不如我意:在查看搜索结果帖子的内容页面中点击下一篇帖子,得到的是帖子所在版块的下一篇帖子,而不是搜索到的帖子的下篇帖子。
而且,在帖子提交、编辑等功能页面的导航可能均需要特殊的考虑。
实际上,系统的查看帖子和搜索帖子功能总的说来都是搜索功能。导航的根本是把经历过的页面的搜索参数保留下来。
一、学习日记导航路径列举:
总的说来,本系统存在下列几种导航路径:
1、用户查看所有目标及其日记的导航:含有下列路径:
1)所有目标列表->相关日记列表;(目标ID)
2)所有目标列表->相关日记列表->显示日记内容;(日记ID)
3)所有目标列表->相关日记列表->编辑日记内容;(日记ID)
4)所有目标列表->相关日记列表->显示日记内容->撰写评论;
5)所有目标列表->相关日记列表->显示日记内容->编辑评论;(评论ID)
2、用户查看目标的内容及提交对目标的评论的导航:含有下列路径:
1)所有目标列表->查看目标内容; (目标ID)
2)所有目标列表->查看目标内容->撰写对目标的评论;(目标ID)
3)所有目标列表->查看目标内容->编辑对目标的评论; (评论ID)
3、您的进行中目标中的导航路径:
1)目标内容:
(1)所有目标列表-》进行中的目标列表; (用户ID)
(2)所有目标列表-》进行中的目标列表-》查看目标内容;
(3)所有目标列表-》进行中的目标列表-》查看目标内容->撰写对目标的评论;
(4)所有目标列表-》进行中的目标列表-》查看目标内容->编辑对目标的评论;
2)所有日记:
(5)所有目标列表-》进行中的目标列表-》所有日记列表;(目标ID)
(6)所有目标列表-》进行中的目标列表-》所有日记列表->显示日记内容;
(7)所有目标列表-》进行中的目标列表-》所有日记列表->编辑日记内容;
(8)所有目标列表-》进行中的目标列表-》所有日记列表->显示日记内容->撰写评论;
(9)所有目标列表-》进行中的目标列表-》所有日记列表->显示日记内容->编辑评论;
3)您的日记:
(10)所有目标列表-》进行中的目标列表-》您的日记列表;(目标ID,用户ID)
(11)所有目标列表-》进行中的目标列表-》您的日记列表->显示日记内容;
(12)所有目标列表-》进行中的目标列表-》您的日记列表->编辑日记内容;
(13)所有目标列表-》进行中的目标列表-》您的日记列表->显示日记内容->撰写评论;
(14)所有目标列表-》进行中的目标列表-》您的日记列表->显示日记内容->编辑评论;
4)写作日记:
(15)所有目标列表-》进行中的目标列表-》写作日记;(目标ID)
1》当提交日记后转到:查看您刚才提交的日记 ,路径为:3)-》(11)所有目标列表-》进行中的目标列表-》您的日记列表->显示日记内容;
2》当提交日记后转到:继续撰写下一篇日记 ,路径为:1)-》(1)所有目标列表-》进行中的目标列表;
3》当提交日记后转到:查看当前目标的您的日记列表 ,路径为:3)-》(10)所有目标列表-》进行中的目标列表-》您的日记列表;
4》当提交日记后转到:查看当前目标的所有日记列表 ,路径为:2)-》(5)所有目标列表-》进行中的目标列表-》所有日记列表;
4、您的已退出的目标中的导航路径:
1)目标内容:
(1)所有目标列表-》已退出的目标列表; (用户ID)
(2)所有目标列表-》已退出的目标列表-》查看目标内容;
(3)所有目标列表-》已退出的目标列表-》查看目标内容->撰写对目标的评论;
(4)所有目标列表-》已退出的目标列表-》查看目标内容->编辑对目标的评论;
2)所有日记:
(5)所有目标列表-》已退出的目标列表-》所有日记列表;(目标ID)
(6)所有目标列表-》已退出的目标列表-》所有日记列表->显示日记内容;
(7)所有目标列表-》已退出的目标列表-》所有日记列表->编辑日记内容;
(8)所有目标列表-》已退出的目标列表-》所有日记列表->显示日记内容->撰写评论;
(9)所有目标列表-》已退出的目标列表-》所有日记列表->显示日记内容->编辑评论;
3)您的日记:
(10)所有目标列表-》已退出的目标列表-》您的日记列表;(目标ID,用户ID)
(11)所有目标列表-》已退出的目标列表-》您的日记列表->显示日记内容;
(12)所有目标列表-》已退出的目标列表-》您的日记列表->编辑日记内容;
(13)所有目标列表-》已退出的目标列表-》您的日记列表->显示日记内容->撰写评论;
(14)所有目标列表-》已退出的目标列表-》您的日记列表->显示日记内容->编辑评论;
5、您的已完成的目标中的导航路径:
1)目标内容:
(1)所有目标列表-》已完成的目标列表; (用户ID)
(2)所有目标列表-》已完成的目标列表-》查看目标内容;
(3)所有目标列表-》已完成的目标列表-》查看目标内容->撰写对目标的评论;
(4)所有目标列表-》已完成的目标列表-》查看目标内容->编辑对目标的评论;
2)所有日记:
(5)所有目标列表-》已完成的目标列表-》所有日记列表;(目标ID)
(6)所有目标列表-》已完成的目标列表-》所有日记列表->显示日记内容;
(7)所有目标列表-》已完成的目标列表-》所有日记列表->编辑日记内容;
(8)所有目标列表-》已完成的目标列表-》所有日记列表->显示日记内容->撰写评论;
(9)所有目标列表-》已完成的目标列表-》所有日记列表->显示日记内容->编辑评论;
3)您的日记:
(10)所有目标列表-》已完成的目标列表-》您的日记列表;(目标ID,用户ID)
(11)所有目标列表-》已完成的目标列表-》您的日记列表->显示日记内容;
(12)所有目标列表-》已完成的目标列表-》您的日记列表->编辑日记内容;
(13)所有目标列表-》已完成的目标列表-》您的日记列表->显示日记内容->撰写评论;
(14)所有目标列表-》已完成的目标列表-》您的日记列表->显示日记内容->编辑评论;
6、添加新目标的导航:
1)
(1)所有目标列表-》添加新目标;
2)提交新目标后的选择:
(2)转到:查看您刚才提交的目标:导航为:所有目标列表-》进行中的目标列表-》查看目标内容;
(3)转到:撰写日记 :导航为:所有目标列表-》进行中的目标列表;
7、搜索功能中的导航:
1)搜索:
(1)所有目标列表-》检索页面;
2)检索目标:
(2)所有目标列表-》检索页面-》搜索结果目标列表;(条件字符串,已经存储在全局Session中)
(3)所有目标列表-》检索页面-》搜索结果目标列表->查看目标内容;
(4)所有目标列表-》检索页面-》搜索结果目标列表->查看目标内容->撰写对目标的评论;
(5)所有目标列表-》检索页面-》搜索结果目标列表->查看目标内容->编辑对目标的评论;
目标的日记的导航:
(6)所有目标列表-》检索页面-》搜索结果目标列表->相关日记列表;(目标ID)
(7)所有目标列表-》检索页面-》搜索结果目标列表->相关日记列表->显示日记内容;
(8)所有目标列表-》检索页面-》搜索结果目标列表->相关日记列表->编辑日记内容;
(9)所有目标列表-》检索页面-》搜索结果目标列表->相关日记列表->显示日记内容->撰写评论;
(10)所有目标列表-》检索页面-》搜索结果目标列表->相关日记列表->显示日记内容->编辑评论;
3)检索日记:
(10)所有目标列表-》检索页面-》搜索结果日记列表;(条件字符串,已经存储在全局Session中)
(11)所有目标列表-》检索页面-》搜索结果日记列表->显示日记内容;
(12)所有目标列表-》检索页面-》搜索结果日记列表->编辑日记内容;
(13)所有目标列表-》检索页面-》搜索结果日记列表->显示日记内容->撰写评论;
(14)所有目标列表-》检索页面-》搜索结果日记列表->显示日记内容->编辑评论;
日记所在目标的导航:
(15)所有目标列表-》检索页面-》搜索结果日记列表->查看目标内容;(目标ID))
(16)所有目标列表-》检索页面-》搜索结果日记列表->查看目标内容->撰写对目标的评论;
(17)所有目标列表-》检索页面-》搜索结果日记列表->查看目标内容->编辑对目标的评论;
日记所在目标所有日记的导航:
(18)所有目标列表-》检索页面-》搜索结果日记列表->相关日记列表; (目标ID)
(19)所有目标列表-》检索页面-》搜索结果日记列表->相关日记列表->显示日记内容;
(20)所有目标列表-》检索页面-》搜索结果日记列表->相关日记列表->编辑日记内容;
(21)所有目标列表-》检索页面-》搜索结果日记列表->相关日记列表->显示日记内容->撰写评论;
(22)所有目标列表-》检索页面-》搜索结果日记列表->相关日记列表->显示日记内容->编辑评论;
二、学习日记导航功能实现思路探索:
例如:在下面的导航中:
1、所有目标导航:
(1)所有目标列表->相关日记列表;
(2)所有目标列表->相关日记列表->显示日记内容;
(3)所有目标列表->相关日记列表->编辑日记内容;
(4)所有目标列表->相关日记列表->显示日记内容->撰写评论;
(5)所有目标列表->相关日记列表->显示日记内容->编辑评论;
中,有如下问题:
(1)点击"所有目标列表"或"相关日记列表"是回到列表的第一页呢,还是回原来的页数呢?是使用默认的排序方式还是使用用户选定的排序方式呢?如果回到原来的页数,这时假设在这期间又有无数帖子提交了,结果是回不到原来帖子所在的那个页面的。
我看了看动网和JAVA资源网的方式,都是回到列表的第一页,所以我会采用回到第一页的做法,并且使用用户选定的排序方式-用户选定的排序方式储存在程序的Session中;
(2)、如何唯一的定位一个特定的页面呢?
如:如何定位”相关日记列表“呢?需要3个参数:
1》目标ID;
2》排序方式;
3》每页条目数和当前页数;
其中,2》中的排序方式已经存储在全局的Session中了,3》中每页的条目数已经定了,当前页数不用考虑,因为经过我们如(1)的分析,我们总是回到第一页中去;
又如:如何定位显示日记内容呢?显然,仅需要这条日记的ID而已;
现在,我把导航系统中定位每一个页面所需要的参数标在“一”中的各个导航路径后面。
可以看到,定位一个页面需要的参数分为5种:
1》列表:需要列表的parentID;
2》单个条目:需要它的ID;
3》用户的进行、完成等目标列表:需要用户ID;
4》用户的进行、完成等目标的用户日记列表;需要用户ID和目标ID;
5》搜索列表:什么都不需要,因为搜索字符串已保留在全局Session中了;
然后,因为用户的ID已经存储在全局的Session中了,所以上面需要的参数进一步减为:
1》列表:需要列表的parentID;
2》单个条目:需要它的ID;
3》用户的进行、完成等目标列表:什么都不需要,用户ID在Session中;
4》用户的进行、完成等目标的用户日记列表;目标ID,所需的用户ID在Session中;
5》搜索列表:什么都不需要,因为搜索字符串已保留在全局Session中了;
这样,唯一定位一个页面所需要的参数变为:
1》页面类型;
2》参数ID;
然后,在页面上显示url需要的参数为:
1》标题:如一个目标的日记列表的标题应该是:“目标‘学习英语’的所有日记列表”,或者是:“目标‘学习英语’您的日记列表”,而显示一页日记的标题应该是:“日记‘今天我背了10个单词’”,等等;
2》页面的url地址,如显示一页日记的url是:http://www.learndiary.com/disdiary.do?diaryID=10
在页面上显示url需要的参数均可以由上面的定位页面所需的两个参数产生出来(即页面类型,参数ID);
于是,可以考虑用一个链表保留定位页面所需的参数,把这两个参数封装为一个导航节点类:
public class PageNavNode {
private int typeID;
private int parameter;
public PageNavNode(int typeID,int parameter){}
public setTypeID(){}
...
}
当点击一个url时,就把这个url所需的节点加入到一个链表中,这时链表变长;当在页面的导航链中点击上一个层次的链接时,就把这个节点后面的节点删除;
也就是说,把这个链表作为一个request的参数传给页面,每一个页面中都有一个导航的链表参数在这个页面的生存周期中起作用;
为了得到页面所需的html字符串,这个链表参数应该传入进入页面前的action中,由action负责将这个链表转变为页面导航所需要的字符串参数,为了分隔功能,可以在Pager类中设置一个方法,名称是:pageNaviGenerate(LinkedList naviLinker);
但是,怎么保证这个链表能够继续下传下去呢?
我知道有2种方法可以解决这个问题:
1、把这个链表放在Session中,多个页面可以共享,并且可以修改这个链表,供下面的页面使用。但是,由于同个页面可以出现在不同的链表中,而且,系统有几个不同的链接路径,随着系统的成长,这样的链接路径还会不断增加。每一个链接路径都需要一个Session中的链表来保持这条路径的导航信息。
比如,在“所有目标”这条路径查看某一篇日记就应该保存在“所有目标”这条链接路径中,而在“我的目标”这条路径查看同一篇日记就应该保存在“我的目标”这条路径中,显然,这在Session中需要不同的条目来保存。而且,为了识别日记页面是位于哪条路径上,打开这个页面前的action中需要上一个页面传来一个路径类别参数。这样,系统才能在Session中找到对Session中哪个链表进行修改。
2、把这个链表封装成字符串,这个字符串在不同的页面间经过,然后不断的改变。把这个封装的字符串连同客户请求的url一起传给请求url前的action,由action解封并处理成下一个页面需要的导航字符串。
今天,我尝试用第二种方法来实现。
第一步:封装字符串传给action->action结合请求的页面和接受的封装字符串得到下一个请求页面的封装字符串并把这个封装字符串传给下一个页面,使下一个页面可以接力把这个封装字符串再传到再下一个请求页面前的action中->
把请求页面封装的字符串转变为请求页面所需的导航字符串,并存入request中,供下一页面显示导航字符串提取使用。
2005年10月19日
昨天,对导航系统的设计思路作了一下验证,基本可行。
在设计中的选择:
1、当前页面的导航条目灰化,主要是导航条目的页码总是第一页,当前的页面可能不是第一页,会引起混乱,而且,惯例是当前页面的导航条目灰化。
2、设计中用到的方法:
1)、接收来自request的导航封装编码字符串,和将要显示的页面的类型,得到将要显示的页面所需的导航封装编码字符串。
实现方法,封装字符串采用的格式:页面类型(1位数字)+页面参数(如显示目标的日记列表所需的目标ID,显示日记内容所需的日记ID)+分隔符(我采用字符"a")
如字符串1025,表示:所有目标-》目标:“建设“学习日记””的日记列表;
得到将要显示的页面所需的导航封装编码字符串的方法:
将显示页面类型ID在字符串中从前向后检索,1》如果检索到相同的类型,就把检索到的节点字符串后面的字符串截掉:
如当前页面是查看日记内容,假设保持的编码字符串是:10a25a373,表示:所有目标-》目标:“建设“学习日记””的日记列表─》日记:用cookie保存用户密码登录流程
这时,我点击“目标:“建设“学习日记””的日记列表”,它的类型ID是2,于是,就把25后面的字符串“a373”截掉。查看日记列表的导航封装字符串变为:10a25。
2》如果没有检索到相同的类型,就把新的节点字符串加到现在的字符串中:
如当前页面是查看日记内容,假设保持的编码字符串是:10a25a373,表示:所有目标-》目标:“建设“学习日记””的日记列表─》日记:用cookie保存用户密码登录流程
这时,我点击了评论日记链接,它的类型ID是4,于是,就在后面加上:a473,评论日记的页面对应的导航封装字符串变为:10a25a373a473
/**
* receive the encoding navigation string come from request and the type id of the page which we are going to and the parameter of the page which we are going to(it is articleID generally),
* get the encoding navigation string which it will be used in the page we are going to.
* @param naviStr the encoding navigation string come from request.
* @param toPageType the type id of the page which we are going to.
* @param parameter the parameter of the page which we are going to(it is articleID generally).
* @return the encoding navigation string which it will be used in the page we are going to.
*/
public static String encodeNaviStr(String naviStr, String toPageType, String parameter) {
String newNaviStr = naviStr;
int searchResult = naviStr.indexOf("a" + toPageType);
if (searchResult == -1) {
newNaviStr = newNaviStr.concat("a").concat(toPageType).concat(parameter);
} else {
newNaviStr = newNaviStr.substring(0, searchResult).concat("a").concat(toPageType).concat(parameter);
}
return newNaviStr;
}
public static void main(String[] args){
System.out.println( encodeNaviStr("a10a25a337", "4", "73"));
System.out.println( encodeNaviStr("a10a25a337", "2", "35"));
}
2005年10月20日
昨天,完成了导航过程中封装字符串处理方法,今天,继续完成把封装的字符串转化为下一个页面显示导航条所需要的字符串的方法。
如何把封装字符串“a10a25a337”转化为页面导航显示所需要的字符串:
系统导航:<a href="http://www.learndiary.com/indexAction.do?pageNum=1">所有目标</a>-》<a href="http://www.learndiary.com/toDiaryAction.do?goalID=5&naviStr=a10>目标:“建设“学习日记””的日记列表</a>─》<a href="http://www.learndiary.com/disDiaryContentAction.do?goalID=73&naviStr=a10a25>日记:用cookie保存用户密码登录流程</a>->撰写评论(当前页面的导航条目灰化,不加超链接)
?
显示这个字符串的要点:
1、页面的类型:根据参数页面类型查找;
2、标题:根据页面参数确定;
3、第一个节点总是所有目标,最后一个节点不加超链接。
实现方法分析:
把“参数”和“页面标题”的对应关系做成一个map,用时在map里面查找。
通过LearndiaryDB.getArtInfoByID(int articleID, int htmlFlag)得到页面导航所需的标题。
为了方便处理,把每一个节点读入一个PageNavNode对象,然后用一个LinkedList来容纳这些对象。这样就可以自由处理其中的内容了。
public String
/**
* receive the encoding navigation string and get the navigation string which will be displayed in the page which we are going to.
* @param naviStr the encoding navigation string will be keeped in the request for the page which we are going to.
* @return the navigation string which will be displayed in the page which we are going to.
*/
public static String decodeNaviStr(String naviStr){
String navigation ="系统导航:";
String[] strArray = naviStr.split("a");
PageNaviNode[] nodesArray = new PageNaviNode[strArray.length()-1];
for (i=1;i<strArray.length();i++) {
nodeArray[i-1] = strToPageNode(strArray[i]);
}
if (nodeArray.length()==1){
navigation = navigation.concat("所有目标");
}
return navigation;
}
关于导航能力改进的代码,今天向http://learndiary.tigris.org中的cvs库作了第一次代码的提交。页面上基本上没有什么反映。
2005年10月21日
we can do something out of network
for example:we can orgernize the members in same city hold party every year.And,invite some learning experts make some speech about learning.etc..
in order to make our website going on(better ISP service),we can build some business about learning service,for example,sale books,cd,and recieve some software developing task.
learn from www.43things.com
www.43things.com and www.43places.com and http://www.43things.com,a good ideas of sharing people's goal and interesting,I have registered in this site,I will use and study their good character and import some into our project-Learndiay.
application is first
I have found maybe we should reslove some application problems of this website at first.e.g.,improve the priority of this kind of issues.
This need exchanging information with other members.
give the freedom to user,form group spontaneously
1.with the XML technique,the articles of a user can be process freely;
2.a user can build a group with several goals spontaneously,like QQ group;
3.before a user creating a goal really,a searching process should be taken for avoiding some duplicate goal;
4.Maybe,if a user build a group,a approving process should be taken for avoiding some duplicate or improper group to be found.
欣赏一篇关于开源软件的好帖(网友推荐转帖)
前车之覆,后车之鉴 --开源项目经验谈
qinpt 转贴 (参与分:26557,专家分:2297) 发表:2005-08-01 18:08 版本:1.0 阅读:215次
(本文发表于《程序员》2005年第2期)
随着开源文化的日益普及,“参与开源”似乎也变成了一种时尚。一时间,似乎大家都乐于把自己的代码拿出来分享了。就在新年前夕,我的一位老朋友、一位向来对开源嗤之以鼻的J2EE架构师竟然也发布了一个开源的J2EE应用框架(姑且称之为“X框架”),不得不令我惊叹开源文化的影响力之强大。
可惜开源并非免费的午餐,把源码公开就意味着要承受众目睽睽的审视。仅仅几天之后,国内几位资深的J2EE架构师就得出一个结论:细看之下,X框架不管从哪个角度都只能算一个失败的开源项目。究竟是什么原因让一个良好的愿望最终只能得到一个失败的结果?本文便以X框架为例,点评初涉开源的项目领导者常犯的一些错误,指出投身开源应当遵循的一些原则,为后来的开源爱好者扫清些许障碍。
成熟度
打开X框架在SourceForge的项目站点,我们立刻可以看到:在“Development Status”一栏赫然写着“5 ? Production/Stable”。也就是说,作者认为X框架已经成熟稳定,可以交付用户使用。那么,现在对其进行评估便不应该有为时过早之嫌。可是,X框架真的已经做好准备了吗?
打开从SourceForge下载的X框架的源码包,笔者不禁大吃一惊:压缩包里真的只有源码??编译、运行整个项目所需的库文件全都不在其中。从作者自己的论坛得知,该项目需要依赖JBoss、JDOM、Castor、Hibernate等诸多开源项目,笔者只好自己动手下载了这些项目,好一番折腾总算是在Eclipse中成功编译了整个项目。
不需要对开源文化有多么深刻的了解,只要曾经用过一些主流的开源产品,你就应该知道:一个开源软件至少应该同时提供源码发布包和二进制发布包,源码包中至少应该有所有必需的依赖库文件(或者把依赖库单独打包发布)、完整的单元测试用例(对于Java项目通常是Junit测试套件)、以及执行编译构建的脚本(对于Java项目通常是Ant脚本或者Maven脚本),但这些内容在X框架的发布包中全都不见踪影。用户如果想要使用这个框架,就必须像笔者一样手工下载所有的依赖库,然后手工完成编译和构建,而且构建完成之后也无从知晓其中是否有错误存在(因为没有单元测试)。这样的发布形式,算得上是“Production/Stable”吗?
开源必读:便捷构建
开源软件应该提供最便捷的构建方式,让用户可以只输入一条命令就完成整个项目的编译、构建和测试,并得到可运行的二进制程序。对于Java项目,这通常意味着提供完整的JUnit测试套件和Ant脚本。你的潜在用户可能会在一天之内试用所有类似的开源软件,如果一个软件需要他用半天时间才能完成构建、而且还无从验证正确性、无从着手编写他自己的测试用例,这个软件很可能在第一时间被扔到墙角。
从SourceForge的项目页面可以看到,X框架的授权协议是Apache License V2.0(APL)。然而在它的发布包中,笔者没有看到任何形式的正式授权协议文本。众所周知,SourceForge的项目描述是可以随时修改的(X框架本身的授权协议就曾经是GPL),如果发布包中没有一份正式的授权协议文本,一旦作者修改了SourceForge的项目描述,用户又该到哪里去寻找证据支持自己的合法使用呢?
在X框架的源码中,大部分源文件在开始处加上了APL的授权声明,但有一部分源码很是令人担心。例如UtilCache这个类,开始处没有任何授权声明,而JavaDoc中则这样声明作者信息:
@author <a href="mailto:jonesde@ofbiz.org">David E. Jones</a>
也就是说,这个类的源码来自另一个开源项目Ofbiz。值得一提的是,Ofbiz一直是“商业开源”的倡导者,它的授权协议相当严格。凡是使用Ofbiz源码,必须将它的授权协议一并全文复制。像X框架这样复制Ofbiz源码、却删掉了授权协议的行为,实际上已经构成了对Ofbiz的侵权。
另外,作者打包用的压缩格式是RAR,而这个压缩格式对于商业用户是收费的。对于一个希望在商业项目中应用的框架项目来说,选择这样一个压缩格式实在算不得明智。而且笔者在源码包中还看到了好几个.jbx文件,这是JBuilder的项目描述文件。把这些JBuilder专用的文件放在源码包中,又怎能让那些买不起或是不想买JBuilder的用户放心呢?更何况,出于朋友的关心,笔者还不得不担心X框架的作者是否会收到Borland公司的律师信呢。
开源必读:授权先行
在启动一个开源项目时,第一件大事就是要确定自己的授权协议,并在最醒目的地方用最正式的方式向所有人声明??当然,在此之前你必须首先了解各种开源授权协议。譬如说,GPL(Linux采用的授权协议)要求在软件之上的扩展和衍生也必须继承GPL,因此这种协议对软件的商业化应用很不友好;相反,APL则允许用户将软件的扩展产物私有化,便于商业应用,却不利于开发者社群的发展。作为一个开源项目的领导者,对于各种授权协议的利弊是不可不知的。
除了源码本身的授权协议之外,软件需要使用的类库、IDE、解压工具等等都需要考虑授权问题。开源绝对不仅仅意味着“免费使用”,开源社群的人们有着更加强烈的版权意识和法律意识。如果你的开源软件会给用户带来潜在的法律麻烦,它离着被抛弃的命运也就不远了。
可以看到,不管从法律的角度还是从发布形式的角度,X框架都远够不上“Production/Stable”的水准??说实在的,以它的成熟度,顶多只能算是一个尚未计划周全的开源项目。虽然作者在自己的网站上大肆宣传,但作为一个潜在的用户,我不得不冷静地说:即便X框架的技术真的能够吸引我,但它远未成熟的项目形态决定了它根本无法在任何有实际意义的项目中运用。要让商业用户对它产生兴趣,作者需要做的工作还很多。
我刚才说“即便X框架的技术真的能够吸引我”,这算得上是一个合理的假设吗?下面,就让我们进入这个被作者寄予厚望的框架内部,看看它的技术水平吧。
整体架构
在X框架的宣传页面上,我们看到了这样的宣传词:
X框架解决了以往J2EE开发存在的诸多问题:EJB难用、J2EE层次复杂、DTO太乱、Struts绕人、缓存难做性能低等。X框架是Aop/Ico[注:应为“IoC”,此处疑似笔误]的实现,优异的缓存性能是其优点。
下面是X框架的整体架构图:
可以看到,在作者推荐的架构中,EJB被作为业务逻辑实现的场所,而POJO被用于实现Fa?ade。这是一个好的技术架构吗?笔者曾在一篇Blog中这样评价它[1]:
让我们先回想一下,使用EJB的理由是什么?常见的答案有:可分布的业务对象;声明性的基础设施服务(例如事务管理)。那么,如果在EJB的上面再加上一 层POJO的Fa?ade,显然你不能再使用EJB的基础设施了,因为完整的业务操作(也就是事务边界)将位于POJO Fa?ade的方法这里,所以你必须重新??以声明性的方式??实现事务管理、安全性管理、remoting、缓存等基础设施服务。换句话说,你失去了 session bean的一半好处。另一方面,“可分布的业务对象”也不复存在,因为POJO本身是不能??像EJB那样??分布的,这样你又失去了session bean的另一半好处。
继续回想,使用基于POJO的轻量级架构的理由是什么?常见的答案有:易于测试;便于移植;“开发-发布”周期短。而如果仅仅把POJO作为一层Fa?ade,把业务逻辑放在下面的EJB,那么你仍然无法轻易地测试业务逻辑,移植自然也无从谈起了,并且每次修改EJB之后必须忍受漫长的发布周期。 即便是仅仅把EJB作为O/R mapping,而不是业务逻辑的居所,你最多只能通过DAO封装获得比较好的业务可测性,但“修改-发布”的周期仍然很长,因为仍然有entity bean存在。也就是说,即使是往最好的方面来说,这个架构至少损失了轻量级架构的一半优点。
作为一个总结,X框架即便是在使用得最恰当的情况下,它仍然不具备轻量级架构的全部优点,至少会对小步前进的敏捷开发造成损害(因为EJB的存在),并且没有Spring框架已经实现的基础设施(例如事务管理、remoting 等),必须重新发明这些轮子;另一方面,它也不具备EJB的任何优点,EJB的声明性基础设施、可分布业务对象等能力它全都不能利用。因此,可以简单地总结说,X框架是一个这样的架构:它结合了EJB和轻量级架构两者各自的短处,却抛弃了两者各自的长处。
在不得不使用EJB的时候,一种常见的架构模式是:用session bean作为Fa?ade,用POJO实现可移植、可测试的业务逻辑。这种模式可以结合EJB和POJO两者的长处。而X框架推荐的架构模式,虽然乍看起来也是依葫芦画瓢,效果却恰恰相反,正可谓是“取其糟粕、去其精华”。
开源必读:架构必须正确
在开源软件的初始阶段,功能可以不完善,代码可以不漂亮,但架构思路必须是正确的。即使你没有完美的实现,参与开源的其他人可以帮助你;但如果架构思路有严重失误,谁都帮不了你。从近两年容器项目的更迭就可以看出端倪:PicoContainer本身只有20个类、数百行代码,但它有清晰而优雅的架构,因此有很多人为它贡献外围的功能;Avalon容器尽管提供了完备的功能,但架构的落伍迫使Apache基金会只能将其全盘废弃。
所以如果你有志于启动一个开源项目(尤其是框架性的项目),务必先把架构思路拿出来给整个社群讨论。只要大家都认可你的架构,你就有机会得到很多的帮助;反之,恐怕你就只能得到无尽的嘲讽了。
技术细节
既然整体架构已经无甚可取之处,那么X框架的实现是否又像它所宣称的那样,能够解决诸多问题呢?既然X框架号称是“AOP/IoC的实现”,我们就选中这两项技术,看看它们在X框架中的实现和应用情况。
IoC
X框架宣称自己是一个“基于IoC的应用框架”。按照定义,框架本身就具有“业务代码不调用框架,框架调用业务代码”的特性,因此从广义上来说,所有的框架必然是基于IoC模式的。所以,在框架这里,“基于IoC”通常是特指“对象依赖关系的管理和组装基于IoC”,也就是Martin Fowler所说的Dependency Injection模式[2]:由容器统一管理组件的创建和组装,组件本身不包含依赖查找的逻辑。那么,X框架实现IoC的情况又如何呢?
我们很快找到了ContainerWrapper这个接口,其中指定了一个POJO容器核心应该具备的主要功能:
public interface ContainerWrapper {
public void registerChild(String name);
public void register(String name, Class className);
public void register(String name, Class className, Parameter[] parameters);
public void register(String name, Object instance);
public void start();
public void stop();
public Collection getAllInstances();
public Object lookup(String name);
}
在这个接口的默认实现DefaultContainerWrapper中,这些功能被转发给PicoContainer的对应方法。也就是说,X框架本身并没有实现组件容器的功能,这部分功能将被转发给其他的IoC组件容器(例如PicoContainer、Spring或HiveMind等)来实现。在ContainerWrapper接口的注释中,我们看到了一句颇可玩味的话:
/**
* 封装了Container,解耦具体应用系统和PicoContainer关系。
了解IoC容器的读者应该知道,在使用PicoContainer或Spring等容器时,绝大多数POJO组件并不需要对容器有任何依赖:它们只需要是最普通的JavaBean,只需要实现自己的业务接口。既然对容器没有依赖,自然也不需要“解耦”。至于极少数需要获得生命周期回调、因此不得不依赖容器的组件,让它们依赖PicoContainer和依赖X框架难道有什么区别吗?更何况,PicoContainer是一个比X框架更成熟、更流行的框架,为什么用户应该选择X框架这么一个不那么成熟、不那么流行的框架夹在中间来“解耦”呢?
不管怎么说,至少我们可以看到:X框架提供了组件容器的核心功能。那么,IoC(或者说,Dependency Injection)在X框架中的应用又怎么样呢?众所周知,引入IoC容器的目标就是要消除应用程序中泛滥的工厂(包括Service Locator),由容器统一管理组件的创建和组装。遗憾的是,不论在框架内部还是在示例应用中,我们仍然看到了大量的工厂和Service Locator。例如作者引以为傲的缓存部分,具体的缓存策略(即Cache接口的实现对象)就是由CacheFactory负责创建的,并且使用的实现类还是硬编码在工厂内部:
public CacheFactory() {
cache = new LRUCache();
也就是说,如果用户需要改变缓存策略,就必须修改CacheFactory的源代码??请注意,这是一个X框架内部的类,用户不应该、也没有能力去修改它。换句话说,用户实际上根本无法改变缓存策略。既然如此,那这个CacheFactory又有什么用呢?
开源必读:开放-封闭原则
开源软件应该遵守开放-封闭原则(Open-Close Principle,OCP):对扩展开放,对修改封闭。如果你希望为用户提供任何灵活性,必须让用户以扩展(例如派生子类或配置文件)的方式使用,不能要求(甚至不能允许)用户修改源代码。如果一项灵活性必须通过修改源码才能获得,那么它对于用户就毫无意义。
在示例应用中,我们同样没有看到IoC的身影。例如JdbcDAO需要使用数据源(即DataSource对象),它就在构造子中通过Service Locator主动获取这个对象:
public JdbcDAO() {
ServiceLocator sl = new ServiceLocator();
dataSource = (DataSource) sl.getDataSource(JNDINames.DATASOURCE);
同样的情况也出现在JdbcDAO的使用者那里。也就是说,虽然X框架提供了组件容器的功能,却没有(至少是目前没有)利用它的依赖注入能力,仅仅把它作为一个“大工厂”来使用。这是对IoC容器的一种典型的误用:用这种方式使用容器,不仅没有获得“自动管理依赖关系”的能力,而且也失去了普通Service Locator“强类型检查”的优点,又是一个“取其糟粕、去其精华”的设计。
开源必读:了解你自己
当你决定要在开源软件中使用某项技术时,请确定你了解它的利弊和用法。如果仅仅为了给自己的软件贴上“基于xx技术”的标签而使用一种自己不熟悉的技术,往往只会给你的项目带来负面的影响。
AOP
在X框架的源码包中,我们找到了符合AOP-Alliance API的一些拦截器,例如用于实现缓存的CacheInterceptor。尽管??毫不意外地??没有找到如何将这些拦截器织入(weave in)的逻辑或配置文件,但我们毕竟可以相信:这里的确有AOP的身影。可是,甫一深入这个“基于AOP的缓存机制”内部,笔者却又发现了更多的问题。
单从CacheInterceptor的实现来看,这是一个最简单、也最常见的缓存拦截器。它拦截所有业务方法的调用,并针对每次方法调用执行下列逻辑:
IF 需要缓存
key = (根据方法签名生成key);
IF (cache.get(key) == null)
value = (实际调用被拦截方法);
cache.put(key, value);
RETURN (cache.get(key));
ELSE
RETURN (实际调用被拦截方法);
看上去很好,基于AOP的缓存实现就应该这么做……可是,清除缓存的逻辑在哪里?如果我们把业务方法分为“读方法”和“写方法”两种,那么这个拦截器实际上只照顾了“读方法”的情况。而“写方法”被调用时会改变业务对象的状态,因此必须将其操作的业务对象从缓存中清除出去,但这部分逻辑在CacheInterceptor中压根不见踪影。如果缓存内容不能及时清理的话,用户从缓存中取出的信息岂不是完全错误的吗?
被惊出一身冷汗之后,笔者好歹还是从几个Struts action(也就是调用POJO Fa?ade的client代码)中找到了清除缓存的逻辑。原来X框架所谓“基于AOP的缓存机制”只实现了一条腿:“把数据放入缓存”和“从缓存中取数据”的逻辑确实用拦截器实现了,但“如何清除失效数据”的逻辑还得散布在所有的客户代码中。AOP原本就是为了把缓存这类横切性(crosscutting)的基础设施逻辑集中到一个模块管理,像X框架的这个缓存实现,不仅横切性的代码仍然四下散布,连缓存逻辑的相关性和概念完整性都被打破了,岂不是弄巧成拙么?
开源必读:言而有信
如果你在宣传词中承诺了一项特性,请务必在你的软件中完整地实现它。不要仅仅提供一个半吊子的实现,更不要让你的任何承诺放空。如果你没有把握做好一件事,就不要承诺它。不仅对于开源软件,对于任何软件开发,这都是应该记住的原则。
更有趣的是,X框架的作者要求领域模型对象继承Model基类,并声称这是为了缓存的需要??事实也的确如此:CacheInterceptor只能处理Model的子对象。但只要对缓存部分的实现稍加分析就会发现,这一要求完全是作者凭空加上的:用于缓存对象的Cache接口允许放入任何Object;而Model尽管提供了setModified()、setCacheable()等用于管理缓存逻辑的方法,却没有任何代码调用它们。换句话说,即便我们修改CacheInterceptor,使其可以缓存任何Object,对X框架目前的功能也不会有任何影响。既然如此,又为什么要给用户凭空加上这一层限制呢?
退一万步说,即使我们认为X框架今后会用Model的方法来管理缓存逻辑,这个限制仍然是理由不足的。毕竟,目前X框架还仅仅提供了缓存这一项基础设施(infrastructure)而已。如果所有基础设施都用“继承一个基类”的套路来实现,当它真正提供企业级应用所需的所有基础设施时,Model类岂不是要变得硕大无朋?用户的领域对象岂不是再也无法移植到这个框架之外?况且,“由领域对象判断自己是否需要缓存”的思路本身也是错误的:如果不仅要缓存领域对象,还要缓存String、Integer等简单对象,该怎么办?如果同一个领域对象在不同的方法中需要不同的缓存策略,又该怎么办?X框架的设计让领域对象背负了太多的责任,而这些责任原本应该是通过AOP转移到aspect中的。在X框架这里,AOP根本没有发挥它应有的效用。
开源必读:避免绑定
开源软件(尤其是框架类软件)应该尽量避免对你的用户造成绑定。能够在POJO上实现的功能,就不要强迫用户实现你的接口;能够通过接口实现的功能,就不要强迫用户继承你的基类。尤其是Java语言只允许单根继承,一旦要求用户的类继承框架基类,那么前者就无法再继承其他任何基类,这是一种非常严重的绑定,不论用户和框架设计者都应当极力避免。
写在最后
看完这篇多少有些尖刻的批评,恐怕读者难免要怪责我“不厚道”??毕竟,糟糕的开源软件堪比恒河沙数,为什么偏要选中X框架大加挞伐呢?在此,我要给各位读者、各位有志于开源的程序员一个最后、却是最重要的建议:
开源必读:切忌好大喜功
开源是一件长期而艰巨的工作,对于只能用业余时间参与的我们更是如此。做开源务必脚踏实地,做出产品首先在小圈子里内部讨论,然后逐渐扩大宣传的圈子。切勿吹大牛、放卫星,把“未来的愿景”当作“今天的承诺”来说??因为一旦工作忙起来,谁都不敢保证这个愿景到哪天才能实现。
国人还有个爱好:凡事喜欢赶个年节“献礼”,或是给自己绑上个“民族软件”的旗号,这更是开源的大忌。凡是做过政府项目的程序员,想必都对“国庆献礼”、“新年献礼”之类事情烦不胜烦,轮到自己做开源项目时,又何苦把自己套进这个怪圈里呢?当然,如果你的开源项目原本就是做给某些官老爷看的,那又另当别论。
所以,我的这位朋友怕也不能怪我刻薄:要不是他紧赶着拿出个远未完善的版本“新年献礼”,要不是他提前放出“AOP/IoC”的卫星,要不是他妄称这个框架“代表民族软件水平”,或许我还会夸他的代码颇有可看之处呢。有一句大家都熟悉的老话,笔者私以为所有投身开源者颇可借鉴,在此与诸位共勉:
长得丑不是你的错……
we can do our program as a plugin
This program can be easy to install on another's website.
and we can import and export everyone's entire or part of articles with XML technique.XML is must be used!
And,all kinds of documentation in our developing website should use XML as well,like http://argouml.tigris.org.In this way,the content can be communicated between any program which can support XML.
I insist an idea:the idea is first,and the technique is second!
Do you agree with me?
Please give your idea about what function should be included in our website(program).
The ideas we can refer:www.43things.com
Before yesterday,my friend TangYong told me there are some web site like ours.He gave me some link:
http://www.blogcn.com/user14/davidczg/blog/22723065.html
http://www.43things.com/
http://www.aimi.cn/
http://www.sharethings.com.cn/
I found these website are like ours very much:guiding with a self-selected goal,and sharing processing with diary.Instead of we are focusing on the learning,their scope cover all things(all goals) that we will to do.and,they use Tag,XML,etc. technique.
I am very happy there are another people are interesting this style,and,we can learn them,exchange with them.
I think,we will own good future if we do this with my heart and brain,eyes,hands.
import the source code to our cvs repository
I have fixed the structure of our cvs repository after consulting a friend of LDDG.
yesterday,I have sent the notice of restarting the development of our Learndiary Open Source Project to the friends of LDDG.