2006年05月24日

RUP是Rational统一过程(Rational Unified Process)的简称,它是Rational公司(现归属IBM公司)推出的一种软件过程产品。从软件过程模式角度看,RUP又是一种典型的软件过程模式,它以迭代增量式、架构为中心、用例驱动的软件开发方法为主要特征,其中以用例驱动乃是贯穿软件开发始终的方法,而将其应用于需求管理自然是首当其冲的课题。

 
  软件需求是一个软件系统必须完成或达到的目标总和,需求管理是指了解、记录、追踪不断变化的需求的一个系统化方法。至今许多软件开发的实践都表明,有无良好的需求管理是整个软件项目成败的至关重要的一步,是与项目得失关系最大的首要因素。RUP把需求定义为“(正在构建的)系统必须符合的条件或具备的功能”,并描述了如何提取、组织需要的功能和限制;跟踪和文档化适中方案和决策;捕获和进行商业需求交流。在此过程中的用例和场景(用例的实例)的使用,已被证明是捕获功能性需求的卓越方法,并确保由它们来驱动软件的设计、实现和测试,使最终系统更能满足最终用户的需要。下面对此稍作具体的分析。
 
                         一、用例驱动较之功能分解的优势
   
  在不同行业中,都有很多机会足以去发明产品,或改善制造和管理的过程,而把握机会的一个重要方法就是从问题中找机会。Rational正是一家善于思考问题的企业。
 
  长期以来,在软件工程实践中普遍存在这样一种认识——用户知道需求是什么,开发人员要做的就是从他们那里得到需求,并由此做出系统功能的说明。基于这样的认识和思想指导,传统的软件需求规约采用的基本上都是以功能分解的方式来描述系统功能。在这种表述方式中,系统功能被分解到各个系统功能模块中,通过描述细分的系统模块功能来达到描述整个系统功能的目的。但采用这种方法来描述系统需求,非常容易混淆需求和设计的界限,其表述中实际上已经包含了部分的设计在内。由此常常导致这样的迷惑:系统需求应该详细到何种程度?极端的情况就是需求可以详细到概要设计,因为这样的需求表述既包含了外部需求也包含了内部设计。以往在有些公司的开发流程中,对于需求就有"内部需求"(实为内部设计)和"外部需求"之区分与称谓。

  功能分解方法的另一个缺点是它分割了各项系统功能的应用环境,从各个功能项入手,你就很难了解到这些功能项是如何相互关联来实现一个完整的系统服务的。所以在传统的SRS文档中,我们往往需要另外一些章节来描述系统的整体结构及各部分之间的相互关联,这些内容使得SRS需求更像是一个设计文档。

  通过软件开发的反复实践后发现,任何系统都会有很多用户(或不同类型的用户),每个用户只知道其个人如何使用系统,他们并不知道也不想了解系统的内部结构、设计及整体运行情况,他们所关心的只是系统所能提供的服务,也就是被开发出来的系统将是如何被使用的,这就是Rational思考问题的角度,也是创制用例方法的基本指导思想。

  用例方法是完全站在用户的角度上(即从系统的外部)来描述系统的功能,所要回答的问题是“系统应该为每个用户做什么”。它把被定义的系统看作是一个黑箱,先不去关心系统内部是如何完成它所提供的功能,而仅描述被定义系统有哪些外部使用者(抽象成为Actor)会与其发生交互;针对每一参与者,又描述系统为这些参与者提供了什么样的服务(抽象成为Use Case),或者说系统是如何被这些参与者使用的。在所有用例构成的用例模型中,判断系统各项功能是否重要或有价值的标准,是考虑系统为每个用户提供的价值,包括该功能辅助哪个用户进行工作?需要提供什么业务?能够为业务增加多少价值?因此,用例能够用于指导找出每个用户所需要的功能,避免提出用户根本就不需要的冗余功能,从而有效界定系统的范围和行为,使整个系统的业务为每个用户提供最大的价值。

  与传统的功能分解方式相比,用例方法全都是从外部来定义系统功能,它把需求与设计完全分离开来。在面向对象的分析设计方法中,用例模型主要用于表述系统的功能性需求,系统的设计主要由对象模型来记录表述。此外,用例定义了系统功能的使用环境与上下文,每一个用例描述的是一个完整的系统服务。

  在RUP中,以用例捕获需求方法的优势是显而易见的。首先它描述了用户是如何与系统交互的,这种描述更易于被用户所理解,是开发人员和用户之间针对系统需求进行沟通、迅速达成共识的有效手段。其次由于它是以时间顺序描述交互过程,因此系统分析员和用户都可以轻易地识别用例中存在的缺陷。再次是它能使团队成员在设计、实现、测试和最后编写用户手册的过程中都紧紧地以用户需求为中心,促使开发人员始终站在用户的角度考虑问题,容易验证设计和实现满足用户的需求。此外,用例还简化了记录功能需求的工作,有效提高开发工作的效率。

                                                 二、用例驱动需求管理的技巧

  注重需求管理,确保满足客户的需求,既是RUP的基本原理之一,又是RUP静态结构中的一个重要核心工作流程。针对软件需求不显见、多源头、不同性、多变化等特点,RUP提供了基于用例驱动的指导来提高需求管理技能和流程的专业技术,并开发了有效进行自动化管理需求的工具。其中下列的管理技巧在有效的需求管理流程中,是以不同的顺序连续应用的。

  分析业务问题。进行业务分析是为了加深了解与熟悉用户的需要和实际业务运作流程,尽力找出“隐藏在问题背后的问题”,并提出高层解决方案。在问题分析过程中,将对实际问题的说明取得一致,并确定有关的涉众。初始解决方案的界限和约束从技术和业务两个方面来定义。在适当的时候,从项目的商业理由分析中还能得到期望从系统获得的投资回报。

  理解涉众需要。需求将有许多来源,客户、合作伙伴、最终用户以及领域专家都是某些需求的来源,而管理人员、项目团队成员、业务策略和管理机构是另外一些需求源。不同的用户和相关人员会关心不同的问题,其间的要求甚至是互相矛盾的;同时这些需求之间又有着千丝万缕的联系,必须在深刻理解的基础上,既综合整理归纳,又分门别类对待。

  明确定义系统。定义系统是指正确解释涉众需求,并整理成对要构建的系统意义明确的说明。说明可以是书面文档、电子文件、图像或用于表达除系统本身以外的系统需求的任何表示方式。在系统定义初期,需要决定需求的构成、文档格式、语言规范、需求等级、请求优先级、预计工作量、技术和管理风险以及系统规模等。系统定义活动还可包括与最关键的涉众请求直接联系的初期原型和设计模型。

  管理系统规模。项目规模由分配给它的需求集定义。管理项目规模,使之适合可用资源(时间、人力和资金),是成功管理项目的关键。使用需求属性(如优先级、工作量和风险)作为协商项目应包含需求的基础,对管理系统规模而言是相当有用的技巧。侧重于属性,而不是需求自身,将减少通常对敏感问题的争论。同时这也有助于培养团队负责人的谈判技巧,有助于在项目中为组织培养推介人。项目推介人应既有能力代表组织拒绝那些超出可用资源的规模变更,也有相应能力扩展资源以适应扩大的规模。

  改进系统定义。对高层系统定义达成一致并充分理解了系统初始规模后,投入资源改进系统定义不仅有可能,而且也是经济的。改进系统定义包括两个重要的考虑事项:制定更详细的高层系统说明和验证系统是否符合涉众需要并按说明运行。说明通常是项目团队的重要参考材料,在制定说明时一定要考虑受众,需要为不同的受众编制不同类型的说明。确定说明格式后,改进将持续贯穿整个项目生命周期。

  管理变更需求。软件需求的变更总是在不断的产生之中,实际上有些变更是非常值得的,我们应当树立“变更不是敌人,而没有管理的变更才是真正敌人”的观念。对于一个团队来说,能否适应变更需求是评测团队涉众敏感度和运作灵活性的一个尺度,而敏感度和灵活性正是对项目成功有贡献的团队的特征。当然需求变更表明多少需要耗费一些时间来实施某个特定的功能,而且一个需求的变更对其他需求可能带来影响。管理需求变更包括这样一些活动:设立基线,追踪每个需求的历史,确定哪些依赖关系值得追踪,在相关项之间建立可追踪关系以及维护版本控制等。此外,建立变更控制或批准流程也很重要,它要求由指定相关负责的团队成员来复审所有提议的变更,以在全局的高度上对变更需求的好处和可能引起的后果之间有个客观的权衡和把握。

                                                         三、管理需求中须注意的问题

  Rational公司的两位RUP的开发与管理者Per Kroll和Philippe Kruchten在《实践者指南》一书中,曾提示了一些不能正确应用RUP的问题,这在管理需求工作中也有所体现。由于以用例驱动需求管理所获得的明显益处,容易使团队成员产生盲目乐观情绪,从而减弱了把握正确应用的思维判断能力,产生过犹不及或轻视麻痹的行为及不良效果,这是在管理需求实践中必须加以注意和避免的。其主要问题有:

  一是创建过多的用例。一个常见的现象是根据功能把用例划分得太细,没能做到“有所为有所不为”,这样将产生下列的后果:用户对粒度太小的用例很难了解及难以判断是否满足他们的需求;设计人员对于过细的功能无法全部实现,难以通过设计满足实际用户需求;开发人员对关系太紧密的用例很可能开发重复功能并妨碍其他人工作;测试人员要花很多额外精力合成测试用例,才能创建有意义的测试。要避免发生这种情况,应注重以下列的几条标志来准确量度把握:无法衡量能否给用户产生价值的用例,代表的是不完整的交互过程,应当重建;用例A总是与用例B或用例C相关,应当把它们整合为一个用例;两个或多个用例有着几乎相同的描述,就可以把它们合并在一起;对于用例模型中用例之间的关系,不要进行多于一层的抽象。

  二是忽视需求定义的准确与共识。系统的用例模型是由多个系统分析员协同完成的,模型本身也是由多个工件所组成。如果忽略了不同工件之间是否存在矛盾或冲突的地方,就会在模型内部产生不一致性,这种不一致性将会直接影响到需求定义的准确性。同时用例模型最大的优点就在于它应该易于被不同涉众所理解且无二义性,因此用例的粒度、个数以及模型元素之间的关系复杂程度都应该依此原则所决定,从而使准确的需求定义成为团队成员和所有涉众达成共识的基础。

  三是在初始阶段过于细化需求。根据RUP对生命周期的阶段、目的和里程碑的划分,初始阶段的目的是定义系统的边界并理解最重要的用户需求,这个阶段最重要的任务是尽快建立可执行的架构,以化解重大风险,因此不必在初始阶段花费太多时间细化需求。这一阶段只要得到一个合理并完整的参与者和用例的清单,广泛而扼要地描述需求,细化基本的或关键的用例,就可结束任务,尔后尽早转入到细化阶段,从而为后续的细化需求工作留出充裕的时间。

  四是不善于设置需求的优先级。由于资源或技术条件的限制,不可能把所有需求都一次性完成,这样就必须进行精心设置,按优先级排序,分批予以实现。为需求设置优先级时应当思考:某个用例是否必须在另一个用例之前实现?是否必须实现整个用例?哪些用例或用例的哪些部分是最重要的?哪一些提供了最多的价值?应把每个需求按对效益的贡献打分,然后将优先级高的先实现,低的到下一个版本,对不断进来的新需求也应照此办理。还要注意的一点是,最合理的需求不一定是要最先考虑的,“经济为本”应始终是指导优先排序的最高原则。

 
  软件需求是一个软件系统必须完成或达到的目标总和,需求管理是指了解、记录、追踪不断变化的需求的一个系统化方法。至今许多软件开发的实践都表明,有无良好的需求管理是整个软件项目成败的至关重要的一步,是与项目得失关系最大的首要因素。RUP把需求定义为“(正在构建的)系统必须符合的条件或具备的功能”,并描述了如何提取、组织需要的功能和限制;跟踪和文档化适中方案和决策;捕获和进行商业需求交流。在此过程中的用例和场景(用例的实例)的使用,已被证明是捕获功能性需求的卓越方法,并确保由它们来驱动软件的设计、实现和测试,使最终系统更能满足最终用户的需要。下面对此稍作具体的分析。
 
                         一、用例驱动较之功能分解的优势
   
  在不同行业中,都有很多机会足以去发明产品,或改善制造和管理的过程,而把握机会的一个重要方法就是从问题中找机会。Rational正是一家善于思考问题的企业。
 
  长期以来,在软件工程实践中普遍存在这样一种认识——用户知道需求是什么,开发人员要做的就是从他们那里得到需求,并由此做出系统功能的说明。基于这样的认识和思想指导,传统的软件需求规约采用的基本上都是以功能分解的方式来描述系统功能。在这种表述方式中,系统功能被分解到各个系统功能模块中,通过描述细分的系统模块功能来达到描述整个系统功能的目的。但采用这种方法来描述系统需求,非常容易混淆需求和设计的界限,其表述中实际上已经包含了部分的设计在内。由此常常导致这样的迷惑:系统需求应该详细到何种程度?极端的情况就是需求可以详细到概要设计,因为这样的需求表述既包含了外部需求也包含了内部设计。以往在有些公司的开发流程中,对于需求就有"内部需求"(实为内部设计)和"外部需求"之区分与称谓。

  功能分解方法的另一个缺点是它分割了各项系统功能的应用环境,从各个功能项入手,你就很难了解到这些功能项是如何相互关联来实现一个完整的系统服务的。所以在传统的SRS文档中,我们往往需要另外一些章节来描述系统的整体结构及各部分之间的相互关联,这些内容使得SRS需求更像是一个设计文档。

  通过软件开发的反复实践后发现,任何系统都会有很多用户(或不同类型的用户),每个用户只知道其个人如何使用系统,他们并不知道也不想了解系统的内部结构、设计及整体运行情况,他们所关心的只是系统所能提供的服务,也就是被开发出来的系统将是如何被使用的,这就是Rational思考问题的角度,也是创制用例方法的基本指导思想。

  用例方法是完全站在用户的角度上(即从系统的外部)来描述系统的功能,所要回答的问题是“系统应该为每个用户做什么”。它把被定义的系统看作是一个黑箱,先不去关心系统内部是如何完成它所提供的功能,而仅描述被定义系统有哪些外部使用者(抽象成为Actor)会与其发生交互;针对每一参与者,又描述系统为这些参与者提供了什么样的服务(抽象成为Use Case),或者说系统是如何被这些参与者使用的。在所有用例构成的用例模型中,判断系统各项功能是否重要或有价值的标准,是考虑系统为每个用户提供的价值,包括该功能辅助哪个用户进行工作?需要提供什么业务?能够为业务增加多少价值?因此,用例能够用于指导找出每个用户所需要的功能,避免提出用户根本就不需要的冗余功能,从而有效界定系统的范围和行为,使整个系统的业务为每个用户提供最大的价值。

  与传统的功能分解方式相比,用例方法全都是从外部来定义系统功能,它把需求与设计完全分离开来。在面向对象的分析设计方法中,用例模型主要用于表述系统的功能性需求,系统的设计主要由对象模型来记录表述。此外,用例定义了系统功能的使用环境与上下文,每一个用例描述的是一个完整的系统服务。

  在RUP中,以用例捕获需求方法的优势是显而易见的。首先它描述了用户是如何与系统交互的,这种描述更易于被用户所理解,是开发人员和用户之间针对系统需求进行沟通、迅速达成共识的有效手段。其次由于它是以时间顺序描述交互过程,因此系统分析员和用户都可以轻易地识别用例中存在的缺陷。再次是它能使团队成员在设计、实现、测试和最后编写用户手册的过程中都紧紧地以用户需求为中心,促使开发人员始终站在用户的角度考虑问题,容易验证设计和实现满足用户的需求。此外,用例还简化了记录功能需求的工作,有效提高开发工作的效率。

                                                 二、用例驱动需求管理的技巧

  注重需求管理,确保满足客户的需求,既是RUP的基本原理之一,又是RUP静态结构中的一个重要核心工作流程。针对软件需求不显见、多源头、不同性、多变化等特点,RUP提供了基于用例驱动的指导来提高需求管理技能和流程的专业技术,并开发了有效进行自动化管理需求的工具。其中下列的管理技巧在有效的需求管理流程中,是以不同的顺序连续应用的。

  分析业务问题。进行业务分析是为了加深了解与熟悉用户的需要和实际业务运作流程,尽力找出“隐藏在问题背后的问题”,并提出高层解决方案。在问题分析过程中,将对实际问题的说明取得一致,并确定有关的涉众。初始解决方案的界限和约束从技术和业务两个方面来定义。在适当的时候,从项目的商业理由分析中还能得到期望从系统获得的投资回报。

  理解涉众需要。需求将有许多来源,客户、合作伙伴、最终用户以及领域专家都是某些需求的来源,而管理人员、项目团队成员、业务策略和管理机构是另外一些需求源。不同的用户和相关人员会关心不同的问题,其间的要求甚至是互相矛盾的;同时这些需求之间又有着千丝万缕的联系,必须在深刻理解的基础上,既综合整理归纳,又分门别类对待。

  明确定义系统。定义系统是指正确解释涉众需求,并整理成对要构建的系统意义明确的说明。说明可以是书面文档、电子文件、图像或用于表达除系统本身以外的系统需求的任何表示方式。在系统定义初期,需要决定需求的构成、文档格式、语言规范、需求等级、请求优先级、预计工作量、技术和管理风险以及系统规模等。系统定义活动还可包括与最关键的涉众请求直接联系的初期原型和设计模型。

  管理系统规模。项目规模由分配给它的需求集定义。管理项目规模,使之适合可用资源(时间、人力和资金),是成功管理项目的关键。使用需求属性(如优先级、工作量和风险)作为协商项目应包含需求的基础,对管理系统规模而言是相当有用的技巧。侧重于属性,而不是需求自身,将减少通常对敏感问题的争论。同时这也有助于培养团队负责人的谈判技巧,有助于在项目中为组织培养推介人。项目推介人应既有能力代表组织拒绝那些超出可用资源的规模变更,也有相应能力扩展资源以适应扩大的规模。

  改进系统定义。对高层系统定义达成一致并充分理解了系统初始规模后,投入资源改进系统定义不仅有可能,而且也是经济的。改进系统定义包括两个重要的考虑事项:制定更详细的高层系统说明和验证系统是否符合涉众需要并按说明运行。说明通常是项目团队的重要参考材料,在制定说明时一定要考虑受众,需要为不同的受众编制不同类型的说明。确定说明格式后,改进将持续贯穿整个项目生命周期。

  管理变更需求。软件需求的变更总是在不断的产生之中,实际上有些变更是非常值得的,我们应当树立“变更不是敌人,而没有管理的变更才是真正敌人”的观念。对于一个团队来说,能否适应变更需求是评测团队涉众敏感度和运作灵活性的一个尺度,而敏感度和灵活性正是对项目成功有贡献的团队的特征。当然需求变更表明多少需要耗费一些时间来实施某个特定的功能,而且一个需求的变更对其他需求可能带来影响。管理需求变更包括这样一些活动:设立基线,追踪每个需求的历史,确定哪些依赖关系值得追踪,在相关项之间建立可追踪关系以及维护版本控制等。此外,建立变更控制或批准流程也很重要,它要求由指定相关负责的团队成员来复审所有提议的变更,以在全局的高度上对变更需求的好处和可能引起的后果之间有个客观的权衡和把握。

                                                         三、管理需求中须注意的问题

  Rational公司的两位RUP的开发与管理者Per Kroll和Philippe Kruchten在《实践者指南》一书中,曾提示了一些不能正确应用RUP的问题,这在管理需求工作中也有所体现。由于以用例驱动需求管理所获得的明显益处,容易使团队成员产生盲目乐观情绪,从而减弱了把握正确应用的思维判断能力,产生过犹不及或轻视麻痹的行为及不良效果,这是在管理需求实践中必须加以注意和避免的。其主要问题有:

  一是创建过多的用例。一个常见的现象是根据功能把用例划分得太细,没能做到“有所为有所不为”,这样将产生下列的后果:用户对粒度太小的用例很难了解及难以判断是否满足他们的需求;设计人员对于过细的功能无法全部实现,难以通过设计满足实际用户需求;开发人员对关系太紧密的用例很可能开发重复功能并妨碍其他人工作;测试人员要花很多额外精力合成测试用例,才能创建有意义的测试。要避免发生这种情况,应注重以下列的几条标志来准确量度把握:无法衡量能否给用户产生价值的用例,代表的是不完整的交互过程,应当重建;用例A总是与用例B或用例C相关,应当把它们整合为一个用例;两个或多个用例有着几乎相同的描述,就可以把它们合并在一起;对于用例模型中用例之间的关系,不要进行多于一层的抽象。

  二是忽视需求定义的准确与共识。系统的用例模型是由多个系统分析员协同完成的,模型本身也是由多个工件所组成。如果忽略了不同工件之间是否存在矛盾或冲突的地方,就会在模型内部产生不一致性,这种不一致性将会直接影响到需求定义的准确性。同时用例模型最大的优点就在于它应该易于被不同涉众所理解且无二义性,因此用例的粒度、个数以及模型元素之间的关系复杂程度都应该依此原则所决定,从而使准确的需求定义成为团队成员和所有涉众达成共识的基础。

  三是在初始阶段过于细化需求。根据RUP对生命周期的阶段、目的和里程碑的划分,初始阶段的目的是定义系统的边界并理解最重要的用户需求,这个阶段最重要的任务是尽快建立可执行的架构,以化解重大风险,因此不必在初始阶段花费太多时间细化需求。这一阶段只要得到一个合理并完整的参与者和用例的清单,广泛而扼要地描述需求,细化基本的或关键的用例,就可结束任务,尔后尽早转入到细化阶段,从而为后续的细化需求工作留出充裕的时间。

  四是不善于设置需求的优先级。由于资源或技术条件的限制,不可能把所有需求都一次性完成,这样就必须进行精心设置,按优先级排序,分批予以实现。为需求设置优先级时应当思考:某个用例是否必须在另一个用例之前实现?是否必须实现整个用例?哪些用例或用例的哪些部分是最重要的?哪一些提供了最多的价值?应把每个需求按对效益的贡献打分,然后将优先级高的先实现,低的到下一个版本,对不断进来的新需求也应照此办理。还要注意的一点是,最合理的需求不一定是要最先考虑的,“经济为本”应始终是指导优先排序的最高原则。
2006年04月10日

abstract class和interface是Java语言中对于抽象类定义进行支持的两种机制,正是由于这两种机制的存在,才赋予了Java强大的面向对象能力。abstract class和interface之间在对于抽象类定义的支持方面具有很大的相似性,甚至可以相互替换,因此很多开发者在进行抽象类定义时对于abstract class和interface的选择显得比较随意。其实,两者之间还是有很大的区别的,对于它们的选择甚至反映出对于问题领域本质的理解、对于设计意图的理解是否正确、合理。本文将对它们之间的区别进行一番剖析,试图给开发者提供一个在二者之间进行选择的依据。

理解抽象类

abstract class和interface在Java语言中都是用来进行抽象类(本文中的抽象类并非从abstract class翻译而来,它表示的是一个抽象体,而abstract class为Java语言中用于定义抽象类的一种方法,请读者注意区分)定义的,那么什么是抽象类,使用抽象类能为我们带来什么好处呢?

在面向对象的概念中,我们知道所有的对象都是通过类来描绘的,但是反过来却不是这样。并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类。抽象类往往用来表征我们在对问题领域进行分析、设计中得出的抽象概念,是对一系列看上去不同,但是本质上相同的具体概念的抽象。比如:如果我们进行一个图形编辑软件的开发,就会发现问题领域存在着圆、三角形这样一些具体概念,它们是不同的,但是它们又都属于形状这样一个概念,形状这个概念在问题领域是不存在的,它就是一个抽象概念。正是因为抽象的概念在问题领域没有对应的具体概念,所以用以表征抽象概念的抽象类是不能够实例化的。

在面向对象领域,抽象类主要用来进行类型隐藏。我们可以构造出一个固定的一组行为的抽象描述,但是这组行为却能够有任意个可能的具体实现方式。这个抽象描述就是抽象类,而这一组任意个可能的具体实现则表现为所有可能的派生类。模块可以操作一个抽象体。由于模块依赖于一个固定的抽象体,因此它可以是不允许修改的;同时,通过从这个抽象体派生,也可扩展此模块的行为功能。熟悉OCP的读者一定知道,为了能够实现面向对象设计的一个最核心的原则OCP(Open-Closed Principle),抽象类是其中的关键所在。

从语法定义层面看abstract class和interface

在语法层面,Java语言对于abstract class和interface给出了不同的定义方式,下面以定义一个名为Demo的抽象类为例来说明这种不同。
使用abstract class的方式定义Demo抽象类的方式如下:

abstract class Demo {
abstract void method1();
abstract void method2();
…
}

使用interface的方式定义Demo抽象类的方式如下:

interface Demo {
void method1();
void method2();
…
}

在abstract class方式中,Demo可以有自己的数据成员,也可以有非abstarct的成员方法,而在interface方式的实现中,Demo只能够有静态的不能被修改的数据成员(也就是必须是static final的,不过在interface中一般不定义数据成员),所有的成员方法都是abstract的。从某种意义上说,interface是一种特殊形式的abstract class。

从编程的角度来看,abstract class和interface都可以用来实现"design by contract"的思想。但是在具体的使用上面还是有一些区别的。

首先,abstract class在Java语言中表示的是一种继承关系,一个类只能使用一次继承关系。但是,一个类却可以实现多个interface。也许,这是Java语言的设计者在考虑Java对于多重继承的支持方面的一种折中考虑吧。

其次,在abstract class的定义中,我们可以赋予方法的默认行为。但是在interface的定义中,方法却不能拥有默认行为,为了绕过这个限制,必须使用委托,但是这会 增加一些复杂性,有时会造成很大的麻烦。

在抽象类中不能定义默认行为还存在另一个比较严重的问题,那就是可能会造成维护上的麻烦。因为如果后来想修改类的界面(一般通过abstract class或者interface来表示)以适应新的情况(比如,添加新的方法或者给已用的方法中添加新的参数)时,就会非常的麻烦,可能要花费很多的时间(对于派生类很多的情况,尤为如此)。但是如果界面是通过abstract class来实现的,那么可能就只需要修改定义在abstract class中的默认行为就可以了。

同样,如果不能在抽象类中定义默认行为,就会导致同样的方法实现出现在该抽象类的每一个派生类中,违反了"one rule,one place"原则,造成代码重复,同样不利于以后的维护。因此,在abstract class和interface间进行选择时要非常的小心。

从设计理念层面看abstract class和interface

上面主要从语法定义和编程的角度论述了abstract class和interface的区别,这些层面的区别是比较低层次的、非本质的。本小节将从另一个层面:abstract class和interface所反映出的设计理念,来分析一下二者的区别。作者认为,从这个层面进行分析才能理解二者概念的本质所在。

前面已经提到过,abstarct class在Java语言中体现了一种继承关系,要想使得继承关系合理,父类和派生类之间必须存在"is a"关系,即父类和派生类在概念本质上应该是相同的(参考文献〔3〕中有关于"is a"关系的大篇幅深入的论述,有兴趣的读者可以参考)。对于interface 来说则不然,并不要求interface的实现者和interface定义在概念本质上是一致的,仅仅是实现了interface定义的契约而已。为了使论述便于理解,下面将通过一个简单的实例进行说明。

考虑这样一个例子,假设在我们的问题领域中有一个关于Door的抽象概念,该Door具有执行两个动作open和close,此时我们可以通过abstract class或者interface来定义一个表示该抽象概念的类型,定义方式分别如下所示:

使用abstract class方式定义Door:

abstract class Door {
abstract void open();
abstract void close();
}

使用interface方式定义Door:

interface Door {
void open();
void close();
}

其他具体的Door类型可以extends使用abstract class方式定义的Door或者implements使用interface方式定义的Door。看起来好像使用abstract class和interface没有大的区别。

如果现在要求Door还要具有报警的功能。我们该如何设计针对该例子的类结构呢(在本例中,主要是为了展示abstract class和interface反映在设计理念上的区别,其他方面无关的问题都做了简化或者忽略)?下面将罗列出可能的解决方案,并从设计理念层面对这些不同的方案进行分析。

解决方案一:

简单的在Door的定义中增加一个alarm方法,如下:

abstract class Door {
abstract void open();
abstract void close();
abstract void alarm();
}

或者

interface Door {
void open();
void close();
void alarm();
}

那么具有报警功能的AlarmDoor的定义方式如下:

class AlarmDoor extends Door {
void open() { … }
void close() { … }
void alarm() { … }
}

或者

class AlarmDoor implements Door {
void open() { … }
void close() { … }
void alarm() { … }
}

这种方法违反了面向对象设计中的一个核心原则ISP(Interface Segregation Priciple),在Door的定义中把Door概念本身固有的行为方法和另外一个概念"报警器"的行为方法混在了一起。这样引起的一个问题是那些仅仅依赖于Door这个概念的模块会因为"报警器"这个概念的改变(比如:修改alarm方法的参数)而改变,反之依然。

解决方案二:

既然open、close和alarm属于两个不同的概念,根据ISP原则应该把它们分别定义在代表这两个概念的抽象类中。定义方式有:这两个概念都使用abstract class方式定义;两个概念都使用interface方式定义;一个概念使用abstract class方式定义,另一个概念使用interface方式定义。

显然,由于Java语言不支持多重继承,所以两个概念都使用abstract class方式定义是不可行的。后面两种方式都是可行的,但是对于它们的选择却反映出对于问题领域中的概念本质的理解、对于设计意图的反映是否正确、合理。我们一一来分析、说明。

如果两个概念都使用interface方式来定义,那么就反映出两个问题:1、我们可能没有理解清楚问题领域,AlarmDoor在概念本质上到底是Door还是报警器?2、如果我们对于问题领域的理解没有问题,比如:我们通过对于问题领域的分析发现AlarmDoor在概念本质上和Door是一致的,那么我们在实现时就没有能够正确的揭示我们的设计意图,因为在这两个概念的定义上(均使用interface方式定义)反映不出上述含义。

如果我们对于问题领域的理解是:AlarmDoor在概念本质上是Door,同时它有具有报警的功能。我们该如何来设计、实现来明确的反映出我们的意思呢?前面已经说过,abstract class在Java语言中表示一种继承关系,而继承关系在本质上是"is a"关系。所以对于Door这个概念,我们应该使用abstarct class方式来定义。另外,AlarmDoor又具有报警功能,说明它又能够完成报警概念中定义的行为,所以报警概念可以通过interface方式定义。如下所示:

abstract class Door {
abstract void open();
abstract void close();
}
interface Alarm {
void alarm();
}
class AlarmDoor extends Door implements Alarm {
void open() { … }
void close() { … }
void alarm() { … }
}

这种实现方式基本上能够明确的反映出我们对于问题领域的理解,正确的揭示我们的设计意图。其实abstract class表示的是"is a"关系,interface表示的是"like a"关系,大家在选择时可以作为一个依据,当然这是建立在对问题领域的理解上的,比如:如果我们认为AlarmDoor在概念本质上是报警器,同时又具有Door的功能,那么上述的定义方式就要反过来了。

结论

abstract class和interface是Java语言中的两种定义抽象类的方式,它们之间有很大的相似性。但是对于它们的选择却又往往反映出对于问题领域中的概念本质的理解、对于设计意图的反映是否正确、合理,因为它们表现了概念间的不同的关系(虽然都能够实现需求的功能)。这其实也是语言的一种的惯用法,希望读者朋友能够细细体会。

(http://bbs.java.ccidnet.com/htm_data/2/0603/1080.html)

2006年04月03日

作为J2EE开发人员,我们似乎经常关注“后端机制(backend mechanics)”。我们通常会忘记,J2EE的主要成功之处在Web应用程序方面;许多原因使得人们喜欢利用Web开发应用程序,但主要还是因为其易于部署的特点允许站点以尽可能低的成本拥有上百万的用户。遗憾的是,在过去几年中,我们在后端投入了太多的时间,而在使我们的Web用户界面对用户自然和响应灵敏方面却投入不足。

  本文介绍一种方法,Ajax,使用它可以构建更为动态和响应更灵敏的Web应用程序。该方法的关键在于对浏览器端的JavaScript、DHTML和与服务器异步通信的组合。本文也演示了启用这种方法是多么简单:利用一个Ajax框架(指DWR)构造一个应用程序,它直接从浏览器与后端服务进行通信。如果使用得当,这种强大的力量可以使应用程序更加自然和响应灵敏,从而提升用户的浏览体验。

  该应用程序中所使用的示例代码已打包为单独的WAR文件,可供下载。

简介

  术语Ajax用来描述一组技术,它使浏览器可以为用户提供更为自然的浏览体验。在Ajax之前,Web站点强制用户进入提交/等待/重新显示范例,用户的动作总是与服务器的“思考时间”同步。Ajax提供与服务器异步通信的能力,从而使用户从请求/响应的循环中解脱出来。借助于Ajax,可以在用户单击按钮时,使用JavaScript和DHTML立即更新UI,并向服务器发出异步请求,以执行更新或查询数据库。当请求返回时,就可以使用JavaScript和CSS来相应地更新UI,而不是刷新整个页面。最重要的是,用户甚至不知道浏览器正在与服务器通信:Web站点看起来是即时响应的。

  虽然Ajax所需的基础架构已经出现了一段时间,但直到最近异步请求的真正威力才得到利用。能够拥有一个响应极其灵敏的Web站点确实激动人心,因为它最终允许开发人员和设计人员使用标准的HTML/CSS/JavaScript堆栈创建“桌面风格的(desktop-like)”可用性。

  通常,在J2EE中,开发人员过于关注服务和持久性层的开发,以至于用户界面的可用性已经落后。在一个典型的J2EE开发周期中,常常会听到这样的话,“我们没有可投入UI的时间”或“不能用HTML实现”。但是,以下Web站点证明,这些理由再也站不住脚了:

  所有这些Web站点都告诉我们,Web应用程序不必完全依赖于从服务器重新载入页面来向用户呈现更改。一切似乎就在瞬间发生。简而言之,在涉及到用户界面的响应灵敏度时,基准设得更高了。

定义Ajax

  Adaptive Path公司的Jesse James Garrett这样定义Ajax

  Ajax不是一种技术。实际上,它由几种蓬勃发展的技术以新的强大方式组合而成。Ajax包含:

  • 基于XHTMLCSS标准的表示;
  • 使用Document Object Model进行动态显示和交互;
  • 使用XMLHttpRequest与服务器进行异步通信;
  • 使用JavaScript绑定一切。

  这非常好,但为什么要以Ajax命名呢?其实术语Ajax是由Jesse James Garrett创造的,他说它是“Asynchronous JavaScript + XML的简写”。

Ajax的工作原理

  Ajax的核心是JavaScript对象XmlHttpRequest。该对象在Internet Explorer 5中首次引入,它是一种支持异步请求的技术。简而言之,XmlHttpRequest使您可以使用JavaScript向服务器提出请求并处理响应,而不阻塞用户。

  在创建Web站点时,在客户端执行屏幕更新为用户提供了很大的灵活性。下面是使用Ajax可以完成的功能:

  • 动态更新购物车的物品总数,无需用户单击Update并等待服务器重新发送整个页面。
  • 提升站点的性能,这是通过减少从服务器下载的数据量而实现的。例如,在Amazon的购物车页面,当更新篮子中的一项物品的数量时,会重新载入整个页面,这必须下载32K的数据。如果使用Ajax计算新的总量,服务器只会返回新的总量值,因此所需的带宽仅为原来的百分之一。
  • 消除了每次用户输入时的页面刷新。例如,在Ajax中,如果用户在分页列表上单击Next,则服务器数据只刷新列表而不是整个页面。
  • 直接编辑表格数据,而不是要求用户导航到新的页面来编辑数据。对于Ajax,当用户单击Edit时,可以将静态表格刷新为内容可编辑的表格。用户单击Done之后,就可以发出一个Ajax请求来更新服务器,并刷新表格,使其包含静态、只读的数据。

  一切皆有可能!但愿它能够激发您开始开发自己的基于Ajax的站点。然而,在开始之前,让我们介绍一个现有的Web站点,它遵循传统的提交/等待/重新显示的范例,我们还将讨论Ajax如何提升用户体验。

Ajax可用于那些场景?——一个例子:MSN Money页面

  前几天,在浏览MSN Money页面的时候,有一篇关于房地产投资的文章引起了我的好奇心。我决定使用站点的“Rate this article”(评价本文)功能,鼓励其他的用户花一点时间来阅读这篇文章。在我单击vote按钮并等待了一会儿之后,整个页面被刷新,在原来投票问题所在的地方出现了一个漂亮的感谢画面。

  而Ajax能够使用户的体验更加愉快,它可以提供响应更加灵敏的UI,并消除页面刷新所带来的闪烁。目前,由于要刷新整个页面,需要传送大量的数据,因为必须重新发送整个页面。如果使用Ajax,服务器可以返回一个包含了感谢信息的500字节的消息,而不是发送26,813字节的消息来刷新整个页面。即使使用的是高速Internet,传送26K和1/2K的差别也非常大。同样重要的是,只需要刷新与投票相关的一小节,而不是刷新整个屏幕。

  让我们利用Ajax实现自己的基本投票系统。

原始的Ajax:直接使用XmlHttpRequest

  如上所述,Ajax的核心是JavaScript对象XmlHttpRequest。下面的示例文章评价系统将带您熟悉Ajax的底层基本知识:http://tearesolutions.com/ajax-demo/raw-ajax.html。注:如果您已经在本地WebLogic容器中安装了ajax-demo.war,可以导航到http://localhost:7001/ajax-demo/raw-ajax.html

  浏览应用程序,参与投票,并亲眼看它如何运转。熟悉了该应用程序之后,继续阅读,进一步了解其工作原理细节。

  首先,您拥有一些简单的定位点标记,它连接到一个JavaScriptcastVote(rank)函数。

function castVote(rank) {
  var url = "/ajax-demo/static-article-ranking.html";
  var callback = processAjaxResponse;
  executeXhr(callback, url);
}

  该函数为您想要与之通信的服务器资源创建一个URL并调用内部函数executeXhr,提供一个回调JavaScript函数,一旦服务器响应可用,该函数就被执行。由于我希望它运行在一个简单的Apache环境中,“cast vote URL”只是一个简单的HTML页面。在实际情况中,被调用的URL将记录票数并动态地呈现包含投票总数的响应。

  下一步是发出一个XmlHttpRequest请求:

function executeXhr(callback, url) {
  // branch for native XMLHttpRequest object
  if (window.XMLHttpRequest) {
    req = new XMLHttpRequest();
    req.onreadystatechange = callback;
    req.open("GET", url, true);
    req.send(null);
  } // branch for IE/Windows ActiveX version
  else if (window.ActiveXObject) {
    req = new ActiveXObject("Microsoft.XMLHTTP");
    if (req) {
      req.onreadystatechange = callback;
      req.open("GET", url, true);
      req.send();
    }
  }
}

  如您所见,执行一个XmlHttpRequest并不简单,但非常直观。和平常一样,在JavaScript领域,大部分的工作量都花在确保浏览器兼容方面。在这种情况下,首先要确定XmlHttpRequest是否可用。如果不能用,很可能要使用Internet Explorer,这样就要使用所提供的ActiveX实现。

executeXhr()方法中最关键的部分是这两行:

req.onreadystatechange = callback;
req.open("GET", url, true);

  第一行定义了JavaScript回调函数,您希望一旦响应就绪它就自动执行,而req.open()方法中所指定的“true”标志说明您想要异步执行该请求。

  一旦服务器处理完XmlHttpRequest并返回给浏览器,使用req.onreadystatechange指派所设置的回调方法将被自动调用。

function processAjaxResponse() {
  // only if req shows "loaded"
  if (req.readyState == 4) {
    // only if "OK"
    if (req.status == 200) {
      502 502'votes').innerHTML = req.responseText;
    } else {
      alert("There was a problem retrieving the XML data:
" +
      req.statusText);
    }
  }
}

  该代码相当简洁,并且使用了几个幻数,这使得难以一下子看出发生了什么。为了弄清楚这一点,下面的表格(引用自http://developer.apple.com/internet/webcontent/xmlhttpreq.html)列举了常用的XmlHttpRequest对象属性。

属性

描述

onreadystatechange

每次状态改变所触发事件的事件处理程序

readyState

对象状态值:

  • 0 = 未初始化(uninitialized)
  • 1 = 正在加载(loading)
  • 2 = 加载完毕(loaded)
  • 3 = 交互(interactive)
  • 4 = 完成(complete)

responseText

从服务器进程返回的数据的字符串形式

responseXML

从服务器进程返回的DOM兼容的文档数据对象

status

从服务器返回的数字代码,比如404(未找到)或200(就绪)

statusText

伴随状态码的字符串信息

  现在processVoteResponse()函数开始显示出其意义了。它首先检查XmlHttpRequest的整体状态以保证它已经完成(readyStatus == 4),然后根据服务器的设定询问请求状态。如果一切正常(status == 200),就使用innerHTML属性重写DOM的“votes”节点的内容。

  既然您亲眼看到了XmlHttpRequest对象是如何工作的,就让我们利用一个旨在简化JavaScript与Java应用程序之间的异步通信的框架来对具体的细节进行抽象。

Ajax: DWR方式

  按照与文章评价系统相同的流程,我们将使用Direct Web Remoting(DWR)框架实现同样的功能。

  假定文章和投票结果存储在一个数据库中,使用某种对象/关系映射技术来完成抽取工作。为了部署起来尽可能地简单,我们不会使用数据库进行持久性存储。此外,为使应用程序尽可能通用,也不使用Web框架。相反,应用程序将从一个静态HTML文件开始,可以认为它由服务器动态地呈现。除了这些简化措施,应用程序还应该使用Spring Framework关联一切,以便轻松看出如何在一个“真实的”应用程序中使用DWR。

  现在应该下载示例应用程序并熟悉它。该应用程序被压缩为标准的WAR文件,因此您可以把它放置到任何一个Web容器中——无需进行配置。部署完毕之后,就可以导航到http://localhost:7001/ajax_demo/dwr-ajax.html来运行程序。

  可以查看HTML 源代码,了解它如何工作。给人印象最深的是,代码如此简单——所有与服务器的交互都隐藏在JavaScript对象ajaxSampleSvc的后面。更加令人惊讶的是,ajaxSampleSvc服务不是由手工编写而是完全自动生成的!让我们继续,看看这是如何做到的。

引入DWR

  如同在“原始的Ajax”一节所演示的那样,直接使用XmlHttpRequest创建异步请求非常麻烦。不仅JavaScript代码冗长,而且必须考虑服务器端为定位Ajax请求到适当的服务所需做的工作,并将结果封送到浏览器。

  设计DWR的目的是要处理将Web页面安装到后端服务上所需的所有信息管道。它是一个Java框架,可以很轻松地将它插入到Web应用程序中,以便JavaScript代码可以调用服务器上的服务。它甚至直接与Spring Framework集成,从而允许用户直接向Web客户机公开bean。

  DWR真正的巧妙之处是,在用户配置了要向客户机公开的服务之后,它使用反射来生成JavaScript对象,以便Web页面能够使用这些对象来访问该服务。然后Web页面只需接合到生成的JavaScript对象,就像它们是直接使用服务一样;DWR无缝地处理所有有关Ajax和请求定位的琐碎细节。

  让我们仔细分析一下示例代码,弄清它是如何工作的。

应用程序细节:DWR分析

  关于应用程序,首先要注意的是,它是一个标准的Java应用程序,使用分层架构(Layered Architecture)设计模式。使用DWR通过JavaScript公开一些服务并不影响您的设计。

  下面是一个简单的Java服务,我们将使用DWR框架直接将其向JavaScript代码公开:

package com.tearesolutions.service;

public interface AjaxSampleSvc {
  Article castVote(int rank);
}

  这是一个被简化到几乎不可能的程度的例子,其中只有一篇文章可以投票。该服务由Spring管理,它使用的bean名是ajaxSampleSvc,它的持久性需求则依赖于ArticleDao。详情请参见applicationContext.xml。

  为了把该服务公开为JavaScript对象,需要配置DWR,添加dwr.xml文件到WEB-INF目录下:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE dwr PUBLIC
 "-//GetAhead Limited//DTD Direct Web Remoting 0.4//EN"
 "http://www.getahead.ltd.uk/dwr/dwr.dtd">

<dwr>
 <allow>
  <create creator="spring" javascript="ajaxSampleSvc">
   <param name="beanName" value="ajaxSampleSvc" />
  </create>
  <convert converter="bean" match="com.tearesolutions.model.Article"/>
  <exclude method="toString"/>
  <exclude method="setArticleDao"/>
 </allow>
</dwr>

  dwr.xml文件告诉DWR哪些服务是要直接向JavaScript代码公开的。注意,已经要求公开Spring bean ajaxSampleSvc。DWR将自动找到由应用程序设置的SpringApplicationContext。为此,必须使用标准的servlet过滤器ContextLoaderListener来初始化Spring ApplicationContext。

  DWR被设置为一个servlet,所以把它的定义添加到web.xml:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD
 Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd">

<web-app>
 <display-name>Ajax Examples</display-name>

 <listener>
  <listener-class>
      org.springframework.web.context.ContextLoaderListener
  </listener-class>
 </listener>

 <servlet>
  <servlet-name>ajax_sample</servlet-name>
  <servlet-class>com.tearesolutions.web.AjaxSampleServlet</servlet-class>
  <load-on-startup>1</load-on-startup>
 </servlet>

 <servlet>
  <servlet-name>dwr-invoker</servlet-name>
  <display-name>DWR Servlet</display-name>
  <description>Direct Web Remoter Servlet</description>
  <servlet-class>uk.ltd.getahead.dwr.DWRServlet</servlet-class>
  <init-param>
   <param-name>debug</param-name>
   <param-value>true</param-value>
  </init-param>
 </servlet>

 <servlet-mapping>
  <servlet-name>ajax_sample</servlet-name>
  <url-pattern>/ajax_sample</url-pattern>
 </servlet-mapping>

 <servlet-mapping>
  <servlet-name>dwr-invoker</servlet-name>
  <url-pattern>/dwr/*</url-pattern>
 </servlet-mapping>
</web-app>

  做完这些之后,可以加载http://localhost:7001/ajax-demo/dwr,看看哪些服务可用。结果如下:

图3. 可用的服务

  单击ajaxSampleSvc链接,查看有关如何在HTML页面内直接使用服务的示例实现。其中包含的两个JavaScript文件完成了大部分的功能:

<script type='text/javascript'
   src='/ajax-demo/dwr/interface/ajaxSampleSvc.js'></script>
<script type='text/javascript'
   src='/ajax-demo/dwr/engine.js'></script>

ajaxSampleSvc.js是动态生成的:

function ajaxSampleSvc() { }

ajaxSampleSvc.castVote = function(callback, p0)
{
  DWREngine._execute(callback, '/ajax-demo/dwr',
 'ajaxSampleSvc', 'castVote', p0);
}

  现在可以使用JavaScript对象ajaxSampleSvc替换所有的XmlHttpRequest代码,从而重构raw-ajax.html文件。可以在dwr-ajax.html文件中看到改动的结果;下面是新的JavaScript函数:

function castVote(rank) {
  ajaxSampleSvc.castVote(processResponse, rank);
}
function processResponse(data) {
 var voteText = "

Thanks for Voting!

"
    + "

Current ranking: " + data.voteAverage
    + " out of 5

"
    + "

Number of votes placed: "
    + data.numberOfVotes + "

";
 502 502'votes').innerHTML = voteText;
}

  惊人地简单,不是吗?由ajaxSampleSvc对象返回的Article域对象序列化为一个JavaScript对象,允许在它上面调用诸如numberOfVotes()和voteAverage()之类的方法。在动态生成并插入到DIV元素“votes”中的HTML代码内使用这些数据。

下一步工作

   在后续文章中,我将继续有关Ajax的话题,涉及下面这些方面:

  • Ajax最佳实践

  像许多技术一样,Ajax是一把双刃剑。对于一些用例,其应用程序其实没有必要使用Ajax,使用了反而有损可用性。我将介绍一些不适合使用的模式,突出说明Ajax的一些消极方面,并展示一些有助于缓和这些消极方面的机制。例如,对Netflix电影浏览器来说,Ajax是合适的解决方案吗?或者,如何提示用户确实出了一些问题,而再次单击按钮也无济于事?

  • 管理跨请求的状态

  在使用Ajax时,最初的文档DOM会发生一些变化,并且有大量的页面状态信息存储在客户端变量中。当用户跟踪一个链接到应用程序中的另一个页面时,状态就丢失了。当用户按照惯例单击Back按钮时,呈现给他们的是缓存中的初始页面。这会使用户感到非常迷惑!

  • 调试技巧

  使用JavaScript在客户端执行更多的工作时,如果事情不按预期方式进行,就需要一些调试工具来帮助弄清出现了什么问题。

结束语

  本文介绍了Ajax方法,并展示了如何使用它来创建一个动态且响应灵敏的Web应用程序。通过使用DWR框架,可以轻松地把Ajax融合到站点中,而无需担心所有必须执行的实际管道工作。

  特别感谢Getahead IT咨询公司的Joe Walker和他的团队开发出DWR这样神奇的工具。感谢你们与世界共享它!

下载

  本文中演示的应用程序源代码可供下载:ajax-demo.war(1.52 MB)。

参考资料

原文出处

An Introduction To Ajax

http://dev2dev.bea.com/pub/a/2005/08/ajax_introduction.html

2006年03月31日

oracle9中怎么会有个默认用户名为open的用户呢????

密码system,很奇怪的问题,

研究研究吧~~

2006年03月30日

启动数据库时可以启动到不同的状态:

START NOMOUNT:根据初始化参数建立SGA。

START MOUNT:根据初始化参数建立SGA。加载数据库,读取控制文件信息。

START:等价于STARTUP OPEN根据初始化参数建立SGA。加载数据库,读取控制文件信息。打开所有数据文件,数据库可以进行访问。

START RESTRICT:打开数据库,但是只有具有RESTRICTED SESSION系统权限的用户可以登陆。

STARTUP FORCE:等价于SHUTDOWN ABORT + STARTUP。

STARTUP OPEN READ ONLY:以只读方式打开数据库。

关闭数据库的不同方法:

SHUTDOWN:等价于SHUTDOWN NORMAL不允许新的会话登陆,等所有的连接都退出后关闭数据库。

SHUTDOWN TRANSACTIONAL:不允许新的会话登陆,当所有当前事务结束时,断开所有连接用户。

SHUTDOWN IMMEDIATE:不允许新的会话登陆,所有没有提交的事务全部回滚,断开所有连接用户。

SHUTDOWN ABORT:不允许新的会话登陆,当前运行的SQL语句立即中止,没有提交的事务不进行回滚,断开所有连接用户。下次重起时,Oracle自动进行实例恢复(instance recovery)。

数据库的不同状态:

QUIESCED状态:当sys或system用户发出ALTER SYSTEM QUIESCE RESTRICTED语句,所有的非DBA用户在当前会话状态变为inactive时,进行到停顿状态。当执行ALTER SYSTEM UNQUIESCE,其他用户的会话恢复。

注意:只有SYS和SYSTEM用户有ALTER SYSTEM QUIESCE RESTRICTED的权限。其实是由于DBA角色的其他用户也没有这个权限。

执行这个操作的前提是:自从本次数据库启动以来,必须一直设置着资源限制。

SUSPEND状态:发出ALTER SYSTEM SUSPEND语句后,任何I/O操作都被停止,直到发出ALTER SYSTEM RESUME语句。

对数据库状态的查询:

 

 

SQL> select database_status from v$instance;

 

DATABASE_STATUS
—————–
SUSPENDED

**************————————–

一个表中有三个字段:ID,NAME,FLAG.
如:
ID NAME FLAG
10 aa 1
10 bb 1
10 cc 1
11 aa 0
12 aa 0
当ID与NAME是1对多的时候,FLAG的值设为1,
否则(即1对1的时候),FLAG的值设为0,
1>>

update aatest set flag=’1’ where

id in(
select id from aatest
group by id
HAVING count(id)>1
)

update aatest set flag=’0’

where id not in(
select id from aatest
group by id
HAVING count(id)>1
)

2>>update aatest set flag=( select decode ( count(*), 1, 0, 1)
from aatest a
where a.id=aatest.id group by id )

 

SQL> select database_status from v$instance;

 

DATABASE_STATUS
—————–
SUSPENDED

1、昨天导数据过慢的原因:
     昨天新建数据库的时候,选择了utf-8,原本备份的数据库是JA16JIS,
     在导数据期间会发生数据超长的错误
     (昨天由于没有人在电脑前面盯着,所以出现错误也不知道)。
   *  除了UTF8字符集外,其他字符集在存储本地字符时将会使用两个字节,
       但如果使用UTF8,则需要占用3个字节的空间。
       所以数据会超长。
——————————————————————————————————————
2、删除用户时,由于该用户被其他人正使用着,所以必须将连接断开。
     刚开始敲“svrmgrl”命令,结果不好使,一顿困惑,
     后来在网上查出“oracle9不支持svrmgrl”。
     解决办法 : 使用sqlplus操作。

    C:\Documents and Settings\Administrator>sqlplus /nolog
    SQL*Plus: Release 9.2.0.1.0 – Production on 木 Mar 30 09:44:13 2006
    Copyright (c) 1982, 2002, Oracle Corporation.  All rights reserved.
    SQL> connect /as sysdba
    接続されました。
    SQL> shutdown immediate
    データベースがクローズされました。
    データベースがアンマウントされました。
    ORACLEインスタンスがシャットダウンされました。

  
     关闭了oracle实例。

2006年03月28日

今天服务器的电源挂掉了,只能把硬盘取下来,
安置在新的机器上,启动完Oracle服务后,
通过控制台和plsql均无法访问数据库,
出现“ora-12560 tns protocol adapter error  ”错误。
—————————————————————————–
1. 检查Terminal Service,看远程桌面是不是起因
2. 检查环境变量ORACLE_SID是否正确,包括注册表中的设定(检查注册表HKEY_LOCAL_MACHINE\Software\Oracle\key_oracle
沒有 oracle_sid=whora這項值,把這項值增加進去重新連,問題就解決了)
3. 检查Oracle服务是否启动
4. 检查LISTENER.ORA,TNSNAME.ORA等的机器名(IP地址)或者服务名是否正确
5. Netstat –a检查端口是否被占用
6. 检查注册表HKEY_LOCAL_MACHINE\Software\Oracle\Home0新增字符串USE_SHARED_SOCKET=TRUE, 重新启动服务

对于运行在Windows下的Oracle 8i还可能是如下情况:

1. 在Server本机执行Svrmgrl或Sqlplus时报该错误,此时需检查ORACLE_SID设置是否正确,包括注册表中的设定,并检查Service是否运行
2. 如果SID设置正确且服务也已经运行,如果这时还报该错误,则应该检查SID NAME是否包含了非字母字符,对于Windows下的Oracle
      8i,SID NAME不允许包含非字母字符,比如下划线或横线
3. 如果出现顽固性的ORA-12560错误,可考虑自己写ORACLE启动及关闭脚本,因为Windows的oracle service自动启动不是很完善

其他情况要具体分析,比如有可能是连接数过多造成内存消耗殆尽,也会造成ora-12560错误。

参考文档:
Note 1070749.6,73399.1
Note 1016454.102 TNS 12560 DB CREATE VIA INSTALLATION OR CONFIGURATION ASSISTANT FAILS
Bug 948671 ORADIM SUCCSSFULLY CREATES AN UNUSABLE SID WITH NON-ALPHANUMERIC   CHARACTER
Bug 892253 ORA-12560 CREATING DATABASE WITH DB CONFIGURATION ASSISTANT IF  SID HAS NON-ALPHA
———————————————————————————————–
我装的oracle 9,操作系统是xp-sp2,
只是将远程控制的服务变为“无效”了,重启机器就可以正常访问oracle了。

不过有一点奇怪的是,我的远程服务按钮为什么都是灰色的?
“启动”“停止”“重启动”这三个按钮,
我只能将服务设为“无效”,然后重启机器了。

2006年03月24日

在Jdon.com里面看到的,觉得很值得借鉴
原文:
http://www.jdon.com/jive/thread.jsp?forum=16&thread=302

看下面比较:

public List getUsers() 
ResultSet rs = userDbQuery();
List retval = new ArrayList();
while (rs.next()) {
retval.add(rs.getString(1));
}
return retval;
}

上面是个我们采取返回Collection后最常用的方法,将ResultSet中的用户名加入List再返回,显然这很耗费内存。

使用Iterator返回:

public Iterator getUsers() {
final ResultSet rs = userDbQuery();
return new Iterator() {
private Object next;

public void hasNext() {
if (next == null{
if (! rs.next()) {
return false
} 
next = rs.getString(1);
}
return true;
}

public Object next() {
if (! hasNext()) {
throw new NoSuchElementException();
}
String retval = next;
next = null;
return retval;
}

public void remove() {
throw new UnsupportedOperationException("no remove allowed");
}
}
}

这个Javabean只是做了一个指针传递作用,将调用本Javabean的指针传递到ResultSet,这样既提高了效率,节约了内存,又降低了偶合性,这是堪称中间件典型的示范。

2006年03月23日

(大量参考PoEAA…-_-…)


    最近网上看到这样的帖子<<谁能告诉我Hibernate有何优秀之处>> 作者给出了对hibernate在实现orm的诸多不满和怀疑。而且很多开发人员都有着这样疑问,“怎么这个流行东东,我用起来就是不顺手呢?,“或许这个东东只是用来吹牛的”。然而,这或许很可能是因为,你习惯的架构模式开发策略并不适合使用orm这种持久化模式。用了反而是加大复杂度降低性能和效率。比如,用惯了resultset,喜欢把resultset放在表示层作为展示的数据结构的人。这样的开发人员因为习惯了二维表为核心进行开发,对于Hibernate这样的orm工具根本就没办法了解其优势了。但是这并不是说这种以二维表的架构模式就比较差了,只是不同的模式有不同的优缺点就看所在的应用是否适合使用了。或许你就一直在使用这样的模式在开发应用,自己还没有意识到(甚至一直以为这样的方式可以适应任何应用,:-)。反而被其他架构模式中才会用到的框架搞的晕头转向甚至被误导。事实上,像前面提到的以二维表为核心的架构被称之为表模块架构(设计)模式。

       何为“表模块”(table module)?“处理某一个数据库表、视图或虚拟表中所有行为的业务逻辑组成的一个实例”。最初,我就是从马丁同学的POEAA中得到的这个定义。在书中他被定义在DomainLogic 层次或者该层次的一部分,DomainLogic也就是其他架构模型中的BizLogic层。与该模式相对应个还有完成相似功能的DomainModel以及TransactionScript模式。该层次的职责是专门处理业务逻辑。

       简单的讲,所谓“表模块”就是以一个类对应数据库中一个表(视图)的数据来组织业务逻辑。从设计层次表妙面上看这种结构很像经典OO模型,同样是以不同对象对应不同逻辑。但是,它与领域建模不同之处在于,它的对象并不表示一个问题领域对象在软件系统中的映射。而是表示对一组数据的处理逻辑,而通常这组数据的结构是重数据库表或试图上的二维数据结构。当然也可以来自其他集成部分,比如返回两位数据结构的webservice

       这里给出个比较形象的例子:


<!–[if !vml]–>
<!–[endif]–>

       (上图)感觉像比较标准的OO吧,好象一个Contract的对象表示合同,而Product表示产品,还有他们之间的关联关系。但是其实现实给出几个方法的时候就看出区别了。Product内部的数据其实就是一个两位结构的rowset。而不像真正的领域模型分析出来的表示一个Product的数据,Contract也是如此。

<!–[if !vml]–>
<!–[endif]–>

       你会发现tableModule的类中一个对象其实对应一个表的数据,其拥有的操作也有很多是多数据记录的。多数据记录的操作在真正的领域中往往应该放在类似Manager这样的实体中,而不是业务实体本身。

       使用这种模式实现的业务逻辑层不像领域模型模式可以适应搞复杂度的逻辑。但是由于它更亲近于数据持久的二维表,没有像领域模型那样需要复杂的ORM支持。书对于数据驱动的应用或模块非常适合。POEAA中也已经给出了这种bizLogic最有效的持久层实现,就是table getway,当然也可以使用其他的方式比如“查询工厂”。

       事实上,最多使用这种模式的情况是当后台数据源大部分都是一种两维甚至多维数据结构的时候进行。比如一个极端的例子(几乎没有业务),一个应用的复杂查询模块,这种模块就是一个查询模型加上一组对数据进行排列统计过滤的TableModule

       

<!–[if !vml]–><!–[endif]–>

       表模块还具有很多特点:处理的二维数据表将成为代码的核心部分。所以,是否使用tableModule还会取决于整个应用的其他部分是否对这种二维表提供强大的支持。(如上图)

       优点:持久层架构简单,在复杂度大的以数据为核心的应用中开发效率极高。而且表模块能够识别的table越多,能够处理table的组建工具越强,这种模型的能力越强。

       缺点:不能适应复杂的应用逻辑,或者与表结构差别较大的应用逻辑。由于没有对内部的两位数据表(table)进行有效标识的方式(比如,无法快速简单的确定使用table是否是需要的数据内容),对于大量细颗粒的逻辑会增加其复杂度。

       其实java中的 rowset/jdbc resultset.net中的 dateSet/dataTable都属于表模块操作的二维表。但是,由于.netGUI层面以及持久化层面对他自己的二维表的强大支持使得 TableModule几乎成为MS.Net上的首选模式。这也就可以解释了,许多.net开发人员一直抱怨,看到dataset/dataTable泛滥,甚至在WebService中也是这样。而java中却有所不同,虽然有持久化用到的jdbc/resultSet的支持(事实上jdbc/ado 给出的接口都只直接适合TableModule。),然而其他方面就少得可怜了。所以,使用java还是以其他的bizLogic架构模式为好。除非你的逻辑真的很简单,简单到把resultSet放在jsp中都不会造成逻辑混乱。又或者你能够自己提供一套完整的 rowset组建框架。

(http://www.blogjava.net/wfeng007/archive/2006/03/13/35021.html)

前两天在和讯上发表文章,很令人奇怪的事情,日文可以发表,韩文就不行了,怪怪的~~
到底是与什么有关系呢?和讯的数据库?操作系统?IE?or others……
操作系统是英文版2003,区域默认语言是日文,IE优先语言中文,哈哈乱套了~
刚好看到一篇关于unicode的文章,嗯~~ 看看吧,也许会有些收获。

———————————————————————————————

Unicode是一种字符编码规范 。

先从ASCII说起。ASCII是用来表示英文字符的一种编码规范,每个ASCII字符占用1个字节(8bits)
因此,ASCII编码可以表示的最大字符数是256,其实英文字符并没有那么多,一般只用前128个(最高位为0),其中包括了控制字符、数字、大小写字母和其他一些符号 。
而最高位为1的另128个字符被成为“扩展ASCII”,一般用来存放英文的制表符、部分音标字符等等的一些其他符号
这种字符编码规范显然用来处理英文没有什么问题 。(实际上也可以用来处理法文、德文等一些其他的西欧字符,但是不能和英文通用),但是面对中文、阿拉伯文之类复杂的文字,255个字符显然不够用,于是,各个国家纷纷制定了自己的文字编码规范,其中中文的文字编码规范叫做“GB2312-80”,它是和ASCII兼容的一种编码规范,其实就是利用扩展ASCII没有真正标准化这一点,把一个中文字符用两个扩展ASCII字符来表示。

       但是这个方法有问题,最大的问题就是,中文文字没有真正属于自己的编码,因为扩展ASCII码虽然没有真正的标准化,但是PC里的ASCII码还是有一个事实标准的(存放着英文制表符),所以很多软件利用这些符号来画表格。这样的软件用到中文系统中,这些表格符就会被误认作中文字,破坏版面。而且,统计中英文混合字符串中的字数,也是比较复杂的,我们必须判断一个ASCII码是否扩展,以及它的下一个ASCII是否扩展,然后才“猜”那可能是一个中文字 。 

       总之当时处理中文是很痛苦的。而更痛苦的是GB2312是国家标准,台湾当时有一个Big5编码标准,很多编码和GB是相同的,所以……,嘿嘿。

       这时候,我们就知道,要真正解决中文问题,不能从扩展ASCII的角度入手,也不能仅靠中国一家来解决。而必须有一个全新的编码系统,这个系统要可以将中文、英文、法文、德文……等等所有的文字统一起来考虑,为每个文字都分配一个单独的编码,这样才不会有上面那种现象出现。

       于是,Unicode诞生了。

       Unicode有两套标准,一套叫UCS-2(Unicode-16),用2个字节为字符编码,另一套叫UCS-4(Unicode-32),用4个字节为字符编码。

       以目前常用的UCS-2为例,它可以表示的字符数为2^16=65535,基本上可以容纳所有的欧美字符和绝大部分的亚洲字符 。

        UTF-8的问题后面会提到 。

      在Unicode里,所有的字符被一视同仁。汉字不再使用“两个扩展ASCII”,而是使用“1个Unicode”,注意,现在的汉字是“一个字符”了,于是,拆字、统计字数这些问题也就自然而然的解决了 。

       但是,这个世界不是理想的,不可能在一夜之间所有的系统都使用Unicode来处理字符,所以Unicode在诞生之日,就必须考虑一个严峻的问题:和ASCII字符集之间的不兼容问题。

      我们知道,ASCII字符是单个字节的,比如“A”的ASCII是65。而Unicode是双字节的,比如“A”的Unicode是0065,这就造成了一个非常大的问题:以前处理ASCII的那套机制不能被用来处理Unicode了 。

      另一个更加严重的问题是,C语言使用’\0′作为字符串结尾,而Unicode里恰恰有很多字符都有一个字节为0,这样一来,C语言的字符串函数将无法正常处理Unicode,除非把世界上所有用C写的程序以及他们所用的函数库全部换掉 。

      于是,比Unicode更伟大的东东诞生了,之所以说它更伟大是因为它让Unicode不再存在于纸上,而是真实的存在于我们大家的电脑中。那就是:UTF 。

      UTF= UCS Transformation Format UCS转换格式

      它是将Unicode编码规则和计算机的实际编码对应起来的一个规则。现在流行的UTF有2种:UTF-8和UTF-16 。

       其中UTF-16和上面提到的Unicode本身的编码规范是一致的,这里不多说了。而UTF-8不同,它定义了一种“区间规则”,这种规则可以和ASCII编码保持最大程度的兼容 。

      UTF-8有点类似于Haffman编码,它将Unicode编码为00000000-0000007F的字符,用单个字节来表示;

      00000080-000007FF的字符用两个字节表示

      00000800-0000FFFF的字符用3字节表示

      因为目前为止Unicode-16规范没有指定FFFF以上的字符,所以UTF-8最多是使用3个字节来表示一个字符。但理论上来说,UTF-8最多需要用6字节表示一个字符。

      在UTF-8里,英文字符仍然跟ASCII编码一样,因此原先的函数库可以继续使用。而中文的编码范围是在0080-07FF之间,因此是2个字节表示(但这两个字节和GB编码的两个字节是不同的),用专门的Unicode处理类可以对UTF编码进行处理。

      下面说说中文的问题。

由于历史的原因,在Unicode之前,一共存在过3套中文编码标准。

GB2312-80,是中国大陆使用的国家标准,其中一共编码了6763个常用简体汉字。Big5,是台湾使用的编码标准,编码了台湾使用的繁体汉字,大概有8千多个。HKSCS,是中国香港使用的编码标准,字体也是繁体,但跟Big5有所不同。

这3套编码标准都采用了两个扩展ASCII的方法,因此,几套编码互不兼容,而且编码区间也各有不同

因为其不兼容性,在同一个系统中同时显示GB和Big5基本上是不可能的。当时的南极星、RichWin等等软件,在自动识别中文编码、自动显示正确编码方面都做了很多努力 。

他们用了怎样的技术我就不得而知了,我知道好像南极星曾经以同屏显示繁简中文为卖点。

后来,由于各方面的原因,国际上又制定了针对中文的统一字符集GBK和GB18030,其中GBK已经在Windows、Linux等多种操作系统中被实现。

GBK兼容GB2312,并增加了大量不常用汉字,还加入了几乎所有的Big5中的繁体汉字。但是GBK中的繁体汉字和Big5中的几乎不兼容。

GB18030相当于是GBK的超集,比GBK包含的字符更多。据我所知目前还没有操作系统直接支持GB18030。

谈谈Unicode编码,简要解释UCS、UTF、BMP、BOM等名词
这是一篇程序员写给程序员的趣味读物。所谓趣味是指可以比较轻松地了解一些原来不清楚的概念,增进知识,类似于打RPG游戏的升级。整理这篇文章的动机是两个问题:

问题一:
使用Windows记事本的“另存为”,可以在GBK、Unicode、Unicode big endian和UTF-8这几种编码方式间相互转换。同样是txt文件,Windows是怎样识别编码方式的呢?

我很早前就发现Unicode、Unicode big endian和UTF-8编码的txt文件的开头会多出几个字节,分别是FF、FE(Unicode),FE、FF(Unicode big endian),EF、BB、BF(UTF-8)。但这些标记是基于什么标准呢?

问题二:
最近在网上看到一个ConvertUTF.c,实现了UTF-32、UTF-16和UTF-8这三种编码方式的相互转换。对于Unicode(UCS2)、GBK、UTF-8这些编码方式,我原来就了解。但这个程序让我有些糊涂,想不起来UTF-16和UCS2有什么关系。
查了查相关资料,总算将这些问题弄清楚了,顺带也了解了一些Unicode的细节。写成一篇文章,送给有过类似疑问的朋友。本文在写作时尽量做到通俗易懂,但要求读者知道什么是字节,什么是十六进制。

0、big endian和little endian
big endian和little endian是CPU处理多字节数的不同方式。例如“汉”字的Unicode编码是6C49。那么写到文件里时,究竟是将6C写在前面,还是将49写在前面?如果将6C写在前面,就是big endian。还是将49写在前面,就是little endian。

“endian”这个词出自《格列佛游记》。小人国的内战就源于吃鸡蛋时是究竟从大头(Big-Endian)敲开还是从小头(Little-Endian)敲开,由此曾发生过六次叛乱,其中一个皇帝送了命,另一个丢了王位。

我们一般将endian翻译成“字节序”,将big endian和little endian称作“大尾”和“小尾”。

1、字符编码、内码,顺带介绍汉字编码
字符必须编码后才能被计算机处理。计算机使用的缺省编码方式就是计算机的内码。早期的计算机使用7位的ASCII编码,为了处理汉字,程序员设计了用于简体中文的GB2312和用于繁体中文的big5。

GB2312(1980年)一共收录了7445个字符,包括6763个汉字和682个其它符号。汉字区的内码范围高字节从B0-F7,低字节从A1-FE,占用的码位是72*94=6768。其中有5个空位是D7FA-D7FE。

GB2312支持的汉字太少。1995年的汉字扩展规范GBK1.0收录了21886个符号,它分为汉字区和图形符号区。汉字区包括21003个字符。2000年的GB18030是取代GBK1.0的正式国家标准。该标准收录了27484个汉字,同时还收录了藏文、蒙文、维吾尔文等主要的少数民族文字。现在的PC平台必须支持GB18030,对嵌入式产品暂不作要求。所以手机、MP3一般只支持GB2312。

从ASCII、GB2312、GBK到GB18030,这些编码方法是向下兼容的,即同一个字符在这些方案中总是有相同的编码,后面的标准支持更多的字符。在这些编码中,英文和中文可以统一地处理。区分中文编码的方法是高字节的最高位不为0。按照程序员的称呼,GB2312、GBK到GB18030都属于双字节字符集 (DBCS)。

有的中文Windows的缺省内码还是GBK,可以通过GB18030升级包升级到GB18030。不过GB18030相对GBK增加的字符,普通人是很难用到的,通常我们还是用GBK指代中文Windows内码。

这里还有一些细节:

GB2312的原文还是区位码,从区位码到内码,需要在高字节和低字节上分别加上A0。

在DBCS中,GB内码的存储格式始终是big endian,即高位在前。

GB2312的两个字节的最高位都是1。但符合这个条件的码位只有128*128=16384个。所以GBK和GB18030的低字节最高位都可能不是1。不过这不影响DBCS字符流的解析:在读取DBCS字符流时,只要遇到高位为1的字节,就可以将下两个字节作为一个双字节编码,而不用管低字节的高位是什么。

2、Unicode、UCS和UTF
前面提到从ASCII、GB2312、GBK到GB18030的编码方法是向下兼容的。而Unicode只与ASCII兼容(更准确地说,是与ISO-8859-1兼容),与GB码不兼容。例如“汉”字的Unicode编码是6C49,而GB码是BABA。

Unicode也是一种字符编码方法,不过它是由国际组织设计,可以容纳全世界所有语言文字的编码方案。Unicode的学名是"Universal Multiple-Octet Coded Character Set",简称为UCS。UCS可以看作是"Unicode Character Set"的缩写。

根据维基百科全书(http://zh.wikipedia.org/wiki/)的记载:历史上存在两个试图独立设计Unicode的组织,即国际标准化组织(ISO)和一个软件制造商的协会(unicode.org)。ISO开发了ISO 10646项目,Unicode协会开发了Unicode项目。

在1991年前后,双方都认识到世界不需要两个不兼容的字符集。于是它们开始合并双方的工作成果,并为创立一个单一编码表而协同工作。从Unicode2.0开始,Unicode项目采用了与ISO 10646-1相同的字库和字码。

目前两个项目仍都存在,并独立地公布各自的标准。Unicode协会现在的最新版本是2005年的Unicode 4.1.0。ISO的最新标准是10646-3:2003。

UCS规定了怎么用多个字节表示各种文字。怎样传输这些编码,是由UTF(UCS Transformation Format)规范规定的,常见的UTF规范包括UTF-8、UTF-7、UTF-16。

IETF的RFC2781和RFC3629以RFC的一贯风格,清晰、明快又不失严谨地描述了UTF-16和UTF-8的编码方法。我总是记不得IETF是Internet Engineering Task Force的缩写。但IETF负责维护的RFC是Internet上一切规范的基础。

3、UCS-2、UCS-4、BMP

UCS有两种格式:UCS-2和UCS-4。顾名思义,UCS-2就是用两个字节编码,UCS-4就是用4个字节(实际上只用了31位,最高位必须为0)编码。下面让我们做一些简单的数学游戏:

UCS-2有2^16=65536个码位,UCS-4有2^31=2147483648个码位。

UCS-4根据最高位为0的最高字节分成2^7=128个group。每个group再根据次高字节分为256个plane。每个plane根据第3个字节分为256行 (rows),每行包含256个cells。当然同一行的cells只是最后一个字节不同,其余都相同。

group 0的plane 0被称作Basic Multilingual Plane, 即BMP。或者说UCS-4中,高两个字节为0的码位被称作BMP。

将UCS-4的BMP去掉前面的两个零字节就得到了UCS-2。在UCS-2的两个字节前加上两个零字节,就得到了UCS-4的BMP。而目前的UCS-4规范中还没有任何字符被分配在BMP之外。

4、UTF编码

UTF-8就是以8位为单元对UCS进行编码。从UCS-2到UTF-8的编码方式如下:

UCS-2编码(16进制) UTF-8 字节流(二进制)
0000 – 007F 0xxxxxxx
0080 – 07FF 110xxxxx 10xxxxxx
0800 – FFFF 1110xxxx 10xxxxxx 10xxxxxx

例如“汉”字的Unicode编码是6C49。6C49在0800-FFFF之间,所以肯定要用3字节模板了:1110xxxx 10xxxxxx 10xxxxxx。将6C49写成二进制是:0110 110001 001001, 用这个比特流依次代替模板中的x,得到:11100110 10110001 10001001,即E6 B1 89。

读者可以用记事本测试一下我们的编码是否正确。

UTF-16以16位为单元对UCS进行编码。对于小于0×10000的UCS码,UTF-16编码就等于UCS码对应的16位无符号整数。对于不小于0×10000的UCS码,定义了一个算法。不过由于实际使用的UCS2,或者UCS4的BMP必然小于0×10000,所以就目前而言,可以认为UTF-16和UCS-2基本相同。但UCS-2只是一个编码方案,UTF-16却要用于实际的传输,所以就不得不考虑字节序的问题。

5、UTF的字节序和BOM
UTF-8以字节为编码单元,没有字节序的问题。UTF-16以两个字节为编码单元,在解释一个UTF-16文本前,首先要弄清楚每个编码单元的字节序。例如收到一个“奎”的Unicode编码是594E,“乙”的Unicode编码是4E59。如果我们收到UTF-16字节流“594E”,那么这是“奎”还是“乙”?

Unicode规范中推荐的标记字节顺序的方法是BOM。BOM不是“Bill Of Material”的BOM表,而是Byte Order Mark。BOM是一个有点小聪明的想法:

在UCS编码中有一个叫做"ZERO WIDTH NO-BREAK SPACE"的字符,它的编码是FEFF。而FFFE在UCS中是不存在的字符,所以不应该出现在实际传输中。UCS规范建议我们在传输字节流前,先传输字符"ZERO WIDTH NO-BREAK SPACE"。

这样如果接收者收到FEFF,就表明这个字节流是Big-Endian的;如果收到FFFE,就表明这个字节流是Little-Endian的。因此字符"ZERO WIDTH NO-BREAK SPACE"又被称作BOM。

UTF-8不需要BOM来表明字节顺序,但可以用BOM来表明编码方式。字符"ZERO WIDTH NO-BREAK SPACE"的UTF-8编码是EF BB BF(读者可以用我们前面介绍的编码方法验证一下)。所以如果接收者收到以EF BB BF开头的字节流,就知道这是UTF-8编码了。

Windows就是使用BOM来标记文本文件的编码方式的。

6、进一步的参考资料
本文主要参考的资料是 "Short overview of ISO-IEC 10646 and Unicode" (http://www.nada.kth.se/i18n/ucs/unicode-iso10646-oview.html)。

我还找了两篇看上去不错的资料,不过因为我开始的疑问都找到了答案,所以就没有看:

"Understanding Unicode A general introduction to the Unicode Standard" (http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&item_id=IWS-Chapter04a)
"Character set encoding basics Understanding character set encodings and legacy encodings" (http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&item_id=IWS-Chapter03)