每日一得

不求多得,只求一得 about java,hibernate,spring,design,database,Ror,ruby,快速开发
最近关心的内容:SSH,seam,flex,敏捷,TDD
本站的官方站点是:颠覆软件

  BlogJava :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理 ::
  220 随笔 :: 9 文章 :: 421 评论 :: 0 Trackbacks

#

     摘要: 终于翻开这本James都称赞的java经典书籍了,发现比一般的英语书籍要难懂一些。但是里面的Item都是非常实用的,是java程序员应该理解的。 Creating and Destroying ObjectItem 1:考虑用静态工厂方法替代构造器例如:public static Boolean valueOf(boolean b)     {          return (b?Boolean...  阅读全文
posted @ 2006-09-11 18:14 Alex 阅读(671) | 评论 (0)编辑 收藏

从工作流状态机实践中总结状态模式使用心得

banq http://www.jdon.com Dec 7, 2003 12:10 AM 回复此消息回复

状态模式好像是很简单的模式,正因为状态好像是个简单的对象,想复杂化实现设计模式就不是容易,误用情况很多。

我个人曾经设计过一个大型游戏系统的游戏状态机,游戏状态可以说是游戏设计的主要架构,但是由于系统过分复杂
和时间仓促,并没有真正实现状态模式。

目前在实现一个电子政务项目中,需要进行流程状态变化,在电子政务设计中,我发现,如果一开始完全按照工作流
规范开发,难度很大,它和具体项目实践结合无法把握,而且工作流规范现在有wfmc,还有bpml,选择也比较难。因
此,我决定走自创的中间道路。

因为,我需要做一个状态机API,或者说状态机框架,供具体系统调用:类如公文流转应用或信息发报送应用等。

好的状态模式必须做到两点:
1. 状态变化必须从外界其它逻辑划分出来。
2. 状态必须可方便拓展,对其它代码影响非常小。

要做到这两点,必须先明确状态变化机制,状态变化实际是由Event事件驱动的,可以认为是Event-condition-State,
在MVC模式一般是Event-condition-Action实现。状态模式需要封装的是Event-condition-State中的condition-State
部分。

清晰理解状态和流程的关系也非常重要,因为状态不是孤立的,可以说和流程是点和线的关系,状态从一个方面说明
了流程,流程是随时间而改变,状态是截取流程某个时间片。因此,必须明白使用状态模式实现状态机实际是为了更
好地表达和说明流程。

状态和流程以及事件的关系如下:


|Event
___currentState__|______newState___




图中表示了是事件改变了流程的状态,在业务逻辑中,经常发生的是事件,如果不使用状态模式,需要在很多业务逻
辑处实现事件到状态判定和转换,这有很多危险性。

最大的危险是系统没有一个一抓就灵的主体结构,以那个游戏系统为例,在没有状态模式对状态提炼的情况下,状态
改变由每个程序员想当然实现,导致每个程序员开发的功能在整合时就无法调试,因为这个程序员可能不知道那个程
序员的代码在什么运行条件下改变了游戏状态,结果导致自己的代码无法运行。

这种现象实际上拒绝了项目管理的协作性,大大地拖延项目进度(程序员之间要反复商量讨论对方代码设计)。从这
一点也说明,一个好的架构设计是一个项目快速成功完成的基础技术保证,没有这个技术基础,再先进的项目管理手
段也是没有效率的,或者是笨拙的。

状态模式对于很多系统来说,确实是架构组成一个重要部分。

下面继续讨论如何实现一个好的状态模式,为了实现好的状态模式,必须在状态模式中封装下面两个部分:
1. 状态转换规则(行为)
2. 状态属性(属性)

状态转换行为有两种划分标准:
1. run和next两个行为,run是当前状态运行行为,next是指在Event参与下,几种可能转向的下一个状态。
2. stateEnter和stateExit, 状态进入和状态退出。

如果用进入一个个房间来表示状态流程的话, 第一种分析是只重视着“在房间里”和“如何转入下一个房间”,这两种行
为一旦确定,可以被反复使用,进而一个流程的状态切换可以全部表达出来。

第二中分析方法有所区别,只重视进入房间和离开房间这个两个行为,同样,这种模型也可以被反复利用在其它房间,
一个流程的状态切换也可以全部表达出来。

具体选择取决于你的需求,比如,如果你在进入一个状态开始,要做很多初始化工作,那么第二种模型就很适合。

状态变化都离不开一个主体对象,主体对象可以说包含状态变化(行为)和状态属性(属性),假设为StateOwner,
StateOwner有两个部分组成:Task/事情和状态。任何一个Task/事情都会对应一个状态。

这样,我们已经抽象出两个主要对象:状态State和StateOwner。

为了封装状态变化细节,我们可以抽象出一个状态机StateMachine来专门实现状态根据事情实现转换。

这样,客户端外界通过状态机可访问状态模式这个匣子。在实践中,外界客户端需要和状态机实现数据交换,我们把
它也分为两种:属性和行为。

其中属性可能需要外界告诉状态状态变化的主体对象Task,解决状态的主人问题,是谁的问题;行为可能是需要持久
化当前这个主体对象的状态到数据库。

这两种数据交换可以分别通过StateOwner和StateMachine与整个状态机实现数据交换,这样,具体状态和状态切换也
和外界实现了解耦隔离。

因此好的状态模式实现必须有下列步骤:
(1)将每个状态变成State的子类,实现每个状态对象化。
(2)在每个状态中,封装着进入下一个状态可能规则,这些规则是状态变化的核心,换句话说,统一了状态转换的规则。
具体可采取run和next这样的转换行为。

下面是一个子状态代码:


publicclass Running extends StateT{

//
publicvoid run(StateOwner stateOwner){
stateOwner.setCurrentState(this);
}

//转换到下一个状态的规则
//当前是Running状态,下一个状态可能是暂停、结束或者强制退出等
//状态,但是绝对不会是Not_Started这样的状态
//转换规则在这里得到了体现。
public State next(Event e) {
if(transitions == null){
addEventState(new EventImp(
"PAUSE"), new Suspended());
addEventState(new EventImp(
"END"), new Completed());
addEventState(new EventImp(
"STOP"), new Aborted());
}
returnsuper.next(e);
}

}



外界直接调用 StateMachine的关键方法transition;实行状态的自动转变。


publicclass StateMachine {

/**
* 状态切换
* 根据Event参数,运行相应的状态。
* 1. 获得当前状态
* 2. 使用当前状态的next()转换
* |Event
* ___currentState__|______newState___
*
* @param inputs
*/

publicfinalvoid transition(String taskid, Event e) throws Exception {
State currentState = readCurrentState(taskid);
//从数据库获得当前状态
StateOwner stateOwner = new StateOwner(taskid, currentState);
//转换状态
currentState = currentState.next(e);
if (currentState != null) {
currentState.run(stateOwner);
saveCurrentState(stateOwner);
//保存当前状态
}
}

}
posted @ 2006-09-09 10:43 Alex 阅读(967) | 评论 (0)编辑 收藏

设计模式之State

板桥里人 http://www.jdon.com 2002/4/6/

模式实战书籍《Java实用系统开发指南》

State模式的定义 : 不同的状态,不同的行为;或者说,每个状态有着相应的行为.

何时使用 ?
State模式在实际使用中比较多,适合"状态的切换".因为我们经常会使用If elseif else 进行状态切换, 如果针对状态的这样判断切换反复出现,我们就要联想到是否可以采取State模式了.

不 只是根据状态,也有根据属性.如果某个对象的属性不同,对象的行为就不一样,这点在数据库系统中出现频率比较高,我们经常会在一个数据表的尾部,加上 property属性含义的字段,用以标识记录中一些特殊性质的记录,这种属性的改变(切换)又是随时可能发生的,就有可能要使用State.

是否使用?
在实际使用,类似开关一样的状态切换是很多的,但有时并不是那么明显,取决于你的经验和对系统的理解深度.

这里要阐述的是"开关切换状态" 和" 一般的状态判断"是有一些区别的, " 一般的状态判断"也是有 if..elseif结构,例如:

    if (which==1) state="hello";
    else if (which==2) state="hi";
    else if (which==3) state="bye";

这是一个 " 一般的状态判断",state值的不同是根据which变量来决定的,which和state没有关系.如果改成:

    if (state.euqals("bye")) state="hello";
    else if (state.euqals("hello")) state="hi";
    else if (state.euqals("hi")) state="bye";

这就是 "开关切换状态",是将state的状态从"hello"切换到"hi",再切换到""bye";在切换到"hello",好象一个旋转开关,这种状态改变就可以使用State模式了.

如 果单纯有上面一种将"hello"-->"hi"-->"bye"-->"hello"这一个方向切换,也不一定需要使用State模 式,因为State模式会建立很多子类,复杂化,但是如果又发生另外一个行为:将上面的切换方向反过来切换,或者需要任意切换,就需要State了.

请看下例:

public class Context{

  private Color state=null;

  public void push(){

    //如果当前red状态 就切换到blue
    if (state==Color.red) state=Color.blue;

    //如果当前blue状态 就切换到green
    else if (state==Color.blue) state=Color.green;

    //如果当前black状态 就切换到red
    else if (state==Color.black) state=Color.red;

    //如果当前green状态 就切换到black
    else if (state==Color.green) state=Color.black;
    
    Sample sample=new Sample(state);
    sample.operate();
  }

  public void pull(){

    //与push状态切换正好相反

    if (state==Color.green) state=Color.blue;
    else if (state==Color.black) state=Color.green;
    else if (state==Color.blue) state=Color.red;
    else if (state==Color.red) state=Color.black;

    Sample2 sample2=new Sample2(state);
    sample2.operate();
  }

}

在上例中,我们有两个动作push推和pull拉,这两个开关动作,改变了Context颜色,至此,我们就需要使用State模式优化它.

另外注意:但就上例,state的变化,只是简单的颜色赋值,这个具体行为是很简单的,State适合巨大的具体行为,因此在,就本例,实际使用中也不一定非要使用State模式,这会增加子类的数目,简单的变复杂.

例如: 银行帐户, 经常会在Open 状态和Close状态间转换.

例如: 经典的TcpConnection, Tcp的状态有创建 侦听 关闭三个,并且反复转换,其创建 侦听 关闭的具体行为不是简单一两句就能完成的,适合使用State

例如:信箱POP帐号, 会有四种状态, start HaveUsername Authorized quit,每个状态对应的行为应该是比较大的.适合使用State

例如:在工具箱挑选不同工具,可以看成在不同工具中切换,适合使用State.如 具体绘图程序,用户可以选择不同工具绘制方框 直线 曲线,这种状态切换可以使用State.

如何使用
State需要两种类型实体参与:

1.state manager 状态管理器 ,就是开关 ,如上面例子的Context实际就是一个state manager, 在state manager中有对状态的切换动作.
2.用抽象类或接口实现的父类,,不同状态就是继承这个父类的不同子类.

以上面的Context为例.我们要修改它,建立两个类型的实体.
第一步: 首先建立一个父类:

public abstract class State{

  public abstract void handlepush(Context c);
  public abstract void handlepull(Context c);
  public abstract void getcolor();

}

父类中的方法要对应state manager中的开关行为,在state manager中 本例就是Context中,有两个开关动作push推和pull拉.那么在状态父类中就要有具体处理这两个动作:handlepush() handlepull(); 同时还需要一个获取push或pull结果的方法getcolor()

下面是具体子类的实现:

public class BlueState extends State{

  public void handlepush(Context c){
     //根据push方法"如果是blue状态的切换到green" ;
     c.setState(new GreenState());

  }
  public void handlepull(Context c){

     //根据pull方法"如果是blue状态的切换到red" ;
    c.setState(new RedState());

  }

  public abstract void getcolor(){ return (Color.blue)}

}

 

同样 其他状态的子类实现如blue一样.

第二步: 要重新改写State manager 也就是本例的Context:

public class Context{

  private Sate state=null; //我们将原来的 Color state 改成了新建的State state;

  //setState是用来改变state的状态 使用setState实现状态的切换
  pulic void setState(State state){

    this.state=state;

  }

  public void push(){

    //状态的切换的细节部分,在本例中是颜色的变化,已经封装在子类的handlepush中实现,这里无需关心
    state.handlepush(this);
    
    //因为sample要使用state中的一个切换结果,使用getColor()
    Sample sample=new Sample(state.getColor());
    sample.operate();

  }

 

  public void pull(){

    state.handlepull(this);
    
    Sample2 sample2=new Sample2(state.getColor());
    sample2.operate();

  }

}

 

至此,我们也就实现了State的refactorying过程.

以上只是相当简单的一个实例,在实际应用中,handlepush或handelpull的处理是复杂的.

状态模式优点:
(1) 封装转换过程,也就是转换规则
(2) 枚举可能的状态,因此,需要事先确定状态种类。

状态模式可以允许客户端改变状态的转换行为,而状态机则是能够自动改变状态,状态机是一个比较独立的而且复杂的机制,具体可参考一个状态机开源项目:http://sourceforge.net/projects/smframework/

状态模式在工作流或游戏等各种系统中有大量使用,甚至是这些系统的核心功能设计,例如政府OA中,一个批文的状态有多种:未办;正在办理;正在批示;正在审核;已经完成等各种状态,使用状态机可以封装这个状态的变化规则,从而达到扩充状态时,不必涉及到状态的使用者。

在网络游戏中,一个游戏活动存在开始;开玩;正在玩;输赢等各种状态,使用状态模式就可以实现游戏状态的总控,而游戏状态决定了游戏的各个方面,使用状态模式可以对整个游戏架构功能实现起到决定的主导作用。

状态模式实质
使用状态模式前,客户端外界需要介入改变状态,而状态改变的实现是琐碎或复杂的。

使用状态模式后,客户端外界可以直接使用事件Event实现,根本不必关心该事件导致如何状态变化,这些是由状态机等内部实现。

这是一种Event-condition-State,状态模式封装了condition-State部分。

每个状态形成一个子类,每个状态只关心它的下一个可能状态,从而无形中形成了状态转换的规则。如果新的状态加入,只涉及它的前一个状态修改和定义。

状态转换有几个方法实现:一个在每个状态实现next(),指定下一个状态;还有一种方法,设定一个StateOwner,在StateOwner设定stateEnter状态进入和stateExit状态退出行为。

状态从一个方面说明了流程,流程是随时间而改变,状态是截取流程某个时间片。


相关文章:

从工作流状态机实践中总结状态模式使用心得

参考资源:
the State and Stategy
How to implement state-dependent behavior
The state patterns

 

posted @ 2006-09-09 10:42 Alex 阅读(632) | 评论 (0)编辑 收藏

转自

.Net中的设计模式——Composite模式

前言:google到一篇关于复合模式的文章,虽然是关于 .NET的,但是对于开发java同样有借鉴意义.

一、模式概述

描述Composite模式的最佳方式莫过于树形图。从抽象类或接口为根节点开始,然后生枝发芽,以形成树枝节点和叶结点。因此, Composite模式通常用来描述部分与整体之间的关系,而通过根节点对该结构的抽象,使得客户端可以将单元素节点与复合元素节点作为相同的对象来看 待。

由于Composite模式模糊了单元素和复合元素的区别,就使得我们为这些元素提供相关的操作时,可以有一个统一的接口。例如,我们要编写一个字 处理软件,该软件能够处理文字,对文章进行排版、预览、打印等功能。那么,这个工具需要处理的对象,就应该包括单个的文字、以及由文字组成的段落,乃至整 篇文档。这些对象从软件处理的角度来看,对外的接口应该是一致的,例如改变文字的字体,改变文字的位置使其居中或者右对齐,也可以显示对象的内容,或者打 印。而从内部实现来看,我们对段落或者文档进行操作,实质上也是对文字进行操作。从结构来看,段落包含了文字,文档又包含了段落,是一个典型的树形结构。 而其根节点正是我们可以抽象出来暴露在外的统一接口,例如接口IElement:

composite1.GIF

既然文字、段落、文档都具有这些操作,因此它们都可以实现IElement接口:

从上图可以看到,对象Word、Paragraph、Document均实现了IElement接口,但Paragraph和Document与 Word对象不同的是,这两者处除了实现了IElement接口,它们还与IElement接口对象之间具有聚合的关系,且是一对多。也就是说 Paragraph与Document对象内可以包含0个到多个IElement对象,这也是与前面对字处理软件分析获得的结果是一致的。

从整个结构来看,完全符合树形结构的各个要素,接口IElement是根节点,而Paragraph和Document类为枝节点,Word对象为 叶节点。既然作为枝节点,它就具有带叶节点的能力,从上图的聚合关系中我们体现出了这一点。也就是说,Paragraph和Document类除了具有排 版、打印方面的职责外,还能够添加、删除叶节点的操作。那么这些操作应该放在哪里呢?

管理对子节点的管理,Composite模式提供了两种方式:一个是透明方式,也就是说在根节点中声明所有用来管理子元素的方法,包括Add()、 Remove()等方法。这样一来,实现根节点接口的子节点同时也具备了管理子元素的能力。这种实现策略,最大的好处就是完全消除了叶节点和枝节点对象在 抽象层次的区别,它们具备完全一致的接口。而缺点则是不够安全。由于叶节点本身不具备管理子元素的能力,因此提供的Add()、Remove()方法在实 现层次是无意义的。但客户端调用时,却看不到这一点,从而导致在运行期间有出错的可能。

另一种策略则是安全方式。与透明方式刚好相反,它只在枝节点对象里声明管理子元素的方法由于叶节点不具备这些方法,当客户端在操作叶节点时,就不会出现前一种方式的安全错误。然而,这种实现方式,却导致了叶节点和枝节点接口的不完全一致,这给客户端操作时带来了不便。

这两种方式各有优缺点,我们在实现时,应根据具体的情况,作出更加合理的抉择。在字处理软件一例中,我选择了安全方式来实现,因为对于客户端而言, 在调用IElement接口时,通常是将其视为可被排版、打印等操作的对象。至于为Paragraph和Document对象添加、删除子对象,往往是一 种初始化的行为,完全可以放到一个单独的模块中。根据单一职责原则(SRP),我们没有必要让IElement接口负累太重。所以,我们需要对上图作稍许 的修改,在Paragraph和Document对象中增加Add()和Remove()方法:

以下是IElement对象结构的实现代码:
public interface IElement
{
      void ChangeFont(Font font);
      void Show();
      //其他方法略;
}
public class Word
{
 public void ChangeFont(Font font)
 {
      this.font = font;
 }
 public void Show()
 {
      Console.WriteLine(this.ToString());
 }
 //其他方法略;
}
public class Paragraph
{
 private ArrayList elements = new ArrayList();
 public void Add(IElement element)
 {
      elements.Add(element);
 }
 public void Remove(IElement element)
 {
      elements.Remove(element);
 }
 public void ChangeFont(Font font)
 {
      foreach (IElement element in elements)
      {
          element.ChangeFont(font);
  }
 }
 public void Show()
 {
      foreach (IElement element in elements)
      {
          element.Show(font);
  }
 }
 //其他方法略;
}
//Document类略;

实际上,我们在为叶节点实现Add(),Remove()方法时,还需要考虑一些异常情况。例如在Paragraph类中,添加的子元素就不能是 Document对象和Paragraph对象。所以在添加IElement对象时,还需要做一些条件判断,以确定添加行为是否正确,如果错误,应抛出异 常。

采用Composite模式,我们将Word、Paragraph、Document抽象为IElement接口。虽然各自内部的实现并不相同,枝 节点和叶节点的实质也不一样,但对于调用者而言,是没有区别的。例如在类WordProcessor中,包含一个GetSelectedElement ()静态方法,它能够获得当前选择的对象:
public class WordProcessor
{
 public static IElement GetSelectedElement(){……}
}

对于字处理软件的UI来说,如果要改变选中对象的字体,则可以在命令按钮cmdChangeFont的Click事件中写下如下代码:
public void cmdChangeFont_Click(object sender, EventArgs e)
{
    WordProcessor.GetSelectedElement().ChangeFont(currentFont);
}

不管当前选中的对象是文字、段落还是整篇文档,对于UI而言,操作都是完全一致的,根本不需要去判断对象的类别。因此,如果在Business Layer的类库设计时,采用Composite模式,将极大地简化UI表示层的开发工作。此外,应用该模式也较好的支持项目的可扩展性。例如,我们为 IElement接口增加了Sentence类,对于前面的例子而言,只需要修改GetSelectedElement()方法,而 cmdChangeFont命令按钮的Click事件以及Business Layer类库原有的设计,都不需要做任何改变。这也符合OO的开放-封闭原则(OCP),即对于扩展是开放的(Open for extension),对于更改则是封闭的(Closed for modification)。

二、.Net Framework中的Composite模式

在.Net中,最能体现Composite模式的莫过于Windows或Web的控件。在这些控件中,有的包含子控件,有的则不包含且不能包含子控 件,这正好符合叶节点和枝节点的含义。所有Web控件的基类为System.Web.UI.Contril类(如果是Windows控件,则基类为 System.Windows.Forms.Control类)。其子类包含有HtmlControl、HtmlContainerControl等。按 照Composite模式的结构,枝节点和叶节点属于根节点的不同分支,同时枝节点与根节点之间应具备一个聚合关系,可以通过Add()、Remove ()方法添加和移除其子节点。设定HtmlControl为叶节点,而HtmlContaiinerControl为枝节点,那么采用透明方式的设计方 法,在.Net中控件类的结构,就应该如下图所示:

虽然根据透明方式的Composite模式,HtmlControl类与其父类Control之间也应具备一个聚合关系,但实质上该类并不具备管理 子控件的职责,因此我在类图中忽略了这个关系。此时,HtmlControl类中的Add()、Remove()方法,应该为空,或者抛出一个客户端能够 捕获的异常。

然而,从具体实现来考虑,由于HtmlControl类和HtmlContainerControl类在实现细节层次,区别仅在于前者不支持子控 件,但从控件本身的功能来看,很多行为是相同或者相近的。例如HtmlControl类的Render()方法,调用了方法RenderBeginTag ()方法:
protected override void Render(HtmlTextWriter writer)
{
      this.RenderBeginTag(writer);
}
protected virtual void RenderBeginTag(HtmlTextWriter writer)
{
      writer.WriteBeginTag(this.TagName);
      this.RenderAttributes(writer);
      writer.Write(’>');
}

而HtmlContainerControl类也具有Render()方法,在这个方法中也调用了RenderBeginTag()方法,且RenderBeginTag方法的实现和前者完全一致:
protected override void Render(HtmlTextWriter writer)
{
      this.RenderBeginTag(writer);
      this.RenderChildren(writer);
      this.RenderEndTag(writer);
}

按照上面的结构,由于HtmlControl和HtmlContainerControl之间并无继承关系,这就要求两个类中,都要重复实现 RenderBeginTag()方法,从而导致产生重复代码。根据OO的特点,解决的办法,就是让HtmlContainerControl继承自 HtmlControl类(因为HtmlContainerControl的接口比HtmlControl宽,所以只能令 HtmlContainerControl作为子类),并让RenderBeginTag()方法成为HtmlControl类的protected方 法,子类HtmlContainerControl可以直接调用这个方法。然而与之矛盾的是,HtmlContainerControl却是一个可以包含 子控件的枝节点,而HtmlControl则是不能包含子控件的叶节点,那么这样的继承关系还成立吗?

HtmlControl类对Add()方法和Remove()方法的重写后,这两个方法内容为空。由于HtmlContainerControl类 继承HtmlControl类,但我们又要求它的Add()和Remove()方法和Control类保持一致,而父类HtmlControl已经重写这 两个方法,此时是无法直接继承来自父类的方法的。以上是采用透明方式的设计。

如果采用安全方式,仍然有问题。虽然在HtmlControl类中不再有Add()和Remove()方法,但由于Control类和 HtmlContainerControl类都允许添加子控件,它们包含的Add()、Remove()方法,只能分别实现。这样的设计必然会导致重复代 码。这也是与我们的期望不符的。

那么在.Net中,Control类究竟是怎样实现的呢?下面,我将根据.Net实现Control控件的源代码,来分析Control控件的真实结构,以及其具体的实现细节。

三、深入分析.Net中的Composite模式

首先,我们来剖析Web控件的基类Control类的内部实现:

public class Control : IComponent, IDisposable, IParserAccessor, IDataBindingsAccessor
{
       // Events;略  
       // Methods
 public Control()
 {
             if (this is INamingContainer)
             {
                    this.flags[0×80] = true;
             }
 }
 public virtual bool HasControls()
 {
              if (this._controls != null)
              {
                    return (this._controls.Count > 0);
              }
              return false;
 }
 public virtual void DataBind()
        {
              this.OnDataBinding(EventArgs.Empty);   
              if (this._controls != null)           
       {                
              string text1 = this._controls.SetCollectionReadOnly(”Parent_collections_readonly”);
                        int num1 = this._controls.Count;
                 for (int num2 = 0; num2 < num1; num2++)                 
   {
                         this._controls[num2].DataBind();
                   }
                   this._controls.SetCollectionReadOnly(text1);
              }
 }
        protected virtual void Render(HtmlTextWriter writer)
 {
              this.RenderChildren(writer);
 }
 protected virtual ControlCollection CreateControlCollection()
 {
              return new ControlCollection(this);
 }
       // Properties
 public virtual ControlCollection Controls
 {
             get
             {
                   if (this._controls == null)
                   {
                         this._controls = this.CreateControlCollection();
                   }
                   return this._controls;
             }
 }
        // Fields
        private ControlCollection _controls;
}

Control基类中的属性和方法很多,为清晰起见,我只保留了几个与模式有关的关键方法与属性。在上述的源代码中,我们需要注意几点:

1、Control类不是抽象类,而是具体类。这是因为在设计时,我们可能会创建Control类型的实例。根据这一点来看,这并不符合OOP的要 求。一般而言,作为抽象出来的基类,必须定义为接口或抽象类。不过在实际的设计中,也不应拘泥于这些条条框框,而应审时度势,根据实际的情况来抉择最佳的 设计方案。
2、公共属性Controls为ControlCollection类型,且该属性为virtual属性。也就是说,这个属性可以被它的子类 override。同时,该属性为只读属性,在其get访问器中,调用了方法CreateControlCollection();这个方法为 protected虚方法,默认的实现是返回一个ControlCollection实例。
3、方法HasControls(),功能为判断Control对象是否有子控件。它判断的依据是根据私有字段_controls(即公共属性 Controls)的Count值。但是需要注意的是,通过HasControls()方法的返回值,并不能决定对象本身属于叶节点,还是枝节点。因为即 使是枝节点其内部仍然可以不包含任何子对象。
4、 方法DataBind()的实现中,首先调用了自身的OnDataBinding()方法,然后又遍历了Controls中的所有控件,并调用其 DataBind()方法。该方法属于控件的共有行为,从这里可以看出不管是作为叶节点的控件,还是作为枝节点的控件,它们都实现统一的接口。对于客户端 调用而言,枝节点和叶节点是没有区别的。
5、 Control类的完整源代码中,并不存在Add()、Remove()等类似的方法,以提供添加和移除子控件的功能。事实上,继承Control类的所有子类均不存在Add()、Remove()等方法。

显然,在Control类的定义和实现中,值得我们重视的是公共属性Controls的类型ControlCollection。顾名思义,该类必 然是一个集合类型。是否有关子控件的操作,都是在ControlCollection类型中实现呢?我们来分析一下ControlCollection的 代码:
public class ControlCollection : ICollection, IEnumerable
{
       // Methods
 public ControlCollection(Control owner)
 {
               this._readOnlyErrorMsg = null;
               if (owner == null)
               {
                       throw new ArgumentNullException("owner");
               }
               this._owner = owner;
        }
 public virtual void Add(Control child)
        {
               if (child == null)
               {
                throw new ArgumentNullException("child");
               }
               if (this._readOnlyErrorMsg != null)
                   {
                throw new HttpException(HttpRuntime.FormatResourceString(this._readOnlyErrorMsg));
               }
               if (this._controls == null)
               {
                   this._controls = new Control[5];
               }
               else if (this._size >= this._controls.Length)
               {
                   Control[] controlArray1 = new Control[this._controls.Length * 4];
                   Array.Copy(this._controls, controlArray1, this._controls.Length);
                   this._controls = controlArray1;
               }
               int num1 = this._size;
               this._controls[num1] = child;
               this._size++;
               this._version++;
               this._owner.AddedControl(child, num1);
        }
        public virtual void Remove(Control value)
 {
               int num1 = this.IndexOf(value);
               if (num1 >= 0)
               {
                this.RemoveAt(num1);
               }
        }
        // Indexer
 public virtual Control this[int index]
 {
               get
               {
                    if ((index < 0) || (index >= this._size))
                    {
                         throw new ArgumentOutOfRangeException(”index”);
                    }
                    return this._controls[index];
               }
 }
 // Properties    
 public int Count
 {
        get
               {
                    return this._size;
               }
        }
 protected Control Owner
 {
               get
               {
                    return this._owner;
               }
 }
        protected Control Owner { get; }   
        // Fields
        private Control[] _controls;
        private const int _defaultCapacity = 5;
        private const int _growthFactor = 4;
 private Control _owner;    
}

一目了然,正是ControlCollection的Add()、Remove()方法完成了对子控件的添加和删除。例如:
Control parent = new Control();
Control child = new Child();
//添加子控件child;
parent.Controls.Add(child);
//移除子控件child;
parent.Controls.Remove(child);

为什么要专门提供ControlCollection类型来管理控件的子控件呢?首先,作为类库使用者,自然希望各种类型的控件具有统一的接口,尤 其是自定义控件的时候,不希望自己重复定义管理子控件的操作;那么采用透明方式自然是最佳方案。然而,在使用控件的时候,安全也是需要重点考虑的,如果不 考虑子控件管理的合法性,一旦使用错误,会导致整个应用程序出现致命错误。从这样的角度考虑,似乎又应采用安全方式。这里就存在一个抉择。故而,.Net 在实现Control类库时,利用了职责分离的原则,将控件对象管理子控件的属性与行为和控件本身分离,并交由单独的ControlCollection 类负责。同时采用聚合而非继承的方式,以一个公共属性Controls,存在于Control类中。这种方式,集保留了透明方式和安全方式的优势,又摒弃 了这两种方式固有的缺陷,因此我名其为“复合方式”。

“复合方式”的设计,其对安全的保障,不仅仅是去除了Control类关于子控件管理的统一接口,同时还通过异常管理的方式,在ControlCollection类的子类中实现:
public class EmptyControlCollection : ControlCollection
{
        // Methods
 public EmptyControlCollection(Control owner) : base(owner)
 {}
 public override void Add(Control child)
 {
            this.ThrowNotSupportedException();
 }                         
        private void ThrowNotSupportedException()
        {
            throw new HttpException(HttpRuntime.FormatResourceString(”Control_does_not_allow_children”, base.Owner.GetType().ToString()));
        }
}

EmptyControlCollection继承了ControlCollection类,并重写了Add()等添加子控件的方法,使其抛出一个 异常。注意,它并没有重写父类的Remove()方法,这是因为ControlCollection类在实现Remove()方法时,对集合内的数据进行 了非空判断。而在EmptyControlCollection类中,是不可能添加子控件的,直接调用父类的Remove()方法,是不会出现错误的。

既然管理子控件的职责由ControlCollection类型负责,且Control类中的公共属性Controls即为 ControlCollection类型。所以,对于控件而言,如果是树形结构中的叶节点,它不能包含子控件,它的Controls属性就应为 EmptyControlCollection类型,假如用户调用了Controls的Add()方法,就会抛出异常。如果控件是树形结构中的枝节点,它 支持子控件,那么Controls属性就是ControlCollection类型。究竟是枝节点还是叶节点,决定权在于公共属性Controls:
public virtual ControlCollection Controls
{
      get
      {
             if (this._controls == null)
             {
                   this._controls = this.CreateControlCollection();
             }
             return this._controls;
      }
}

在属性的get访问器中,调用了protected方法CreateControlCollection(),它创建并返回了一个ControlCollection实例:
protected virtual ControlCollection CreateControlCollection()
{
     return new ControlCollection(this);
}

很明显,在Control基类实现Controls属性时,采用了Template Method模式,它推迟了ControlCollection的创建,将决定权交给了CreateControlCollection()方法。

如果我们需要定义一个控件,要求它不能管理子控件,就重写CreateControlCollection()方法,返回EmptyControlCollection对象:

protected override ControlCollection CreateControlCollection()
{
     return new EmptyControlCollection(this);
}
 
现在再回过头来看HtmlControl和HtmlContainerControl类。根据前面的分析,我们要求 HtmlContainerControl继承HtmlControl类,同时,HtmlContainerControl应为枝节点,能够管理子控件; HtmlControl则为叶节点,不支持子控件。通过引入ControlCollection类和其子类 EmptyControlCollection,以及Template Method模式后,这些类之间的关系与结构如下所示:

HtmlContainerControl继承了HtmlControl类,这两个类都重写了自己父类的protected方法 CreateControlCollection()。HtmlControl类,该方法返回EmptyControlCollection对象,使其成 为了不包含子控件的叶节点;HtmlContainerControl类中,该方法则返回ControlCollection对象,从而被赋予了管理子控 件的能力,成为了枝节点:
public abstract class HtmlControl : Control, IAttributeAccessor
{
        // Methods
 protected override ControlCollection CreateControlCollection()
 {
            return new EmptyControlCollection(this);
 }
}
public abstract class HtmlContainerControl : HtmlControl
{
       // Methods
 protected override ControlCollection CreateControlCollection()
 {
            return new ControlCollection(this);
 }
}

HtmlControl和HtmlContainerControl类均为抽象类。要定义它们的子类,如果不重写其父类的 CreateControlCollection()方法,那么它们的Controls属性,就与父类完全一致。例如HtmlImage控件继承自 HtmlControl类,该控件不能添加子控件;而HtmlForm控件则继承自HtmlContainerControl类,显然,HtmlForm 控件是支持添加子控件的操作的。

.Net的控件设计采用Composite模式的“复合方式”,较好地将控件的透明性与安全性结合起来,它的特点是:

1、在统一接口中消除了Add()、Remove()等子控件的管理方法,而由ControlCollection类实现,同时通过EmptyControlCollection类保障了控件进一步的安全;
2、控件能否管理子控件,不由继承的层次决定;而是通过重写CreateControlCollection()方法,由Controls属性的真正类型来决定。

如此一来,要定义自己的控件就更加容易。我们可以任意地扩展自己的控件类。不管继承自Control,还是HtmlControl或 HtmlContainerControl,都可以轻松地定义出具有枝节点或叶节点属性的新控件。如果有新的需求要求改变管理子控件的方式,我们还可以定 义继承自ControlCollection的类,并在控件类的方法CreateControlCollection()中创建并返回它的实例。

posted @ 2006-09-09 00:51 Alex 阅读(462) | 评论 (0)编辑 收藏

设计模式之Composite(组合)

板桥里人 http://www.jdon.com 2002/04/27(转载请保留)

 

Composite模式定义:
将对象以树形结构组织起来,以达成“部分-整体” 的层次结构,使得客户端对单个对象和组合对象的使用具有一致性.

Composite比较容易理解,想到Composite就应该想到树形结构图。组合体内这些对象都有共同接口,当组合体一个对象的方法被调用执行 时,Composite将遍历(Iterator)整个树形结构,寻找同样包含这个方法的对象并实现调用执行。可以用牵一动百来形容。

所以Composite模式使用到Iterator模式,和Chain of Responsibility模式类似。

Composite好处:
1.使客户端调用简单,客户端可以一致的使用组合结构或其中单个对象,用户就不必关系自己处理的是单个对象还是整个组合结构,这就简化了客户端代码。
2.更容易在组合体内加入对象部件. 客户端不必因为加入了新的对象部件而更改代码。

如何使用Composite?
首先定义一个接口或抽象类,这是设计模式通用方式了,其他设计模式对接口内部定义限制不多,Composite却有个规定,那就是要在接口内部定义一个用于访问和管理Composite组合体的对象们(或称部件Component).

下面的代码是以抽象类定义,一般尽量用接口interface,

public abstract class Equipment
{
  private String name;
  //实价
  public abstract double netPrice();
  //折扣价格
  public abstract double discountPrice();
  //增加部件方法  
  public boolean add(Equipment equipment) { return false; }
  //删除部件方法
  public boolean remove(Equipment equipment) { return false; }
  //注意这里,这里就提供一种用于访问组合体类的部件方法。
  public Iterator iter() { return null; }
  
  public Equipment(final String name) { this.name=name; }
}

抽象类Equipment就是Component定义,代表着组合体类的对象们,Equipment中定义几个共同的方法。

public class Disk extends Equipment
{
  public Disk(String name) { super(name); }
  //定义Disk实价为1
  public double netPrice() { return 1.; }
  //定义了disk折扣价格是0.5 对折。
  public double discountPrice() { return .5; }
}

Disk是组合体内的一个对象,或称一个部件,这个部件是个单独元素( Primitive)。
还有一种可能是,一个部件也是一个组合体,就是说这个部件下面还有'儿子',这是树形结构中通常的情况,应该比较容易理解。现在我们先要定义这个组合体:

abstract class CompositeEquipment extends Equipment
{
  private int i=0;
  //定义一个Vector 用来存放'儿子'
  private Lsit equipment=new ArrayList();

  public CompositeEquipment(String name) { super(name); }

  public boolean add(Equipment equipment) {
     this.equipment.add(equipment);
     return true;
   }


  public double netPrice()
  {
    double netPrice=0.;
    Iterator iter=equipment.iterator();
    for(iter.hasNext())
      netPrice+=((Equipment)iter.next()).netPrice();
    return netPrice;
  }

  public double discountPrice()
  {
    double discountPrice=0.;
    Iterator iter=equipment.iterator();
    for(iter.hasNext())
      discountPrice+=((Equipment)iter.next()).discountPrice();
    return discountPrice;
  }
  

  //注意这里,这里就提供用于访问自己组合体内的部件方法。
  //上面dIsk 之所以没有,是因为Disk是个单独(Primitive)的元素.

  public Iterator iter()
  {
    return equipment.iterator() ;
  {
  //重载Iterator方法
   public boolean hasNext() { return i<equipment.size(); }
  //重载Iterator方法
   public Object next()
   {
    if(hasNext())
       return equipment.elementAt(i++);
    else
        throw new NoSuchElementException();
   }
  

}

上面CompositeEquipment继承了Equipment,同时为自己里面的对象们提供了外部访问的方法,重载了Iterator,Iterator是Java的Collection的一个接口,是Iterator模式的实现.

我们再看看CompositeEquipment的两个具体类:盘盒Chassis和箱子Cabinet,箱子里面可以放很多东西,如底板,电源盒,硬盘盒等;盘盒里面可以放一些小设备,如硬盘 软驱等。无疑这两个都是属于组合体性质的。

public class Chassis extends CompositeEquipment
{
   public Chassis(String name) { super(name); }
   public double netPrice() { return 1.+super.netPrice(); }
   public double discountPrice() { return .5+super.discountPrice(); }
}

public class Cabinet extends CompositeEquipment
{
   public Cabinet(String name) { super(name); }
   public double netPrice() { return 1.+super.netPrice(); }
   public double discountPrice() { return .5+super.discountPrice(); }
}

至此我们完成了整个Composite模式的架构。

我们可以看看客户端调用Composote代码:

Cabinet cabinet=new Cabinet("Tower");

Chassis chassis=new Chassis("PC Chassis");
//将PC Chassis装到Tower中 (将盘盒装到箱子里)
cabinet.add(chassis);
//将一个10GB的硬盘装到 PC Chassis (将硬盘装到盘盒里)
chassis.add(new Disk("10 GB"));

//调用 netPrice()方法;
System.out.println("netPrice="+cabinet.netPrice());
System.out.println("discountPrice="+cabinet.discountPrice());

上面调用的方法netPrice()或discountPrice(),实际上Composite使用Iterator遍历了整个树形结构,寻找同样包含这个方法的对象并实现调用执行.

Composite是个很巧妙体现智慧的模式,在实际应用中,如果碰到树形结构,我们就可以尝试是否可以使用这个模式。

以论坛为例,一个版(forum)中有很多帖子(message),这些帖子有原始贴,有对原始贴的回应贴,是个典型的树形结构,那么当然可以使用Composite模式,那么我们进入Jive中看看,是如何实现的.

Jive解剖
在Jive中 ForumThread是ForumMessages的容器container(组合体).也就是说,ForumThread类似我们上例中的 CompositeEquipment.它和messages的关系如图:
[thread]
   |- [message]
   |- [message]
      |- [message]
      |- [message]
         |- [message]

我们在ForumThread看到如下代码:

public interface ForumThread {
   ....
   public void addMessage(ForumMessage parentMessage, ForumMessage newMessage)
         throws UnauthorizedException;

   public void deleteMessage(ForumMessage message)
         throws UnauthorizedException;

  
   public Iterator messages();
      ....

}

类似CompositeEquipment, 提供用于访问自己组合体内的部件方法: 增加 删除 遍历.

结合我的其他模式中对Jive的分析,我们已经基本大体理解了Jive论坛体系的框架,如果你之前不理解设计模式,而直接去看Jive源代码,你肯定无法看懂。

:)

posted @ 2006-09-09 00:44 Alex 阅读(493) | 评论 (0)编辑 收藏

     摘要: 转自: CCIENET 自从J2EE出现以来,就大大简化了在Java下的企业级开发。但是随着J2EE越来越普遍 地被应用到各个领域中,开发者们渐渐意识到需要一种方法来标准化应用程序的开发过程,他们采用的方法是标准化应用程序的结构层。在结构层通常封装了一些独 立于业务逻辑的复杂技术,以便在业务逻辑和底层的架构之间建立起弱连接。无可否认,J2EE是一个很成功的技术,它为一些基本的任务提供了一...  阅读全文
posted @ 2006-09-08 23:59 Alex 阅读(1967) | 评论 (1)编辑 收藏

     摘要: 引言:最近在看一个开源的聊天室AjaxChat 时看到一个被引用的包:javawebparts,处于好奇去看了一下,突然发现这么好的一个常用web组件不去用实在是太可惜了,下面逐一介绍,详细文档大家可以去官方文档看看,最好看它的Demo,很直观。 javawebparts的口号是:不用重新发明轮子 ! 对这点我是严重支持啊,在我的身边看到N多所谓...  阅读全文
posted @ 2006-09-08 21:18 Alex 阅读(2525) | 评论 (7)编辑 收藏

本文主要谈一下密码学中的加密和数字签名,以及其在java中如何进行使用。对密码学有兴趣的伙伴,推荐看 Bruce Schneier的著作:Applied Crypotography。在jdk1.5的发行版本中安全性方面有了很大的改进,也提供了对RSA算法的直接支持,现在我们从实例入手解决问题(本文 仅是作为简单介绍):

  一、密码学上常用的概念 

  1)消息摘要:

   这是一种与消息认证码结合使用以确保消息完整性的技术。主要使用单向散列函数算法,可用于检验消息的完整性,和通过散列密码直接以文本形式保存等,目前 广泛使用的算法有MD4、MD5、SHA-1,jdk1.5对上面都提供了支持,在java中进行消息摘要很简单, java.security.MessageDigest提供了一个简易的操作方法:

/**
*MessageDigestExample.java
*Copyright 2005-2-16
*/
import java.security.MessageDigest;
/**
*单一的消息摘要算法,不使用密码.可以用来对明文消息(如:密码)隐藏保存
*/
public class MessageDigestExample{
 
public static void main(String[] args) throws Exception{
  
if(args.length!=1){
   System.err.println(
"Usage:java MessageDigestExample text");
   System.exit(
1);
  }

  
byte[] plainText=args[0].getBytes("UTF8");

  
//使用getInstance("算法")来获得消息摘要,这里使用SHA-1的160位算法
  MessageDigest messageDigest=MessageDigest.getInstance("SHA-1");

  System.out.println(
"\n"+messageDigest.getProvider().getInfo());
  
//开始使用算法
  messageDigest.update(plainText);
  System.out.println(
"\nDigest:");
  
//输出算法运算结果
  System.out.println(new String(messageDigest.digest(),"UTF8"));
 }
}

  还可以通过消息认证码来进行加密实现,javax.crypto.Mac提供了一个解决方案,有兴趣者可以参考相关API文档,本文只是简单介绍什么是摘要算法。

这里补充另一个运用消息摘要的方式加密的例子:
public class TestEncrypt {

    
public TestEncrypt() {
    }

    
/**
     * 
@param strSrc  :strSrc is a string will be encrypted,
     * 
@param encName : encName is the algorithm name will be used.
     *                encName dafault to "MD5"
     * 
@return String
     
*/
    
public String Encrypt(String strSrc, String encName) {

        MessageDigest md 
= null;
        String strDes 
= null;

        
byte[] bt = strSrc.getBytes();
        
try {
            
if (encName == null || encName.equals("")) {
                encName 
= "MD5";
            }
            md 
= MessageDigest.getInstance(encName);
            md.update(bt);
            strDes 
= bytes2Hex(md.digest()); //to HexString
        }
        
catch (NoSuchAlgorithmException e) {
            System.out.println(
"Invalid algorithm.");
            
return null;
        }
        
return strDes;
    }

    
public String bytes2Hex(byte[] bts) {
        String des 
= "";
        String tmp 
= null;
        
for (int i = 0; i < bts.length; i++) {
            tmp 
= (Integer.toHexString(bts[i] & 0xFF));
            
if (tmp.length() == 1) {
                des 
+= "0";
            }
            des 
+= tmp;
        }
        
return des;
    }

    
public static void main(String[]args) {
        TestEncrypt te 
= new TestEncrypt();
        String strSrc 
= "可以加密汉字.Oh,and english";
        System.out.println(
"Source String:" + strSrc);
        System.out.println(
"Encrypted String:");
        System.out.println(
"Use Def:" + te.Encrypt(strSrc, null));
        System.out.println(
"Use MD5:" + te.Encrypt(strSrc, "MD5"));
        System.out.println(
"Use SHA:" + te.Encrypt(strSrc, "SHA-1"));
        System.out.println(
"Use SHA-256:" + te.Encrypt(strSrc, "SHA-256"));
    }
}

另外,在javawebparts中的 RequestHelpers里的generateGUID方法也涉及到了MD5的方法,代码如下:
public static String generateGUID(HttpServletRequest request) {

    String out 
= "";
    
try {
      
// Construct a string that is comprised of:
      
// Remote IP Address + Host IP Address + Date (yyyyMMdd) +
      
// Time (hhmmssSSa) + Requested Path + Session ID +
      
// HashCode Of ParameterMap
      StringBuffer sb = new StringBuffer(1024);
      sb.append(request.getRemoteAddr());
      InetAddress ia 
= InetAddress.getLocalHost();
      sb.append(ia.getHostAddress());
      sb.append(
new SimpleDateFormat("yyyyMMddhhmmssSSa").format(new Date()));
      String path 
= request.getServletPath();
      String pathInfo 
= request.getPathInfo();
      
if (pathInfo != null) {
        path 
+= pathInfo;
      }
      sb.append(path);
      sb.append(request.getSession(
false));
      sb.append(request.getParameterMap().hashCode());
      String str 
= sb.toString();
      
// Now encode the string using an MD5 encryption algorithm.
      MessageDigest md = MessageDigest.getInstance("md5");
      md.update(str.getBytes());
      
byte[] digest = md.digest();
      StringBuffer hexStr 
= new StringBuffer(1024);
      
for (int i = 0; i < digest.length; i++) {
        str 
= Integer.toHexString(0xFF & digest[i]);
        
if (str.length() < 2) {
          str 
= "0" + str;
        }
        hexStr.append(str);
      }
      out 
= hexStr.toString();
    } 
catch (NoSuchAlgorithmException nsae) {
      log.error(nsae);
    } 
catch (UnknownHostException uhe) {
      log.error(uhe);
    }
    
// Return the encrypted string.  It should be unique based on the
    
// components that comprise the plain text string, and should always be
    
// 32 characters thanks to the MD5 algorithm.
    return out;

  } 
// End generateGUID().


  2)私钥加密:

  消息摘要只能检查消息的完整性,但是单向的,对明文消息并不能加密,要加密明文的消息的话,就要使用其他的算法,要确保机密性,我们需要使用私钥密码术来交换私有消息。

  这种最好理解,使用对称算法。比如:A用一个密钥对一个文件加密,而B读取这个文件的话,则需要和A一样的密钥,双方共享一个私钥(而在web环境下,私钥在传递时容易被侦听):

   使用私钥加密的话,首先需要一个密钥,可用javax.crypto.KeyGenerator产生一个密钥(java.security.Key), 然后传递给一个加密工具(javax.crypto.Cipher),该工具再使用相应的算法来进行加密,主要对称算法有:DES(实际密钥只用到56 位),AES(支持三种密钥长度:128、192、256位),通常首先128位,其他的还有DESede等,jdk1.5种也提供了对对称算法的支持, 以下例子使用AES算法来加密:

/**
*PrivateExmaple.java
*Copyright 2005-2-16
*/
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import java.security.Key;

/**
*私鈅加密,保证消息机密性
*/
public class PrivateExample{
 
public static void main(String[] args) throws Exception{
  
if(args.length!=1){
   System.err.println(
"Usage:java PrivateExample <text>");
   System.exit(
1);
  }
  
byte[] plainText=args[0].getBytes("UTF8");

  
//通过KeyGenerator形成一个key
  System.out.println("\nStart generate AES key");
  KeyGenerator keyGen
=KeyGenerator.getInstance("AES");
  keyGen.init(
128);
  Key key
=keyGen.generateKey();
  System.out.println(
"Finish generating DES key");

  
//获得一个私鈅加密类Cipher,ECB是加密方式,PKCS5Padding是填充方法
  Cipher cipher=Cipher.getInstance("AES/ECB/PKCS5Padding");
  System.out.println(
"\n"+cipher.getProvider().getInfo());

  
//使用私鈅加密
  System.out.println("\nStart encryption:");
  cipher.init(Cipher.ENCRYPT_MODE,key);
  
byte[] cipherText=cipher.doFinal(plainText);
  System.out.println(
"Finish encryption:");
  System.out.println(
new String(cipherText,"UTF8"));

  System.out.println(
"\nStart decryption:");
  cipher.init(Cipher.DECRYPT_MODE,key);
  
byte[] newPlainText=cipher.doFinal(cipherText);
  System.out.println(
"Finish decryption:");

  System.out.println(
new String(newPlainText,"UTF8"));

 }
}

  3)公钥加密:

   上面提到,私钥加密需要一个共享的密钥,那么如何传递密钥呢?web环境下,直接传递的话很容易被侦听到,幸好有了公钥加密的出现。公钥加密也叫不对称 加密,不对称算法使用一对密钥对,一个公钥,一个私钥,使用公钥加密的数据,只有私钥能解开(可用于加密);同时,使用私钥加密的数据,只有公钥能解开 (签名)。但是速度很慢(比私钥加密慢100到1000倍),公钥的主要算法有RSA,还包括Blowfish,Diffie-Helman等, jdk1.5种提供了对RSA的支持,是一个改进的地方:

/**
*PublicExample.java
*Copyright 2005-2-16
*/
import java.security.Key;
import javax.crypto.Cipher;
import java.security.KeyPairGenerator;
import java.security.KeyPair;
/**
*一个简单的公鈅加密例子,Cipher类使用KeyPairGenerator生成的公鈅和私鈅
*/
public class PublicExample{
 
public static void main(String[] args) throws Exception{
  
if(args.length!=1){
   System.err.println(
"Usage:java PublicExample <text>");
   System.exit(
1);
  }

  
byte[] plainText=args[0].getBytes("UTF8");
  
//构成一个RSA密钥
  System.out.println("\nStart generating RSA key");
  KeyPairGenerator keyGen
=KeyPairGenerator.getInstance("RSA");
  keyGen.initialize(
1024);
  KeyPair key
=keyGen.generateKeyPair();
  System.out.println(
"Finish generating RSA key");

  
//获得一个RSA的Cipher类,使用公鈅加密
  Cipher cipher=Cipher.getInstance("RSA/ECB/PKCS1Padding");
  System.out.println(
"\n"+cipher.getProvider().getInfo());

  System.out.println(
"\nStart encryption");
  cipher.init(Cipher.ENCRYPT_MODE,key.getPublic());
  
byte[] cipherText=cipher.doFinal(plainText);
  System.out.println(
"Finish encryption:");
  System.out.println(
new String(cipherText,"UTF8"));

  
//使用私鈅解密
  System.out.println("\nStart decryption");
  cipher.init(Cipher.DECRYPT_MODE,key.getPrivate());
  
byte[] newPlainText=cipher.doFinal(cipherText);
  System.out.println(
"Finish decryption:");
  System.out.println(
new String(newPlainText,"UTF8"));
 }
}

posted @ 2006-09-07 23:59 Alex 阅读(1149) | 评论 (0)编辑 收藏

key words: Digester  解析xml

假设有下列xml文件:
<?xml version='1.0' encoding='utf-8'?>
<address-book>
    
<contact myType="individual">
        
<name>Zane Pasolini</name>
        
<address>999 W. Prince St.</address>
        
<city>New York</city>
        
<province>NY</province>
        
<postalcode>10013</postalcode>
        
<country>USA</country>
        
<telephone>1-212-345-6789</telephone>
    
</contact>
    
<contact myType="business">
        
<name>SAMOFIX d.o.o.</name>
        
<address>Ilica 47-2</address>
        
<city>Zagreb</city>
        
<province></province>
        
<postalcode>10000</postalcode>
        
<country from="cn">Croatia</country>
        
<telephone>385-1-123-4567</telephone>
    
</contact>
</address-book>

这是一份常用到的文件,现在我们需要将之映射到java bean,用Digester解析显得非常简单
public class AddressBookParser
{
    
/**
     * Prints the contact information to standard output.
     *
     * 
@param contact the <code>Contact</code> to print out
     
*/
    
public void addContact(Contact contact)
    {
        System.out.println(
"TYPE: " + contact.getType());
        System.out.println(
"NAME: " + contact.getName());
        System.out.println(
"    ADDRESS:    " + contact.getAddress());
        System.out.println(
"    CITY:       " + contact.getCity());
        System.out.println(
"    PROVINCE:   " + contact.getProvince());
        System.out.println(
"    POSTALCODE: " + contact.getPostalcode());
        System.out.println(
"    COUNTRY:    " + contact.getCountry());
        System.out.println(
"    COUNTRY-From:    " + contact.getCountryFrom());
        System.out.println(
"    TELEPHONE:  " + contact.getTelephone());
    }

    
/**
     * Configures Digester rules and actions, parses the XML file specified
     * as the first argument.
     *
     * 
@param args command line arguments
     
*/
    
public static void main(String[] args) throws IOException, SAXException
    {
        
// instantiate Digester and disable XML validation
        Digester digester = new Digester();
        digester.setValidating(
false);

        
// instantiate AddressBookParser class
        digester.addObjectCreate("address-book", AddressBookParser.class );
        
// instantiate Contact class
        digester.addObjectCreate("address-book/contact", Contact.class );

        
// set type property of Contact instance when 'type' attribute is found
        
//对有属性的值通过setProperties方法

        digester.addSetProperties(
"address-book/contact",         "myType""type" );

        
// set different properties of Contact instance using specified methods
        
//addCallMethod与addBeanPropertySetter等价
        
// 参数 0代表一个参数,默认就是当前读的数据

        digester.addCallMethod(
"address-book/contact/name",       "setName"0);
        digester.addCallMethod(
"address-book/contact/address",    "setAddress"0);
        digester.addCallMethod(
"address-book/contact/address",    "setAddress",0);
        digester.addCallMethod(
"address-book/contact/city",       "setCity"0);
        digester.addCallMethod(
"address-book/contact/province",   "setProvince"0);
        digester.addCallMethod(
"address-book/contact/postalcode""setPostalcode"0);
        digester.addCallMethod(
"address-book/contact/country",    "setCountry"0);



        
//增加country的属性 : from
        digester.addSetProperties("address-book/contact/country","from","countryFrom");
        digester.addCallMethod(
"address-book/contact/telephone",  "setTelephone"0);

        
// call 'addContact' method when the next 'address-book/contact' pattern is seen
        digester.addSetNext("address-book/contact",               "addContact" );

        
// now that rules and actions are configured, start the parsing process
        AddressBookParser abp = (AddressBookParser) digester.parse(new File("c:\\addressbook.xml"));
    }

    
/**
     * JavaBean class that holds properties of each Contact entry.
     * It is important that this class be public and static, in order for
     * Digester to be able to instantiate it.
     
*/
    
public static class Contact
    {
        
private String type;
        
private String name;
        
private String address;
        
private String city;
        
private String province;
        
private String postalcode;
        
private String country;
        //增加一个country的属性: from
        private String countryFrom;
        private String telephone;

        
public void setType(String newType)
        {
            type 
= newType;
        }
        
public String getType()
        {
            
return type;
        }

        
public void setName(String newName)
        {
            name 
= newName;
        }
        
public String getName()
        {
            
return name;
        }

        
public void setAddress(String newAddress)
        {
            address 
= newAddress;
        }
        
public String getAddress()
        {
            
return address;
        }

        
public void setCity(String newCity)
        {
            city 
= newCity;
        }
        
public String getCity()
        {
            
return city;
        }

        
public void setProvince(String newProvince)
        {
            province 
= newProvince;
        }
        
public String getProvince()
        {
            
return province;
        }

        
public void setPostalcode(String newPostalcode)
        {
            postalcode 
= newPostalcode;
        }
        
public String getPostalcode()
        {
            
return postalcode;
        }

        
public void setCountry(String newCountry)
        {
            country 
= newCountry;
        }
        
public String getCountry()
        {
            
return country;
        }

        
public void setTelephone(String newTelephone)
        {
            telephone 
= newTelephone;
        }
        
public String getTelephone()
        {
            
return telephone;
        }

        
public String getCountryFrom() {
            
return countryFrom;
        }

        
public void setCountryFrom(String countryFrom) {
            
this.countryFrom = countryFrom;
        }
    }
}


AjaxChat 中的读取房间信息的方式显得更简洁
房间的xml配置文件如下:
<rooms>
  
<room id="1" name="General Topics" />
  
<room id="2" name="Programming" />
  
<room id="3" name="Movies" />
  
<room id="4" name="Music" />
  
<room id="5" name="Television" />
</rooms>

解析代码如下 :
public synchronized void init(InputStream isConfigFile) {

        log.debug(
"init()");
        
if (isConfigFile != null) {
            
// Read in rooms config and create beans, hand off to DAO.
            Digester digester = new Digester();
            digester.setValidating(
false);
            digester.push(
this);
            digester.addObjectCreate(
"rooms/room",
                    
"org.apache.struts.apps.ajaxchat.dto.RoomDTO");
            //注意这里,如果xl的属性名称和bean的属性名称完全对应,则直接提供xml的位置即可
            digester.addSetProperties(
"rooms/room");
            digester.addSetNext(
"rooms/room""addRoom");
            
try {
                digester.parse(isConfigFile);
                log.info(
"***** Rooms = " + rooms);
            } 
catch (IOException ioe) {
                ioe.printStackTrace();
            } 
catch (SAXException se) {
                se.printStackTrace();
            }
        }

    } 
// End init().

如果在xml文件中增加非attribute则更改后的配置文件如下:

<rooms>
  
<room id="1" name="General Topics" />
  
<room id="2" name="Programming" />
  
<room id="3" name="Movies" />
  
<room id="4" name="Music" />
  
<room id="5" name="Television" />
  
<room>
    
<id>6</id>
    
<name>shit</name>
  
</room>
  
<room>
    
<id>7</id>
    
<name>haha</name>
  
</room>
</rooms>
对应的解析如下:
public synchronized void init(InputStream isConfigFile) {

        log.debug(
"init()");
        
if (isConfigFile != null) {
            
// Read in rooms config and create beans, hand off to DAO.
            Digester digester = new Digester();
            digester.setValidating(
false);
            digester.push(
this);
            digester.addObjectCreate(
"rooms/room",
                    
"org.apache.struts.apps.ajaxchat.dto.RoomDTO");
            digester.addSetProperties(
"rooms/room");
            //增加addCallMethod方法
            digester.addCallMethod(
"rooms/room/id","setId",0);
            digester.addCallMethod(
"rooms/room/name","setName",0);
            digester.addSetNext(
"rooms/room""addRoom");
            
try {
                digester.parse(isConfigFile);
                log.info(
"***** Rooms = " + rooms);
            } 
catch (IOException ioe) {
                ioe.printStackTrace();
            } 
catch (SAXException se) {
                se.printStackTrace();
            }
        }

    } 
// End init().

posted @ 2006-09-06 23:32 Alex 阅读(20849) | 评论 (19)编辑 收藏

how to set the SSH timeout?
a: set 'TMOUT=3600' in /etc/profile

fine.
posted @ 2006-09-06 16:07 Alex 阅读(833) | 评论 (0)编辑 收藏

key words:struts国际化

一、Struts的国际化
    Struts是一种支持国际化的MVC的Web Framework。可是如何来使用struts国际化是一个问题。下面我们来探讨一下,如何实现Struts的国际化。Web程式的国际化涉及到3个层面的东西。第一、jsp部分的输入/输出;第二、应用处理程序的国际化;第三、DB的国际化问题。这里主要探讨的是jsp部分的输入/输出问题。

二、静态部分的国际化
   Struts的jsp页面静态内容(包括静态文字,静态图片)国际化问题,是通过资源文件来实现的。要实现国际化,需要做如下几项工作:1、定义web.xml的动ActionServlet的参数;2、定义资源文件;3、定义JSP页面的字符集合;4、在JSP页面获取资源文件里面的内容。
1、定义web.xml的动ActionServlet的参数

<servlet>
  
<servlet-name>action</servlet-name>
  
<servlet-class>org.apache.struts.action.ActionServlet</servlet-class>   
  
<init-param>
    
<param-name>config</param-name>
    
<param-value>/WEB-INF/struts-config.xml</param-value>
  
</init-param>
  
<init-param>
    
<param-name>application</param-name>
    
<param-value>ApplicationResources</param-value> <!-- 默认资源文件名 -->
  
</init-param>
  
<load-on-startup>2</load-on-startup>
</servlet>

2、定义资源文件
在/WEB-INF/classes下面添加UTF-8资源束文件。每一个资源文件是“键-值”对的集合。在JSP页面里面可以通过键来找到相应的数据值。本例子的文件名是ApplicationResources,所以相应的资源文件束是(包括e文,简体中文,繁体中文)
ApplicationResources.properties : 默认资源文件。当在其他资源文件里面找不到某个资源的时候,就使用该资源文件里面的定义。
ApplicationResources_zh_CN.properties:简体中文资源文件。
ApplicationResources_zh_TW.properties:繁体中文资源文件。

资源文件的格式为:默认资源文件名_国别_语言.properties。其中每个文件都是通过%JAVA_HONE%/BIN/native2ascii.exe工具转换而来。你也可以使用其他工具来处理得到(http://java.sun.com/products/jilkit/ 有一个工具Internationalization Java Internationalization and Localization Toolkit 可以处理)。下面是一个例子,我们显示如何使用%JAVA_HONE%/BIN/native2ascii.exe命令来定义资源束文件。
2.1 准备文件
//ApplicationResources.properties ;默认资源文件,通常里面的内容是英文的。
label.username=USERNAME :
label.password=PASSWORD :

//ApplicationResources_zh_CN.bak ;简体中文的资源文件。里面的内容是中文的。它需要工具将其中的内容处理成UTF-8
label.username=用户名 :
label.password=密  码 :

//ApplicationResources_zh_TW.bak : 繁体中文的资源文件。里面的内容是中文的。它需要工具将其中的内容处理成UTF-8,下面的内容是繁体码。
label.username=ノめ?W :
label.password=ノめ?W :

2.2 准备完成以后,使用如下的命令创建UTF-8资源文件束
native2ascii -encoding gb2312 ApplicationResources_zh_CN.bak ApplicationResources_zh_CN.properties
native2ascii -encoding big5 Applica tionResources_zh_TW.bak ApplicationResources_zh_TW.properties

3、定义JSP页面的字符集合
定义JSP页面的语言为UTF-8。在每个JSP页面,必须有如下的内容(如果使用的模板技术,则只是需要在模板页面添加,其他使用该模板的页面无需添加)
<%@ page contentType="text/html;charset=UTF-8"%>

4、在JSP页面获取资源文件里面的内容。
在JSP里面需要显示静态内容的地方使用<bean:message />strus的bean tag包里面的message标签。例如下面的页面

<table>
  <tr>
    <td align="right"><bean:message key="label.username" /></td>   
  </tr>
  <tr>
    <td align="right"><bean:message key="label.password" /></td>
  </tr>
</table>

好了,在这个页面显示的时候,如果客户的IE的语言集合是zh_CN的话,就会显示
用户名:
口  令:

如果是客户的IE的语言是zh_TW的话,就会显示
用户名:
用户名:

可以在IE的工具->Internet选项->语言的地方,来选择,定义IE的语言。

三、表单的数据的处理。
对于表单数据的处理,我们是通过添加一个Filter来实现的。所有提交的请求,都需要做字符处理。然后在web.xml里面定义该Filter。这样我们就不需要在程序里面做任何的字符处理。
3.1 定义Filter。下面是一个例子。
package com.webapps.commons;

import java.io.*;
import javax.servlet.*;

public class CharsetEncodingFilter implements Filter{
  private FilterConfig config = null;
  private String defaultEncode = "UTF-8";

  public void init(FilterConfig config) throws ServletException {
    this.config = config;
    if(config.getInitParameter("Charset")!=null){
        defaultEncode=config.getInitParameter("Charset");
    }
  }

  public void destroy() {
    this.config = null;
  }

  public void doFilter(ServletRequest request, ServletResponse response,
                       FilterChain chain) throws IOException, ServletException {
    ServletRequest srequest=request;
    srequest.setCharacterEncoding(defaultEncode);
    chain.doFilter(srequest,response);
  }
}

3.2 在web.xml里面声明使用该Filter
<filter>
  <filter-name>Character Encoding</filter-name>
  <filter-class>com.webapps.commons.CharsetEncodingFilter</filter-class>
</filter>
<filter-mapping>
  <filter-name>Character Encoding</filter-name>
  <url-pattern>/*</url-pattern>
</filter-mapping>

四、扩展
待续的是应用程序部分的国际化问题,和DB的国际化问题。


posted @ 2006-09-05 22:22 Alex 阅读(709) | 评论 (0)编辑 收藏

使用 AppFuse 的七个理由

学习 Java 开放源码工具 —— 并使用这些工具提高生产效率


级别: 初级

Matt Raible (mraible@virtuas.com), 开放源码实践先驱, Virtuas Open Source Solutions

2006 年 8 月 31 日

开 始学习在 Java™ 平台上使用诸如 Spring、Hibernate 或 MySQL 之类的开放源码工具时可能非常困难。再加上 Ant 或 Maven,以及与 DWR 一起的小 Ajax,还有 Web 框架 —— 即 JSF,我们必须睁大眼睛盯着如何配置应用程序。AppFuse 减少了集成开放源码项目的痛苦。它可以把测试变成一等公民,让我们可以从数据库表生成整个 UI,并使用 XFire 来支持 Web 服务。另外,AppFuse 的社区也非常健全,这是不同 Web 框架用户可以一起融洽相处的地方之一。

AppFuse 是一个开放源码的项目和应用程序,它使用了在 Java 平台上构建的开放源码工具来帮助我们快速而高效地开发 Web 应用程序。我最初开发它是为了减少在为客户构建新 Web 应用程序时所花费的那些不必要的时间。从核心上来说,AppFuse 是一个项目骨架,类似于通过向导创建新 Web 项目时 IDE 所创建的东西。当我们使用 AppFuse 创建一个项目时,它会提示我们将使用开放源码框架,然后才创建项目。它使用 Ant 来驱动测试、代码生成、编译和部署。它提供了目录和包结构,以及开发基于 Java 语言的 Web 应用程序所需要的库。

与大部分 “new project” 向导不同,AppFuse 创建的项目从最开始就包含很多类和文件。这些文件用来实现特性,不过它们同时也会在您开发应用程序时被用作示例。通过使用 AppFuse 启动新项目,我们通常可以减少一到两周的开发时间。我们不用担心如何将开放源码框架配置在一起,因为这都已经完成了。我们的项目都已提前配置来与数据库进 行交互,它会部署到应用服务器上,并对用户进行认证。我们不必实现安全特性,因为这都早已集成了。

当我最初开发 AppFuse 时,它只支持 Struts 和 Hibernate。经过几年的努力,我发现了比 Struts 更好的 Web 框架,因此我还添加了为这些 Web 框架使用的选项。现在,AppFuse 可以支持 Hibernate 或 iBATIS 作为持久性框架。对于 Web 框架来说,我们可以使用 JavaServer Faces(JSF)、Spring MVC、Struts、Tapestry 或 WebWork。

AppFuse 提供了很多应用程序需要的一些特性,包括:

  • 认证和授权
  • 用户管理
  • Remember Me(这会保存您的登录信息,这样就不用每次都再进行登录了)
  • 密码提醒
  • 登记和注册
  • SSL 转换
  • E-mail
  • URL 重写
  • 皮肤
  • 页面修饰
  • 模板化布局
  • 文件上载

这种 “开箱即用” 的功能是 AppFuse 与其他 CRUD 代 框架的区别之一(CRUD 取自创建、检索、更新删除 几个操作的英文首字母),包括 Ruby on Rails、Trails 和 Grails。上面提到的这些框架,以及 AppFuse,都让我们可以从数据库表或现有的模型对象中生成主页/细节页。

图 1 阐述了一个典型 AppFuse 应用程序的概念设计:


图 1. 典型的 AppFuse 应用程序
典型的 AppFuse 应用程序

清单 1 给出了我们在创建 devworks 项目时所使用的命令行交互操作,同时还给出了所生成的结果。这个项目使用了 WebWork 作为自己的 Web 框架(请参考下面 参考资料 一节给出的链接)。


清单 1. 使用 AppFuse 创建新项目
alotta:~/dev/appfuse mraible$ ant new
Buildfile: build.xml

clean:
[echo] Cleaning build and distribution directories

init:

new:
[echo]
[echo] +-------------------------------------------------------------+
[echo] | -- Welcome to the AppFuse New Application Wizard! -- |
[echo] | |
[echo] | To create a new application, please answer the following |
[echo] | questions. |
[echo] +-------------------------------------------------------------+

[input] What would you like to name your application [myapp]?
devworks
[input] What would you like to name your database [mydb]?
devworks
[input] What package name would you like to use [org.appfuse]?
com.ibm
[input] What web framework would you like to use [webwork,tapestry,spring,js
f,struts]?
webwork
[echo] Creating new application named 'devworks'...
[copy] Copying 359 files to /Users/mraible/Work/devworks
[copy] Copying 181 files to /Users/mraible/Work/devworks/extras
[copy] Copying 1 file to /Users/mraible/Work/devworks
[copy] Copying 1 file to /Users/mraible/Work/devworks

install:
[echo] Copying WebWork JARs to ../../lib
[copy] Copying 6 files to /Users/mraible/Work/devworks/lib
[echo] Adding WebWork entries to ../../lib.properties
[echo] Adding WebWork classpath entries
[echo] Removing Struts-specific JARs
[delete] Deleting directory /Users/mraible/Work/devworks/lib/struts-1.2.9
[delete] Deleting directory /Users/mraible/Work/devworks/lib/strutstest-2.1.3
[echo] Deleting struts_form.xdt for XDoclet
[delete] Deleting directory /Users/mraible/Work/devworks/metadata/templates
[echo] Deleting Struts merge-files in metadata/web
[delete] Deleting 7 files from /Users/mraible/Work/devworks/metadata/web
[echo] Deleting unused Tag Libraries and Utilities
[delete] Deleting 2 files from /Users/mraible/Work/devworks/src/web/org/appfu
se/webapp
[echo] Modifying appgen for WebWork
[copy] Copying 12 files to /Users/mraible/Work/devworks/extras/appgen
[echo] Replacing source and test files
[delete] Deleting directory /Users/mraible/Work/devworks/src/web/org/appfuse/
webapp/form
[delete] Deleting directory /Users/mraible/Work/devworks/src/web/org/appfuse/
webapp/action
[copy] Copying 13 files to /Users/mraible/Work/devworks/src
[delete] Deleting directory /Users/mraible/Work/devworks/test/web/org/appfuse
/webapp/form
[delete] Deleting directory /Users/mraible/Work/devworks/test/web/org/appfuse
/webapp/action
[copy] Copying 5 files to /Users/mraible/Work/devworks/test
[echo] Replacing web files (images, scripts, JSPs, etc.)
[delete] Deleting 1 files from /Users/mraible/Work/devworks/web/scripts
[copy] Copying 34 files to /Users/mraible/Work/devworks/web
[delete] Deleting: /Users/mraible/Work/devworks/web/WEB-INF/validator-rules-c
ustom.xml
[echo] Modifying Eclipse .classpath file
[echo] Refactoring build.xml
[echo] ----------------------------------------------
[echo] NOTE: It's recommended you delete extras/webwork as you shouldn't ne
ed it anymore.
[echo] ----------------------------------------------
[echo] Repackaging info written to rename.log
[echo]
[echo] +-------------------------------------------------------------+
[echo] | -- Application created successfully! -- |
[echo] | |
[echo] | Now you should be able to cd to your application and run: |
[echo] | > ant setup test-all |
[echo] +-------------------------------------------------------------+

BUILD SUCCESSFUL
Total time: 15 seconds

为什么使用 WebWork?
Struts 社区最近在热情地拥抱 WebWork,这种结合导致为 Java 平台提供了一个非常优秀的新 Web 框架:Struts 2。当然,Spring MVC 是一个非常优秀的基于请求的框架,但是它不能像 Struts 2 一样支持 JSF。基于内容的框架(例如 JSF 和 Tapestry)也都很好,但是我发现 WebWork 更为直观,更容易使用(更多有关 Structs 2 和 JSF 的内容请参看 参考资料)。

在创建一个新项目之后,我们就得到了一个类似于图 2 所示的目录结构。Eclipse 和 Intellij IDEA 项目文件都是作为这个过程的一部分创建的。


图 2. 项目的目录结构
项目的目录结构

这个目录结构与 Sun 为 Java 2 Platform Enterprise Edition(J2EE)Web 应用程序推荐的目录结构非常类似。在 2.0 版本的 AppFuse 中,这个结构会变化成适合 Apache Maven 项目的标准目录结构(有关这两个目录介绍的内容,请参看 参考资料 中的链接)。AppFuse 还会从 Ant 迁移到 Maven 2 上,从而获得相关下载的能力和对生成 IDE 项目文件的支持。目前基于 Ant 的系统要求提交者维护项目文件,而 Maven 2 可以通过简单地使用项目的 pom.xml 文件生成 IDEA、Eclipse 和 NetBeans 项目文件。(这个文件位于您项目的根目录中,是使用 Maven 构建应用程序所需要的主要组件)。它与利用 Ant 所使用的 build.xml 文件非常类似。)

现在我们对 AppFuse 是什么已经有一点概念了,在本文剩下的部分中,我们将介绍使用 AppFuse 的 7 点理由。即使您选择不使用 AppFuse 来开始自己的项目,也会看到 AppFuse 可以为您提供很多样板代码,这些代码可以在基于 Java 语言的 Web 应用程序中使用。由于它是基于 Apache 许可证的,因此非常欢迎您在自己的应用程序中重用这些代码。

理由 1:测试

测试是在软件开发项目中很少被给予足够信任的一个环节。注意我并不是说在软件开发的一些刊物中没有得到足够的信任!很多文章和案例研究都给出了测试 优先的开发方式和足够的测试覆盖面以提高软件的质量。然而,测试通常都被看作是一件只会延长项目开发时间的事情。实际上,如果我们使用测试优先的方法在编 写代码之前就开始撰写测试用例,我相信我们可以发现这实际上会加速 开发速度。另外,测试优先也可以使维护和重用更加 容易。如果我们不编写代码来测试自己的代码,那么就需要手工对应用程序进行测试 —— 这通常效率都不高。自动化才是关键。

当我们首次开始使用 AppFuse 时,我们可能需要阅读这个项目 Web 站点上提供的快速入门指南和教程(请参看 参考资料 中的链接)。这些教程的编写就是为了您可以首先编写测试用例;它们直到编写接口和/或实现之后才能编译。如果您有些方面与我一样,就会在开始编写代码之前 就已经编写好测试用例了;这是真正可以加速编写代码的惟一方式。如果您首先编写了代码的实现,通过某种方式验证它可以工作,那么您可能会对自己说,“哦, 看起来不错 —— 谁需要测试呢?我还有更多的代码需要编写!”这种情况不幸的一面是您通常都会做一些事情 来测试自己的代码;您简单地跳过了可以自动化进行测试的地方。

AppFuse 的文档展示了如何测试应用程序的所有 层次。它从数据库层开始入手,使用了 DbUnit(请参看 参考资料)在运行测试之前提前使用数据来填充自己的数据库。在数据访问(DAO)层,它使用了 Spring 的 AbstractTransactionalDataSourceSpringContextTests 类(是的,这的确是一个类的名字!)来允许简单地加载 Spring 上下文文件。另外,这个类对每个 testXXX() 方法封装了一个事务,并当测试方法存在时进行回滚。这种特性使得测试 DAO 逻辑变得非常简单,并且不会对数据库中的数据造成影响。

在服务层,jMock (请参看 参考资料)用来编写那些可以消除 DAO 依赖的真正 单元测试。这允许进行验证业务逻辑正确的快速测试;我们不用担心底层的持久性逻辑。

HtmlUnit 支持
HtmlUnit 团队在 1.8 发行版中已经完成了相当多的工作来确保包可以与流行的 Ajax 框架(Prototype 和 Scriptaculous)很好地工作。

在 Web 层,测试会验证操作(Struts/WebWork)、控件(Spring MVC)、页面(Tapestry)和管理 bean(JSF)如我们所期望的一样进行工作。Spring 的 spring-mock.jar 可以非常有用地用来测试所有这些框架,因为它包含了一个 Servlet API 的仿真实现。如果没有这个有用的库,那么测试 AppFuse 的 Web 框架就会变得非常困难。

UI 通常是开发 Web 应用程序过程中最为困难的一部分。它也是顾客最经常抱怨的地方 —— 这既是由于它并不是非常完美,也是由于它的工作方式与我们期望的并不一样。另外,没有什么会比在客户面前作演示的过程中看到看到异常堆栈更糟糕的了!您的 应用程序可能会非常可怕,但是客户可能会要求您做到十分完美。永远不要让这种事情发生。Canoo WebTest 可以对 UI 进行测试。它使用了 HtmlUnit 来遍历测试 UI,验证所有的元素都存在,并可以填充表单的域,甚至可以验证一个假想的启用 Ajax 的 UI 与我们预期的工作方式一样。(有关 WebTest 和 HTMLUnit 的链接请参看 参考资料。)

为了进一步简化 Web 的测试,Cargo(请参看 参考资料)对 Tomcat 的启动和停止(分别在运行 WebTest 测试之前和之后)进行了自动化。





理由 2:集成

正如我在本文简介中提到的一样,很多开放源码库都已经预先集成到 AppFuse 中了。它们可以分为以下几类:

  • 编译、报告和代码生成:Ant、Ant Contrib Tasks、Checkstyle、EMMA、Java2Html、PMD 和 Rename Packages
  • 测试框架:DbUnit、Dumbster、jMock、JUnit 和 Canoo WebTest
  • 数据库驱动程序:MySQL 和 PostgreSQL
  • 持久性框架:Hibernate 和 iBATIS
  • IoC 框架:Spring
  • Web 框架:JSF、Spring MVC、Struts、Tapestry 和 WebWork
  • Web 服务:XFire
  • Web 工具:Clickstream、Display Tag、DWR、JSTL、SiteMesh、Struts Menu 和 URL Rewrite Filter
  • Security:Acegi Security
  • JavaScript 和 CSS:Scriptaculous、Prototype 和 Mike Stenhouse 的 CSS Framework

除了这些库之外,AppFuse 还使用 Log4j 来记录日志,使用 Velocity 来构建 e-mail 和菜单模板。Tomcat 可以支持最新的开发,我们可以使用 1.4 或 5 版本的 Java 平台来编译或构建程序。我们应该可以将 AppFuse 部署到任何 J2EE 1.3 兼容的应用服务器上;这已经经过了测试,我们知道它在所有主要版本的 J2EE 服务器和所有主要的 servlet 容器上都可以很好地工作。

图 3 给出了上面创建的 devworks 项目的 lib 目录。这个目录中的 lib.properties 文件控制了每个依赖性的版本号,这意味着我们可以简单地通过把这些包的新版本放到这个目录中并执行诸如 ant test-all -Dspring.version=2.0 之类的命令来测试这些包的新版本。


图 3. 项目依赖性
AppFuse 项目依赖性

预先集成这些开放源码库可以在项目之初极大地提高生产效率。尽管我们可以找到很多文档介绍如何集成这些库,但是定制工作示例并简单地使用它来开发应用程序要更加简单。

除了可以简化 Web 应用程序的开发之外,AppFuse 让我们还可以将 Web 服务简单地集成到自己的项目中。尽管 XFire 也在 AppFuse 下载中一起提供了,不过如果我们希望,也可以自己集成 Apache Axis(请参看 参考资料 中有关 Axis 集成的教程)。另外,Spring 框架和 XFire 可以一起将服务层作为 Web 服务非常简单地呈现出来,这就为我们提供了开发面向服务架构的能力。

另外,AppFuse 并不会将我们限定到任何特定的 API 上。它只是简单地对可用的最佳开放源码解决方案重新进行打包和预先集成。AppFuse 中的代码可以处理这种集成,并实现了 AppFuse 的基本安全性和可用性特性。只要可能,就会减少代码,以便向 AppFuse 的依赖框架添加一个特性。例如,AppFuse 自带的 Remember Me 和 SSL 切换特性最近就因为类似的特性而从 Acegi Security 中删除了。







理由 3:自动化

Ant 使得简化了从编译到构建再到部署的自动化过程。Ant 是 AppFuse 中的一等公民,这主要是因为我发现在命令行中执行操作比从 IDE 中更加简单。我们可以使用 Ant 实现编译、测试、部署和执行任何代码生成的任务。

尽管这种能力对于有些人来说非常重要,但是它并不适用于所有的人。很多 AppFuse 用户目前都使用 Eclipse 或 Intellij IDEA 来构建和测试自己的项目。在这些 IDE 中运行 Ant 的确可以工作,但是这样做的效率通常都不如使用 IDE 内置的 JUnit 支持来运行测试效率高。

幸运的是,AppFuse 支持在 IDE 中运行测试,不过管理这种特性对于 AppFuse 开发人员来说就变得非常困难了。最大的痛苦在于 XDoclet 用来生成 Hibernate 映射文件和 Web 框架所使用的一些工件(例如 ActionForms 和 Struts 使用的 struts-config.xml)。IDE 并不知道需要生成的代码,除非我们配置使用 Ant 来编译它们,或者安装了一些可以认识 XDoclet 的插件。

这种对知识的缺乏是 AppFuse 2.0 切换到 JDK 5 和 Maven 2 上的主要原因。JDK 5、注释和 Struts 2 将让我们可以摆脱 XDoclet。Maven 2 将使用这些生成的文件和动态类路径来生成 IDE 项目文件,这样对项目的管理就可以进行简化。目前基于 Ant 的编译系统已经为不同的层次生成了一些工件(包括 dao.jar、service.jar 和 webapp.war),因此切换到 Maven 的模型上应该是一个非常自然的调整。

除了 Ant 之外(它对于编译、测试、部署和报告具有丰富的支持),对于 CruiseControl 的支持也构建到了 AppFuse 中。CruiseControl 是一个 Continuous Integration 应用程序,让我们可以在源代码仓库中代码发生变化时自动运行所有的测试。extras/cruisecontrol 目录包含了我们为基于 AppFuse 的项目快速、简单地设置 Continuous Integration 时所需要的文件。

设置 Continuous Integration 是软件开发周期中我们首先要做的事情之一。它不但激发程序员去编写测试用例,而且还通过 “You broke the build!” 游戏促进了团队之间的合作和融合。







理由 4:安全特性和可扩展性

AppFuse 最初是作为我为 Apress 编写的书籍 Pro JSP 中示例应用程序的一部分开发的。这个示例应用程序展示了很多安全特性和用于简化 Struts 开发的特性。这个应用程序中的很多安全特性在 J2EE 的安全框图中都不存在。使用容器管理认证(CMA)的认证方法非常简单,但是 Remember Me、密码提示、SSL 切换、登记和用户管理等功能却都不存在。另外,基于角色的保护方法功能在非 EJB 环境中也是不可能的。

最初,AppFuse 使用自己的代码和用于 CMA 的解决方案完全实现了这些特性。我在 2004 年年初开始学习 Spring 时就听说过有关 Acegi Security 的知识。我对 Acegi 所需要的 XML 的行数(175)与 web.xml 中所需要的 CMA 的行数(20)进行了比较,很快就决定丢弃 Acegi 了,因为它太过复杂了。

一年半之后 —— 在为另外一本书 Spring Live 中编写了一章有关使用 Acegi Security 的内容之后 —— 我就改变了自己的想法。Acegi 的确(目 前仍然)需要很多 XML,但是一旦我们理解了这一点,它实际上是相当简单的。当我们最终作出改变,使用 Acegi Security 的特性来全部取代 AppFuse 的特性之后,我们最终删除了大量的代码。类之上的类都已经没有了,“Acegi handles that now” 中消失的部分现在全部进入了 CVS 的 Attic 中了。

Acegi Security 是 J2EE 安全模型中曾经出现过的最好模型。它让我们可以实现很多有用的特性,这些特性在 Servlet API 的安全模型中都不存在:认证、授权、角色保护方法、Remember Me、密码加密、SSL 切换、用户切换和注销。它让我们还可以将用户证书存储到 XML 文件、数据库、LDAP 或单点登录系统(例如 Yale 的 Central Authentication Service (CAS) 或者 SiteMinder)中。

AppFuse 对很多与安全性相关的特性的实现从一开始都是非常优秀的。现在 AppFuse 使用了 Acegi Security,这些特性 —— 以及更多特性 —— 都非常容易实现。Acegi 有很多地方都可以进行扩充:这是它使用巨大的 XML 配置文件的原因。正如我们已经通过去年的课程对 Acegi 进行集成一样,我们已经发现对很多 bean 的定义进行定制可以更加紧密地与 AppFuse 建立联系。

Spring IoC 容器和 Acegi Security 所提供的简单开发、容易测试的代码和松耦合特性的组合是 AppFuse 是这么好的一种开发平台的主要原因。这些框架都是不可插入的,允许生成干净的可测试代码。AppFuse 集成了很多开放源码项目,依赖注入允许对应用程序层进行简单的集成。







理由 5:使用 AppGen 生成代码

有些人会将代码生成称为代码气味的散播(code smell)。在他们的观点中,如果我们需要生成代码,那么很可能就会做一些错事。我倾向于这种确定自己代码使用的模式和自动化生成代码的能力应该称为代码香味的弥漫(code perfume)。如果我们正在编写类似的 DAO、管理器、操作或控件,并且不想为它们生成代码,那么这就需要根据代码的气味来生成代码。当然,当语言可以为我们提供可以简化任务的特性时,一切都是那么美好;不过代码生成通常都是一个必需 —— 通常其生产率也非常高 —— 的任务。

AppFuse 中提供了一个基于 Ant 和 XDoclet 的代码生成工具,名叫 AppGen。默认情况下,常见的 DAO 和管理器都可以允许我们对任何普通老式 Java 对象(POJO)进行 CRUD 操作,但是在 Web 层上这样做有些困难。AppGen 有几个特性可以用来执行以下任务:

  • (使用 Middlegen 和 Hibernate 工具)从数据库表中生成 POJO
  • 从 POJO 生成 UI
  • 为 DAO、管理器、操作/控制器和 UI 生成测试

在运行 AppGen 时,您会看到提示说 AppGen 要从数据库表或 POJO 中生成代码。如果在命令行中执行 ant install-detailed,AppGen 就会安装 POJO 特定的 DAO、管理器以及它们的测试。运行 ant install 会导致 Web 层的类重用通用的 DAO 和默认存在的管理器。

为了阐述 AppGen 是如何工作的,我们在 devworks MySQL 数据库中创建了如清单 2 所示的表:


清单 2. 创建一个名为 cat 的数据库表
    create table cat (
cat_id int(8) auto_increment,
color varchar(20) not null,
name varchar(20) not null,
created_date datetime not null,
primary key (cat_id)
) type=InnoDB;

在 extras/appgen 目录中,运行 ant install-detailed。这个命令的输出结果对于本文来说实在太长了,不过我们在清单 3 中给出了第一部分的内容:


清单 3. 运行 AppGen 的 install-detailed 目标
$ ant install-detailed
Buildfile: build.xml

init:
[mkdir] Created dir: /Users/mraible/Work/devworks/extras/appgen/build
[echo]
[echo] +-------------------------------------------------------+
[echo] | -- Welcome to the AppGen! -- |
[echo] | |
[echo] | Use the "install" target to use the generic DAO and |
[echo] | Manager, or use "install-detailed" to general a DAO |
[echo] | and Manager specifically for your model object. |
[echo] +-------------------------------------------------------+

[input] Would you like to generate code from a table or POJO? (table,pojo)
table
[input] What is the name of your table (i.e. person)?
cat
[input] What is the name, if any, of the module for your table (i.e. organization)?

[echo] Running Middlegen to generate POJO...

要对 cat 表使用这个新生成的代码,我们需要修改 src/dao/com/ibm/dao/hibernate/applicationContext-hibernate.xml,来为 Hibernate 添加 Cat.hbm.xml 映射文件。清单 3 给出了我们修改后的 sessionFactory bean 的样子:


清单 4. 将 Cat.hbm.xml 添加到 sessionFactory bean 中
    <bean id="sessionFactory" class="...">
<property name="dataSource" ref="dataSource"/>
<property name="mappingResources">
<list>
<value>com/ibm/model/Role.hbm.xml</value>
<value>com/ibm/model/User.hbm.xml</value>
<value>com/ibm/model/Cat.hbm.xml</value>
</list>
</property>
...
</bean>

在运行 ant setup deploy 之后,我们就应该可以在部署的应用程序中对 cat 表执行 CRUD 操作了:


图 4. Cat 列表
所生成的主屏幕

图 5. Cat 表单
所生成的详细屏幕

我们在上面的屏幕快照中看到的记录都是作为代码生成的一部分创建的,因此现在就有测试数据了。








理由 6:文档

我们可以找到 AppFuse 各个风味的教程,并且它们都以 6 种不同的语言给出了:中文、德语、英语、韩语、葡萄牙语和西班牙语。使用风味(flavor) 一词,我的意思是不同框架的组合,例如 Spring MVC 加上 iBATIS、Spring MVC 加上 Hibernate 或 JSF 加上 Hibernate。使用这 5 种 Web 框架和两种持久框架,可以有好几种组合。有关它们的翻译,AppFuse 为自己的默认特性提供了 8 种翻译。可用语言包括中文、荷兰语、德语、英语、法语、意大利语、葡萄牙语和西班牙语。

除了核心教程之外,还添加了很多教程(请参看 参考资料) 来介绍与各种数据库、应用服务器和其他开放源码技术(包括 JasperReports、Lucene、Eclipse、Drools、Axis 和 DWR)的集成。





理由 7:社区

Apache 软件基金会对于开放源码有一个有趣的看法。它对围绕开放源码项目开发一个开放源码社区最感兴趣。它的成员相信如果社区非常强大,那么产生高质量的代码就是一个自然的过程。下面的内容引自 Apache 主页:

“我们认为自己不仅仅是一组共享服务器的项目,而且是一个开发人员和用户的社区。”

AppFuse 社区从 2003 年作为 SourceForge 上的一个项目(是 struts.sf.net 的一部分)启动以来,已经获得了极大的增长。通过在 2004 年 3 月转换到 java.net 上之后,它已经成为这里一个非常流行的项目,从 2005 年 1 月到 3 月成为访问量最多的一个项目。目前它仍然是一个非常流行的项目(有关 java.net 项目统计信息的链接,请参看 参考资料),不过在这个站点上它正在让位于 Sun 赞助的很多项目。

在 2004 年年末,Nathan Anderson 成为继我之后第一个提交者。此后有很多人都加入了进来,包括 Ben Gill、David Carter、Mika G?ckel、Sanjiv Jivan 和 Thomas Gaudin。很多现有的提交者都已经通过各种方式作出了自己的贡献,他们都帮助 AppFuse 社区成为一个迅速变化并且非常有趣的地方。

邮件列表非常友好,我们试图维护这样一条承诺 “没有问题是没有人理会的问题”。我们的邮件列表归档文件中惟一一条 “RTFM” 是从用户那里发出的,而不是从开发者那里发出的。我们绝对信奉 Apache 开放源码社区的哲学。引用我最好的朋友 Bruce Snyder 的一句话,“我们为代码而来,为人们而留下”。目前,大部分开发者都是用户,我们通常都喜欢有一段美妙的时间。另外,大部分文档都是由社区编写的;因此, 这个社区的知识是非常渊博的。







结束语

我们应该尝试使用 AppFuse 进行开发,这是因为它允许我们简单地进行测试、集成、自动化,并可以安全地生成 Web 应用程序。其文档非常丰富,社区也非常友好。随着其支撑框架越来越好,AppFuse 也将不断改进。

从 AppFuse 2.0 开始,我们计划迁移到 JDK 5(仍然支持部署到 1.4)和 Maven 2 上去。这些工具可以简化使用 AppFuse 的开发、安装和升级。我们计划充分利用 Maven 2 的功能来处理相关依赖性。我们将碰到诸如 appfuse-hibernate-2.0.jar 和 appfuse-jsf-2.0.jar 之类的工件。这些工件都可以在 pom.xml 文件中进行引用,它们负责提取其他相关依赖性。除了在自己的项目中使用 AppFuse 基类之外,我们还可以像普通的框架一样在 JAR 中对这些类简单地进行扩展,这应该会大大简化它的升级过程,并鼓励更多用户将自己希望的改进提交到这个项目中。

如果没有其他问题,使用 AppFuse 可以让您始终处于 Java Web 开发的技术前沿上 —— 就像我们一样!







参考资料

学习

获得产品和技术
  • AppFuse on java.net:下载不同风味的 AppFuse。

  • WebWork:了解这个易于使用的 Web 框架。

  • DbUnit:查看更多有关 JUnit 扩展的内容。

  • jMock:创建动态仿真对象来简化真正的单元测试。

  • Canoo WebTest:自动化 Web 应用程序的 UI 测试。

  • HtmlUnit:WebTest 的优秀 JavaScript 支持背后的基础。

  • Cargo:自动启动和停止容器。

  • Greenbox:一种代码生成框架。


讨论






关于作者

Matt Raible 居住在美国科罗拉多州的丹佛,他在那里是 Spring 和 Web 框架对 Virtuas Open Source Solutions 的实践先驱。他在开放源码领域具有丰富的经验,是这个领域的专家。他在这个领域中既是用户,又是一名开发人员。Matt 是 SourceBeat PublishingSpring Live 的作者。他还为 Apress 的书籍 Pro JSP Third Edition 作出了很大的贡献。他是很多开放源码会议的积极倡导者,包括 ApacheCon、MySQL User's Conference 和 OSCON,同时他还是 http://raibledesigns.com 上一名非常活跃的博客。Raible 的大部分生活都被计算机所包围了,尽管他是在连电都没有的 Montana 长大的。当不工作的时候时,他总是试图让妻子 Julie 成为世界上最幸福的女人,或者与他们的孩子 Abbie 和 Jack 一起玩耍。

posted @ 2006-09-04 20:34 Alex 阅读(581) | 评论 (0)编辑 收藏

外观模式(Facade pattern)涉及到子系统的一些类。所谓子系统,是为提供一系列相关的特征(功能)而紧密关联的一组类。例如,一个Account类、Address类和CreditCard类相互关联,成为子系统的一部分,提供在线客户的特征。

   在真实的应用系统中,一个子系统可能由很多类组成。子系统的客户为了它们的需要,需要和子系统中的一些类进行交互。客户和子系统的类进行直接的交互会导 致客户端对象和子系统(Figure1)之间高度耦合。任何的类似于对子系统中类的接口的修改,会对依赖于它的所有的客户类造成影响。

  
Figure1: Client Interaction with Subsystem Classes before Applying the Facade Pattern

  外观模式(Facade pattern)很适用于在上述情况。外观模式(Facade pattern)为子系统提供了一个更高层次、更简单的接口,从而降低了子系统的复杂度和依赖。这使得子系统更易于使用和管理。

  外观是一个能为子系统和客户提供简单接口的类。当正确的应用外观,客户不再直接和子系统中的类交互,而是与外观交互。外观承担与子系统中类交互的责任。实际上,外观是子系统与客户的接口,这样外观模式降低了子系统和客户的耦合度(Figure2).

  
Figure2: Client Interaction with Subsystem Classes after Applying the Facade Pattern

  从Figure2中我们可以看到:外观对象隔离了客户和子系统对象,从而降低了耦合度。当子系统中的类进行改变时,客户端不会像以前一样受到影响。

  尽管客户使用由外观提供的简单接口,但是当需要的时候,客户端还是可以视外观不存在,直接访问子系统中的底层次的接口。这种情况下,它们之间的依赖/耦合度和原来一样。

  例子:

  让我们建立一个应用:

  (1) 接受客户的详细资料(账户、地址和信用卡信息)

  (2) 验证输入的信息

  (3) 保存输入的信息到相应的文件中。

  这个应用有三个类:Account、Address和CreditCard。每一个类都有自己的验证和保存数据的方法。

  Listing1: AccountClass

public class Account {
 String firstName;
 String lastName;
 final String ACCOUNT_DATA_FILE = "AccountData.txt";
 public Account(String fname, String lname) {
  firstName = fname;
  lastName = lname;
 }
 public boolean isValid() {
  /*
  Let's go with simpler validation
  here to keep the example simpler.
  */
  …
  …
 }
 public boolean save() {
  FileUtil futil = new FileUtil();
  String dataLine = getLastName() + ”," + getFirstName();
  return futil.writeToFile(ACCOUNT_DATA_FILE, dataLine,true, true);
 }
 public String getFirstName() {
  return firstName;
 }
 public String getLastName() {
  return lastName;
 }
}

  Listing2: Address Class

public class Address {
 String address;
 String city;
 String state;
 final String ADDRESS_DATA_FILE = "Address.txt";
 public Address(String add, String cty, String st) {
  address = add;
  city = cty;
  state = st;
 }
 public boolean isValid() {
  /*
  The address validation algorithm
  could be complex in real-world
  applications.
  Let's go with simpler validation
  here to keep the example simpler.
  */
  if (getState().trim().length() < 2)
   return false;
  return true;
 }
 public boolean save() {
  FileUtil futil = new FileUtil();
  String dataLine = getAddress() + ”," + getCity() + ”," + getState();
  return futil.writeToFile(ADDRESS_DATA_FILE, dataLine,true, true);
 }
 public String getAddress() {
  return address;
 }
 public String getCity() {
  return city;
 }
 public String getState() {
  return state;
 }
}

  Listing3: CreditCard Class

public class CreditCard {
 String cardType;
 String cardNumber;
 String cardExpDate;
 final String CC_DATA_FILE = "CC.txt";
 public CreditCard(String ccType, String ccNumber,
 String ccExpDate) {
  cardType = ccType;
  cardNumber = ccNumber;
  cardExpDate = ccExpDate;
 }
 public boolean isValid() {
  /*
  Let's go with simpler validation
  here to keep the example simpler.
  */
  if (getCardType().equals(AccountManager.VISA)) {
   return (getCardNumber().trim().length() == 16);
  }
  if (getCardType().equals(AccountManager.DISCOVER)) {
   return (getCardNumber().trim().length() == 15);
  }
  if (getCardType().equals(AccountManager.MASTER)) {
   return (getCardNumber().trim().length() == 16);
  }
  return false;
 }
 public boolean save() {
  FileUtil futil = new FileUtil();
  String dataLine = getCardType() + ,”" + getCardNumber() + ”," + getCardExpDate();
  return futil.writeToFile(CC_DATA_FILE, dataLine, true, true);
 }
 public String getCardType() {
  return cardType;
 }
 public String getCardNumber() {
  return cardNumber;
 }
 public String getCardExpDate() {
  return cardExpDate;
 }
}

  
Figure3: Subsystem Classes to Provide the Necessary Functionality to Validate and Save the Customer Data



让我们建立一个客户AccountManager,它提供用户输入数据的用户界面。

  Listing4: Client AccountManager Class

public class AccountManager extends JFrame {
 public static final String newline = "\n";
 public static final String VALIDATE_SAVE = "Validate & Save";
 …
 …
 public AccountManager() {
  super(" Facade Pattern - Example ");
  cmbCardType = new JComboBox();
  cmbCardType.addItem(AccountManager.VISA);
  cmbCardType.addItem(AccountManager.MASTER);
  cmbCardType.addItem(AccountManager.DISCOVER);
  …
  …
  //Create buttons
  JButton validateSaveButton = new JButton(AccountManager.VALIDATE_SAVE);
  …
  …
 }
 public String getFirstName() {
  return txtFirstName.getText();
 }
 …
 …
}//End of class AccountManager

  当客户AccountManage运行的时候,展示的用户接口如下:

  
Figure4: User Interface to Enter the Customer Data

  为了验证和保存输入的数据,客户AccountManager需要:

  (1) 建立Account、Address和CreditCard对象。

  (2) 用这些对象验证输入的数据

  (3) 用这些对象保存输入的数据。

  下面是对象间的交互顺序图:

 
Figure5: How a Client Would Normally Interact (Directly) with Subsystem Classes to Validate and Save the Customer Data

   在这个例子中应用外观模式是一个很好的设计,它可以降低客户和子系统组件(Address、Account和CreditCard)之间的耦合度。应用 外观模式,让我们定义一个外观类CustomerFacade (Figure6 and Listing5)。它为由客户数据处理类(Address、Account和CreditCard)所组成的子系统提供一个高层次的、简单的接口。

CustomerFacade
address:String
city:String
state:String
cardType:String
cardNumber:String
cardExpDate:String
fname:String
lname:String
setAddress(inAddress:String)
setCity(inCity:String)
setState(inState:String)
setCardType(inCardType:String)
setCardNumber(inCardNumber:String)
setCardExpDate(inCardExpDate:String)
setFName(inFName:String)
setLName(inLName:String)
saveCustomerData()

  
Figure6: Facade Class to Be Used by the Client in the Revised Design

  Listing5: CustomerFacade Class

public class CustomerFacade {
 private String address;
 private String city;
 private String state;
 private String cardType;
 private String cardNumber;
 private String cardExpDate;
 private String fname;
 private String lname;
 public void setAddress(String inAddress) {
  address = inAddress;
 }
 public void setCity(String inCity) {
  city = inCity;
 }
 public void setState(String inState) {
  state = inState;
 }
 public void setFName(String inFName) {
  fname = inFName;
 }
 public void setLName(String inLName) {
  lname = inLName;
 }
 public void setCardType(String inCardType) {
  cardType = inCardType;
 }
 public void setCardNumber(String inCardNumber) {
  cardNumber = inCardNumber;
 }
 public void setCardExpDate(String inCardExpDate) {
  cardExpDate = inCardExpDate;
 }
 public boolean saveCustomerData() {
  Address objAddress;
  Account objAccount;
  CreditCard objCreditCard;
  /*
   client is transparent from the following
   set of subsystem related operations.
  */
  boolean validData = true;
  String errorMessage = "";
  objAccount = new Account(fname, lname);
  if (objAccount.isValid() == false) {
   validData = false;
   errorMessage = "Invalid FirstName/LastName";
  }
  objAddress = new Address(address, city, state);
  if (objAddress.isValid() == false) {
   validData = false;
   errorMessage = "Invalid Address/City/State";
  }
  objCreditCard = new CreditCard(cardType, cardNumber, cardExpDate);
  if (objCreditCard.isValid() == false) {
   validData = false;
   errorMessage = "Invalid CreditCard Info";
  }
  if (!validData) {
   System.out.println(errorMessage);
   return false;
  }
  if (objAddress.save() && objAccount.save() && objCreditCard.save()) {
   return true;
  } else {
   return false;
  }
 }
}

   CustomerFacade类以saveCustomData方法的形式提供了业务层次上的服务。客户AccountManager不是直接和子系统 的每一个组件交互,而是使用了由CustomFacade对象提供的验证和保存客户数据的更高层次、更简单的接口(Figure7).

 
Figure7: Class Association with the Fa?ade Class in Place 。

  在新的设计中,为了验证和保存客户数据,客户需要:

  (1) 建立或获得外观对象CustomFacade的一个实例。

  (2) 传递数据给CustomFacade实例进行验证和保存。

  (3) 调用CustomFacade实例上的saveCustomData方法。

  CustomFacade处理创建子系统中必要的对象并且调用这些对象上相应的验证、保存客户数据的方法这些细节问题。客户不再需要直接访问任何的子系统中的对象。

  Figure8展示了新的设计的消息流图:

 
Figure 22.8: In the Revised Design, Clients Interact with the Fa?ade Instance to Interface with the Subsystem

  重要提示

  下面是应用外观模式的注意事项:

  (1) 在设计外观时,不需要增加额外的功能。

  (2) 不要从外观方法中返回子系统中的组件给客户。例如:有一个下面的方法:

  CreditCard getCreditCard()

  会报漏子系统的细节给客户。应用就不能从应用外观模式中取得最大的好处。

  (3)应用外观的目的是提供一个高层次的接口。因此,外观方法最适合提供特定的高层次的业务服务,而不是进行底层次的单独的业务执行。
posted @ 2006-09-03 00:07 Alex 阅读(477) | 评论 (0)编辑 收藏

小结:适配器模式用插座的适配器最为形象,插头是2口的,插座是3口的,中间的适配器就是同时支持2口和三口的。从对象的角度就是一般继承一个实现一个,总之,前方百计把两者都关联起来 。


通常,客户类(clients of class)通过类的接口访问它提供的服务。有时,现有的类(existing class)可以提供客户类的功能 需要,但是它所提供的接口不一定是客户类所期望的。这是由于现有的接口太详细或者缺乏详细或接口的名称与客户类所查找的不同等诸多不同原因导致的。

  在这种情况下,现有的接口需要转化(convert) 为客户类期望的接口,这样保证了对现有类的重用。如果不进行这样的转化,客户类就不能利用现有类所提供的功能。适配器模式 (Adapter Pattern)可以完成这样的转化。适配器模式建议定义一个包装类,包装有不兼容接口的对象。这个包装类指的就是适配器 (Adapter),它包装的对象就是适配者(Adaptee)。适配器提供客户类需要的接口,适配器接口的实现是把客户类的请求转化为对适配者的相应接 口的调用。换句话说:当客户类调用适配器的方法时,在适配器类的内部调用适配者类的方法,这个过程对客户类是透明的,客户类并不直接访问适配者类。因此, 适配器可以使由于借口不兼容而不能交互的类可以一起工作(work together)。

  在上面讨论的接口:

  (1)    不是指在JAVA编程语言中接口的概念,虽然类的接口可以通过JAVA借扩来定义。

  (2)    不是指由窗体和GUI控件所组成的GUI应用程序的用户接口。

  (3)    而是指类所报漏的,被其他类调用的编程接口,

  类适配器(Class Adapter)VS对象适配器(Object Adapter)

  适配器总体上可以分为两类??类适配器(Class Adapter)VS对象适配器(Object Adapter)
    

 类适配器:


  类适配器是通过继承类适配者类(Adaptee Class)实现的,另外类适配器实现客户类所需要的接口。当客户对象调用适配器类方法的时候,适配器内部调用它所继承的适配者的方法。
    

 对象适配器:

  对象适配器包含一个适配器者的引用(reference),与类适配器相同,对象适配器也实现了客户类需要的接口。当客户对象调用对象适配器的方法的时候,对象适配器调它所包含的适配器者实例的适当方法。

  下表是类适配器(Class Adapter)和对象适配器(Object Adapter)的详细不同

  
    
  例子:

  让我们建立一个验证给定客户地址的应用。这个应用是作为大的客户数据管理应用的一部分。

  让我们定义一个Customer类:

Customer 



Figure 20.1: Customer Class 
Listing 20.1: Customer Class 
  1. class Customer { 
  2.   public static final String US = "US"
  3.   public static final String CANADA = "Canada"
  4.   private String address; 
  5.   private String name; 
  6.   private String zip, state, type; 
  7.   public boolean isValidAddress() { 
  8.           … 
  9.           … 
  10.   } 
  11.   public Customer(String inp_name, String inp_address, 
  12.                   String inp_zip, String inp_state, 
  13.                   String inp_type) { 
  14.     name = inp_name; 
  15.     address = inp_address; 
  16.     zip = inp_zip; 
  17.     state = inp_state; 
  18.     type = inp_type; 
  19.   } 
  20. }//end of class 
   不同的客户对象创建Customer对象并调用(invoke)isValidAddress方法验证客户地址的有效性。为了验证客户地址的有效性, Customer类期望利用一个地址验证类(address validator class),这个验证类提供了在接口 AddressValidator中声明的接口。

  Listing 20.2: AddressValidator as an Interface 
  1. public interface AddressValidator { 
  2.   public boolean isValidAddress(String inp_address, 
  3.      String inp_zip, String inp_state); 
  4. }//end of class 

  让我们定义一个USAddress的验证类,来验证给定的U.S地址。

  Listing 20.3: USAddress Class 
  1. class USAddress implements AddressValidator { 
  2.   public boolean isValidAddress(String inp_address, 
  3.      String inp_zip, String inp_state) { 
  4.    if (inp_address.trim().length() < 10) 
  5.      return false
  6.    if (inp_zip.trim().length() < 5) 
  7.      return false
  8.    if (inp_zip.trim().length() > 10) 
  9.      return false
  10.    if (inp_state.trim().length() != 2) 
  11.      return false
  12.    return true
  13.   } 
  14. }//end of class 

  USAddress类实现AddressValidator接口,因此Customer对象使用USAddress实例作为验证客户地址过程的一部分是没有任何问题的。

  Listing 20.4: Customer Class Using the USAddress Class 
  1. class Customer { 
  2.           … 
  3.           … 
  4.  public boolean isValidAddress() { 
  5.    //get an appropriate address validator 
  6.    AddressValidator validator = getValidator(type); 
  7.    //Polymorphic call to validate the address 
  8.    return validator.isValidAddress(address, zip, state); 
  9.  } 
  10.  private AddressValidator getValidator(String custType) { 
  11.    AddressValidator validator = null
  12.    if (custType.equals(Customer.US)) { 
  13.      validator = new USAddress(); 
  14.    } 
  15.    return validator; 
  16.  } 
  17. }//end of class 
 


Figure 20.2: Customer/USAddress Validator?Class Association 

  但是当验证来自加拿大的客户时,就要对应用进行改进。这需要一个验证加拿大客户地址的验证类。让我们假设已经存在一个用来验证加拿大客户地址的使用工具类CAAddress。

从下面的CAAdress类的实现,可以发现CAAdress提供了客户类Customer类所需要的验证服务。但是它所提供的接口不用于客户类Customer所期望的。

  Listing 20.5: CAAdress Class with Incompatible Interface 
  1. class CAAddress { 
  2.   public boolean isValidCanadianAddr(String inp_address, 
  3.      String inp_pcode, String inp_prvnc) { 
  4.    if (inp_address.trim().length() < 15) 
  5.      return false
  6.    if (inp_pcode.trim().length() != 6) 
  7.      return false
  8.    if (inp_prvnc.trim().length() < 6) 
  9.      return false
  10.    return true
  11.   } 
  12. }//end of class 

  CAAdress类提供了一个isValidCanadianAddr方法,但是Customer期望一个声明在AddressValidator接口中的isValidAddress方法。

  接口的不兼容使得Customer对象利用现有的CAAdress类是困难的。一种意见是改变CAAdress类的接口,但是可能会有其他的应用正在使用CAAdress类的这种形式。改变CAAdress类接口会影响现在使用CAAdress类的客户。

  应用适配器模式,类适配器CAAdressAdapter可以继承CAAdress类实现AddressValidator接口。

 


  Figure 20.3: Class Adapter for the CAAddress Class 
Listing 20.6: CAAddressAdapter as a Class Adapter 
  1. public class CAAddressAdapter extends CAAddress 
  2.   implements AddressValidator { 
  3.   public boolean isValidAddress(String inp_address, 
  4.      String inp_zip, String inp_state) { 
  5.     return isValidCanadianAddr(inp_address, inp_zip, 
  6.            inp_state); 
  7.   } 
  8. }//end of class 

   因为适配器CAAdressAdapter实现了AddressValidator接口,客户端对象访问适配器CAAdressAdapter对象是没 有任何问题的。当客户对象调用适配器实例的isValidAddress方法的时候,适配器在内部把调用传递给它继承的 isValidCanadianAddr方法。

  在Customer类内部,getValidator私有方法需要扩展,以至于它可以 在验证加拿大客户的时候返回一个CAAdressAdapter实例。返回的对象是多态的,USAddress和CAAddressAdapter都实现 了AddressValidator接口,所以不用改变。

Listing 20.7: Customer Class Using the CAAddressAdapter Class 
  1. class Customer { 
  2.           … 
  3.           … 
  4.   public boolean isValidAddress() { 
  5.     //get an appropriate address validator 
  6.     AddressValidator validator = getValidator(type); 
  7.     //Polymorphic call to validate the address 
  8.     return validator.isValidAddress(address, zip, state); 
  9.   } 
  10.   private AddressValidator getValidator(String custType) { 
  11.     AddressValidator validator = null
  12.     if (custType.equals(Customer.US)) { 
  13.       validator = new USAddress(); 
  14.     } 
  15.     if (type.equals(Customer.CANADA)) { 
  16.       validator = new CAAddressAdapter(); 
  17.     } 
  18.     return validator; 
  19.   } 
  20. }//end of class 
  CAAddressAdapter设计和对AddressValidator(声明期望的接口)对象的多态调用使Customer可以利用接口不兼容CAAddress类提供的服务。

 


  Figure 20.4: Address Validation Application?Using Class Adapter 

 


  Figure 20.5: Address Validation Message Flow?Using Class Adapter 

  作为对象适配器的地址适配器

   当讨论以类适配器来实现地址适配器时,我们说客户类期望的AddressValidator接口是Java接口形式。现在,让我们假设客户类期望 AddressValidator接口是抽象类而不是java接口。因为适配器CAAdapter必须提供抽象类AddressValidatro中声明 的接口,适配器必须是AddressValidator抽象类的子类、实现抽象方法。
  1. Listing 20.8: AddressValidator as an Abstract Class 
  2. public abstract class AddressValidator { 
  3.   public abstract boolean isValidAddress(String inp_address, 
  4.      String inp_zip, String inp_state); 
  5. }//end of class 
  6. Listing 20.9: CAAddressAdapter Class 
  7. class CAAddressAdapter extends AddressValidator { 
  8.           … 
  9.           … 
  10.   public CAAddressAdapter(CAAddress address) { 
  11.     objCAAddress = address; 
  12.   } 
  13.   public boolean isValidAddress(String inp_address, 
  14.      String inp_zip, String inp_state) { 
  15.           … 
  16.           … 
  17.   } 
  18. }//end of class 

  因为多继承在JAVA中不支持,现在适配器CAAddressAdapter不能继承现有的CAAddress类,它已经使用了唯一一次继承其他类的机会。

  应用对象适配器模式,CAAddressAdapter可以包含一个适配者CAAddress的一个实例。当适配器第一次创建的时候,这个适配者的实例通过客户端传递给适配器。通常,适配者实例可以通过下面两种方式提供给包装它的适配器。

  (1)    对象适配器的客户端可以传递一个适配者的实例给适配器。这种方式在选择类的形式上有很大的灵活性,但是客户端感知了适配者或者适配过程。这种方法在适配器不但需要适配者对象行为而且需要特定状态时很适合。

  (2)    适配器可以自己创建适配者实例。这种方法相对来说缺乏灵活性。适用于适配器只需要适配者对象的行为而不需要适配者对象的特定状态的情况。

 


  Figure 20.6: Object Adapter for the CAAddress Class 

  Listing 20.10: CAAddressAdapter as an Object Adapter 
  1. class CAAddressAdapter extends AddressValidator { 
  2.   private CAAddress objCAAddress; 
  3.   public CAAddressAdapter(CAAddress address) { 
  4.     objCAAddress = address; 
  5.   } 
  6.   public boolean isValidAddress(String inp_address, 
  7.      String inp_zip, String inp_state) { 
  8.     return objCAAddress.isValidCanadianAddr(inp_address, 
  9.            inp_zip, inp_state); 
  10.   } 
  11. }//end of class 

  当客户对象调用CAAddressAdapter(adapter)上的isValidAddress方法时, 适配器在内部调用CAAddress(adaptee)上的isValidCanadianAddr方法。


 


  Figure 20.7: Address Validation Application?Using Object Adapter 

  从这个例子可以看出,适配器可以使Customer(client)类访问借口不兼容的CAAddress(adaptee)所提供的服务!

 



  Figure 20.8: Address Validation Message Flow?Using Object Adapter
posted @ 2006-09-03 00:02 Alex 阅读(1031) | 评论 (0)编辑 收藏

Template模板模式定义:
定义一个操作中算法的骨架,将一些步骤的执行延迟到其子类中.

使用Java的抽象类时,就经常会使用到Template模式,因此Template模式使用很普遍.而且很容易理解和使用。

 

public abstract class Benchmark
{
  /**
  * 下面操作是我们希望在子类中完成
  */
  public abstract void benchmark();

  /**
  * 重复执行benchmark次数
  */
  public final long repeat (int count) {
    if (count <= 0)
      return 0;
    else {
      long startTime = System.currentTimeMillis();

    for (int i = 0; i < count; i++)
      benchmark();

    long stopTime = System.currentTimeMillis();
    return stopTime - startTime;
  }
}
}

在上例中,我们希望重复执行benchmark()操作,但是对benchmark()的具体内容没有说明,而是延迟到其子类中描述:

public class MethodBenchmark extends Benchmark
{
  /**
  * 真正定义benchmark内容
  */
  public void benchmark() {

    for (int i = 0; i < Integer.MAX_VALUE; i++){
      System.out.printtln("i="+i);    
    }
  }
}

至此,Template模式已经完成,是不是很简单?

我们称repeat方法为模板方法, 它其中的benchmark()实现被延迟到子类MethodBenchmark中实现了,

看看如何使用:

Benchmark operation = new MethodBenchmark();
long duration = operation.repeat(Integer.parseInt(args[0].trim()));
System.out.println("The operation took " + duration + " milliseconds");

 

也许你以前还疑惑抽象类有什么用,现在你应该彻底明白了吧? 至于这样做的好处,很显然啊,扩展性强,以后Benchmark内容变化,我只要再做一个继承子类就可以,不必修改其他应用代码.

posted @ 2006-09-02 23:16 Alex 阅读(300) | 评论 (0)编辑 收藏

仅列出标题
共15页: First 上一页 3 4 5 6 7 8 9 10 11 下一页 Last