架构设计 例子和实践

系统设计说明书(架构、概要、详细)目录结构

虽然这些文档一般来说公司都是有模板的,但我写这些文档以来基本上是每写一次就把目录结构给改一次,应该说这是因为自己对这些文档的理解开始加深,慢慢的越来越明白这些文档的作用和其中需要阐述的东西,觉得这三份文档主要阐述了一个系统的设计和实现过程,从系统分解为层次、层次内的模块以及相互的接口、模块分解为对象以及对象的接口、实现这些对象接口的方法。这次又整了一份,^_^,欢迎大家指正。

XXX架构设计说明书

(架构设计重点在于将系统分层并产生层次内的模块、阐明模块之间的关系)

一.  概述

描述本文的参考依据、资料以及大概内容。

二.  目的

描述本文编写的目的。

三.  架构设计

阐明进行架构设计的总体原则,如对问题域的分析方法。

3.1.       架构分析

对场景以及问题域进行分析,构成系统的架构级设计,阐明对于系统的分层思想。

3.2.       设计思想

阐明进行架构设计的思想,可参考一些架构设计的模式,需结合当前系统的实际情况而定。

3.3.       架构体系

根据架构分析和设计思想产生系统的架构图,并对架构图进行描述,说明分层的原因、层次的职责,并根据架构图绘制系统的物理部署图,描述系统的部署体系。

3.4.       模块划分

根据架构图进行模块的划分并阐明模块划分的理由,绘制模块物理图以及模块依赖图。

3.4.1.       模块描述

根据模块物理图描述各模块的职责,并声明其对其他模块的接口要求。。

3.4.2.       模块接口设计

对模块接口进行设计,并提供一定的伪代码。


XXX概要设计说明书

(概要设计重点在于将模块分解为对象并阐明对象之间的关系)

一.  概述

描述本文的参考依据、资料以及大概内容。

二.  目的

描述本文的编写目的。

三.  模块概要设计

引用架构设计说明书中的模块图,并阐述对于模块进行设计的大致思路。

3.1.       设计思想

阐明概要设计的思想,概要设计的思想通常是涉及设计模式的。

3.2.       模块A

3.2.1.       概要设计

根据该模块的职责对模块进行概要设计(分解模块为对象、描述对象的职责以及声明对象之间的接口),绘制模块的对象图、对象间的依赖图以及模块主要功能的序列图,分别加以描述并相应的描述模块异常的处理方法。

3.2.2.       模块接口实现

阐明对于架构设计中定义的模块接口的实现的设计。

XXX详细设计说明书

(详细设计重点在于对模块进行实现,将模块的对象分解为属性和方法,并阐述如何实现)



一.  概述

阐述本文的参考依据、资料以及大概内容。

二.  目的

阐述本文的编写目的。

三.  模块详细设计

3.1.       设计思想

阐述对模块进行详细设计的思想。

3.2.       模块A

3.2.1.       详细设计

根据模块概要设计详细描述对于模块内对象的实现,包括对象的职责、属性、方法、对象内功能的流程图、对象关联的类、对象的异常。(需要绘制的主要为类图)


在大型的项目中是有必要分开的....
架构解决系统核心用例以及关键性需求的设计,形成抽象的基础结构,划分模块、形成模块接口.
概要解决模块以及模块接口的实现,形成模块中核心对象以及对象的接口定义..

详细解决模块中具体对象的实现以及对象接口的实现


演进架构中的领域驱动设计

原文链接:http://www.infoq.com/cn/articles/ddd-evolving-architecture

作者Mat Wall and Nik Silver译者王丽娟发布于2009年9月21日 上午11时9分

社区
Architecture,
Java
主题
设计,
面向对象设计,
建模
标签
Hibernate,
领域驱动设计

领域驱动设计能非常容易地应用于稳定领域,其中的关键活动适合开发人员对用户脑海中的内容进行记录和建模。但在领域本身不断变化和发展的情况下,领域驱动设计变得更具有挑战性。这在敏捷项目中很普遍,在业务本身试图演进的时候也会发生。本文分析了在反思、重建guardian.co.uk这一为期两年的计划背景下我们是如何利用DDD的。我们展示了如何确保在软件架构中反映最终用户演变的认知,以及怎样实现该架构来保证以后的变化。我们提供了模型中重要项目过程、具体演进步骤的细节。

顶层标题:

  1. 计划背景
  2. 从DDD开始
  3. 增量计划中的DDD过程
  4. 进化的领域模型
  5. 代码级别的演进
  6. 演进架构中DDD的一些教训
  7. 附录:具体示例

1. 计划背景

Guardian.co.uk有相当长的新闻、评论、特性历史记录,目前已拥有超过1800万的独立用户和每月1.8亿的页面访问量。在此期间的大部分时间,网站都运行在原始的Java技术之上,但在2006年2月开始了一项重要的工作计划——将网站移植到一个更现代的平台上去,计划的最初阶段于2006年11月推出,当时推出了具有新外观的旅游网站,接着在2007年5月推出新的主页,之后相继推出了更多内容。尽管在2006年2月仅有少数几个人在做这项工作,但后来团队最多曾达到了104人。

然而,计划的动机远不止是要一个新外观。多年的经验告诉我们有更好的办法来组织我们的内容,有更好的方式将我们的内容商业化,以及在此背后,还有更先进的开发方法——这才是关键之所在。

其实,我们考虑工作的方式已超越了我们软件可以处理的内容。这就是为什么DDD对我们来说如此有价值。

遗留软件中阻碍我们的概念不匹配有两个方面,我们先简单看一下这两方面问题,首先是我们的内部使用者,其次是我们的开发人员。这些问题都需要借助DDD予以解决。

1.1. 内部使用者的问题

新闻业是一个古老的行业,有既定的培训、资格和职制,但对新闻方面训练有素的新编辑来说,加入我们的行列并使用Web工具有效地工作是不可能的,尤其在刚来的几个月里。要成为一个高效的使用者,了解我们CMS和网站的关键概念还远远不够,还需要了解它们是如何实现的。

比如说,将缓存内容的概念(这应该完全是系统内部的技术优化)暴露给编辑人员;编辑们要将内容放置在缓存中以确保内容已准备好,还需要理解缓存工作流以用CMS工具诊断和解决问题。这显然是对编辑人员不合理的要求。

1.2. 开发人员的问题

概念上的不匹配也体现在技术方面。举例来说,CMS有一个概念是“制品”,这完全是所有开发人员每天工作的核心。以前团队中的一个人坦言,在足足九个月之后他才认识到这些“制品”其实就是网页。围绕“制品”形成的含义模糊的语言和软件越来越多,这些东西让他工作的真正性质变得晦涩难懂。

再举一个例子,生成我们内容的RSS订阅特别耗时。尽管各个版块的主页都包含一个清晰的列表,里面有主要内容和附加内容,但底层软件并不能对两者予以区分。因此,从页面抽取RSS订阅的逻辑是混乱的,比如“在页面上获取每个条目,如果它的布局宽大约一半儿、长度比平均长度长一些,那它可能是主要内容,这样我们就能抽取链接、将其作为一个订阅”。

很显然,对我们来说,人们对他们工作(开始、网页和RSS订阅)及其如何实现(缓存工作流、“制品”、混乱逻辑)的认识之间的分歧给我们的效益造成了明显而惨重的影响。

2. 从DDD开始

本部分阐述了我们使用DDD的场景:为什么选择它,它在系统架构中所处的位置,还有最初的领域模型。在后面的章节中,我们会看一下如何把最初的领域知识传播给扩充的团队,如何演进模型,以及如何以此为中心来演进我们的编码技术。

2.1. 选择DDD

DDD所倡导的首要方面就是统一一致的语言,以及在代码中直接体现用户自己的概念。这能有效地解决前面提及的概念上的不匹配问题。单独看来,这是一个有价值的见解,但其本身价值或许并不比“正确使用面向对象技术”多很多。

使其深入的是DDD引入的技术语言和概念:实体、值对象、服务、资源库等。这确保了在处理非常大的项目时,我们的大型开发团队有可能一致地进行开发——长远来看,这对维护质量是必不可少的。甚至在废弃我们更底层的代码时(我们稍后会进行演示),统一的技术语言能让我们恢复到过去、改进代码质量。

2.2. 在系统中嵌入领域模型

本节显示了DDD在整个系统架构中的地位。

我们的系统逐渐建立了三个主要的组件:渲染应用的用户界面网站;面向编辑、用于创建和管理内容的应用程序;跟系统交互数据的供稿系统。这些应用都是基于Spring和Hibernate构建的Java Web应用,并使用Velocity作为我们的模板语言。

我们可以看下这些应用的布局:

Hibernate层提供数据访问,使用EHCache作为Hibernate的二级缓存。模型层包含领域对象和资源库,服务则处于其上一层。在此之上,我们有Velocity模板层,它提供页面渲染逻辑。最顶层则包含控制器,是应用的入口点。

看一下这个应用的分层架构,仅仅将模型当做是应用的一个自包含的层是很有意思的。这个想法大致正确,但模型层和其它层之间还是有一些细微的差别:由于我们使用领域驱动设计,所以我们需要统一语言,不仅要在我们谈论领域时使用,还要在应用的任何地方使用。模型层的存在不仅是为了从渲染逻辑中分离业务逻辑,还要为使用的其它层提供词汇表。

另外,模型层可作为代码的独立单元进行构建,而且可以作为JAR包导入到依赖于它的许多应用中。其它任何层则不是这样。对构建和发布应用来说,这意味着:在我们基础设施的模型层改变代码一定是跨所有应用的全局变化。我们可以在前端的网站中改变Velocity模板,只用部署前端应用就可以,管理系统或供稿系统则不用。如果我们在领域模型中改变了一个对象的逻辑,我们必须更新所有依赖于模型的应用,因为我们只有(而且期望只有)一个领域视图。

这有一种危害,就是领域建模的这种方法可能会导致单一模型,如果业务领域非常庞大,改变起来的代价会非常昂贵。我们认识到了这一点,因此随着领域不断增长,我们必须确保该层没有变得太过笨重。目前,虽然领域层也是相当庞大和复杂的,但是这还没有带来什么问题。在敏捷环境中工作,我们希望无论如何每两周都要推出所有系统的最新变化。但我们持续关注着该层代码改变的成本。如果成本上升到不能接受的程度,我们可能会考虑将单一模型细分成多个更小的模型,并在每个子模型之间给出适配层。但是我们在项目开始时没有这样做,我们更偏向于单一模型的简单性,而不是用多个模型时必须解决的更为复杂的依赖管理问题。

2.3. 早期的领域建模

在项目初期,大家在键盘上动手开始编写代码之前,我们就决定让开发人员、QA、BA、业务人员在同一个房间里一起工作,以便项目能够持续。在这个阶段我们有一个由业务人员和技术人员组成的小型团队,而且我们只要求有一个稳妥的初次发布。这确保了我们的模型和过程都相当简单。

我们的首要目标是让编辑(我们业务代表的关键组成部分)就项目最初迭代的期望有一个清楚的认识。我们跟编辑们坐在一起,就像一个整体团队一样,用英语与他们交流想法,允许各个功能的代表对这些想法提出质疑和澄清,直到他们认为我们正确理解了编辑需要的内容。

我们的编辑认为,项目初期优先级最高的功能是系统能生成网页,这些网页能显示文章和文章分类系统。

他们最初的需求可归纳为:

  • 我们应该能将一篇文章和任何给定的URL关联起来。
  • 我们要能改变选定的不同模板渲染结果页面的方式。
  • 为了管理,我们要将内容纳入宽泛的版面,也就是新闻、体育、旅游。
  • 系统必须能显示一个页面,该页面包含指向特定分类中所有文章的链接。

我们的编辑需要一种非常灵活的方式来对文章进行分类。他们采用了基于关键字的方法。每个关键字定义了一个与内容相关的主题。每篇文章可以和很多关键字关联,因为一篇文章可以有很多主题。

我们网站有很多编辑,每个人负责不同版块的内容。每个版块都要求有自己导航和特有关键字的所有权。

从编辑使用的语言来看,我们似乎在领域中引入了一些关键实体:

  • 页面 URL的拥有者。负责选择模板来渲染内容。
  • 模板 页面布局,任何时候都有可能改变。技术人员将每个模板实现为磁盘上的一个Velocity文件。
  • 版块 页面更宽泛的分类。每个版块有一个编辑,还有对其中页面共同的感官。新闻、旅游和商业都是版块的例子。
  • 关键字 描述存在于版块中主题的方式。关键字过去用于文章分类,现在则驱动自动导航。这样它们将与某个页面关联,关于给定主题所有文章的自动页面也能生成。
  • 文章 我们能发布给用户的一段文本内容。

提取这些信息后,我们开始对领域建模。项目早期做出的一个决定:编辑拥有领域模型并负责设计,技术团队则提供协助。对过去不习惯这种技术设计的编辑来说,这是相当大的转变。我们发现,通过召开由编辑、开发人员、技术架构师组成的研习会,我们能用简单、技术含量较低的方法勾画出领域模型,并对它持续演进。讨论模型的范围,使用钢笔、档案卡和Blu-Tak绘制备选解决方案。每个候选模型都会进行讨论,技术团队把设计中的每个细节含义告诉给编辑。

尽管过程在最初相当缓慢,但很有趣。编辑发现这非常容易上手;他们能信手拈来、提出对象,然后及时从开发人员那里获得反馈,以知道生成的模型是否满足了他们的需求。对于编辑们能在过程中迅速掌握技术的要领,技术人员都感到很惊喜,而且所有人都对生成的系统是否能满足客户需求很有信心。

观察领域语言的演变也很有趣。有时文章对象会被说成是“故事”。显然对于同一实体的多个名称,我们并没有一种统一语言,这是一个问题。是我们的编辑发现他们描述事物时没有使用统一的语言,也是他们决心称该对象为文章。后来,任何时间有人说“故事”,就会有人说:“你的意思不是文章吗?”在设计统一语言时,持续、公共的改进过程是种很强大的力量。

我们的编辑最初设计生成的模型是这样的:

[网页及其版块之间的关系由应用于文章的关键字导出,文章是网页中的核心内容。网页的版块由应用于文章的首个关键字的版块确定。]

由于并非所有的团队成员都参与了该过程的所有阶段,所以我们需要向他们介绍工作进展,并将这些全部展现在了墙上。然后开发人员开始了敏捷开发之旅,而且由于编辑和开发人员、BA、QA都在一起工作,任何有关模型及其意图的问题都能在开发过程的任何时候获得第一手的可靠信息。

经过几次迭代之后系统初具形态,我们还建立工具来创建和管理关键字、文章和页面。随着这些内容的创建,编辑们很快掌握了它们,并且给出了一些修改建议。大家普遍认为这一简单的关键模型能正常运行,而且可以继续下去、形成网站初次发布的基础。

3. 增量计划中的DDD过程

首次发布之后,我们的项目团队伴随着技术人员和业务代表们的成长,一起取得了进步,打算演进领域模型。很显然,我们需要一种有组织的方式来为领域模型引入新的内容、进行系统的演进。

3.1. 新员工入门

DDD是入门过程中的核心部分。非技术人员在整个项目生命周期内都会加入项目,因为工作计划是横跨各个编辑领域的,反过来,在恰当的时机我们也会引入版面编辑。技术人员很容易就能加入项目,因为我们持续不断地雇用新员工。

我们的入门过程包括针对这两类人的DDD讲习,尽管细节不同,但高层次的议题涵盖两个相同的部分:DDD是什么,它为什么重要;领域模型本身的特定范围。

描述DDD时我们强调的最重要的内容有:

  • 领域模型归业务代表所有。这就要从业务代表的头脑里抽象概念,并将这些概念嵌入到软件中,而不能从软件的角度思考,并试图影响业务代表。
  • 技术团队是关键的利益相关者。我们将围绕具体细节据理力争。

覆盖领域模型的特定范围本身就很重要,因为它予以就任者处理项目中特定问题的真正工具。我们的领域模型中有几十个对象,所以我们只关注于高级别和更为明显的少数几个,在我们的情况中就是本文所讨论的各种内容和关键字概念。在这里我们做了三件事:

  • 我们在白板上画出了概念及其关系,因此我们能给出系统如何工作的一个有形的表示。
  • 我们确保每位编辑当场解释大量的领域模型,以强调领域模型不属于技术团队所有这一事实。
  • 我们解释一些为了达到这一点而做出的历史变迁,所以就任者可以理解(a)这不是一成不变的,而是多变的,(b)为了进一步开发模型,他们可以在即将进行的对话中扮演什么样的角色。

3.2. 规划中的DDD

入门是必不可少的,不过在开始计划每次迭代的时候,知识才真正得以实践。

3.2.1 使用统一语言

DDD强制使用的统一语言使得业务人员、技术人员和设计师能围坐在一起规划并确定具体任务的优先次序。这意味着有很多会议与业务人员有关,他们更接近技术人员,也更加了解技术过程。有一位同事,她先担任项目的编辑助理,然后成长为关键的决策者;她解释说,在迭代启动会议上她亲自去看技术人员怎样判断和(激烈地)评估任务,开始更多地意识到功能和努力之间的平衡。如果她不和技术团队共用一种语言,她就不会一直出席会议,也不会收获那些认识。

在规划阶段利用DDD时我们使用的两个重要原则是:

  1. 领域模型归属于业务;
  2. 领域模型需要一个权威的业务源。

领域模型的业务所有权在入门中就进行了解释,但在这里才发挥作用。这意味着技术团队的关键角色是聆听并理解,而不是解释什么可能、什么不可能。需求抽象要求将概念性的领域模型映射到具体的功能需求上,并在存在不匹配的地方对业务代表提出异议或进行询问。接着,存在不匹配的地方要么改变领域模型,要么在更高层次上解决功能需求(“你想用此功能达成什么效果?”)。

对领域模型来说,权威的业务源是我们组织的性质所明确需要的。我们正在构建一个独立的软件平台,它需要满足很多编辑团队的需求,编辑团队不一定以同样的方式来看世界。Guardian不实行许多公司实行的“命令和控制”结构;编辑台则有很多自由,也能以他们认为合适的方式去开发自己的网站版面,并设定预期的读者。因此,不同的编辑对领域模型会有略微不同的理解和观点,这有可能会破坏单一的统一语言。我们的解决办法是确定并加入业务代表,他们在整个编辑台都有责任。对我们来说,这是生产团队,是那些处理日常构建版面、指定布局等技术细节的人。他们是文字编辑依赖的超级用户,作为专家工具建议,因此技术团队认为他们是领域模型的持有者,而且他们保证软件中大部分的一致性。他们当然不是唯一的业务代表,但他们是与技术人员保持一致的人员。

3.2.2 与DDD一起计划的问题

不幸的是,我们发现了在计划过程中应用DDD特有的挑战,尤其是在持续计划的敏捷环境中。这些问题是:

  1. 本质上讲,我们正在将软件写入新的、不确定商业模式中;
  2. 绑定到一个旧模型;
  3. 业务人员“入乡随俗”。

我们反过来讨论下这些问题……

EricEvans撰写关于创建领域模型的文章时,观点是业务代表的脑海中存在着一个模型,该模型需要提取出来;即便他们的模型不明确,他们也明白核心概念,而且这些概念基本上能解释给技术人员。然而在我们的情况中,我们正在改变我们的模型——事实上是在改变我们的业务——但并不知道我们目标的确切细节。(马上我们就会看到这一点的具体例子。)某些想法显而易见,也很早就建立起来了(比如我们会有文章和关键字),但很多并不是这样(引入页面的想法还有一些阻力;关键字如何关联到其它内容则完全是各有各的想法)。我们的教科书并没有提供解决这些问题的指南。不过,敏捷开发原则则可以:

  • 构建最简单的东西。尽管我们无法在早期解决所有的细节,但通常能对构建下一个有用的功能有足够的理解。
  • 频繁发布。通过发布此功能,我们能看到功能如何实际地运转。进一步的调整和进化步骤因此变得最为明显(不可避免,它们往往不是我们所预期的)。
  • 降低变化的成本。利用这些不可避免的调整和进化步骤,减少变化的成本很有必要。对我们来说这包括自动化构建过程和自动化测试等。
  • 经常重构。经过几个演进步骤,我们会看到技术债务累积,这需要予以解决。

与此相关的是第二个问题:与旧模型有太多的精神联系。比如说,我们的遗留系统要求编辑和制作人员单独安排页面布局,而新系统的愿景则是基于关键字自动生成页面。在新系统里,Guantánamo Bay页面无需任何人工干预,许多内容会给出GuantánamoBay关键字,仅简单地凭借这一事实就能显示出来。但结果却是,这只是由技术团队持有的过度机械化的愿景,技术团队希望减少体力劳动和所有页面的持续管理。相比之下,编辑人员高度重视人的洞察力,他们带入过程的不仅有记述新闻,还有展现新闻;对他们来说,为了突出最重要的故事(而不仅仅是最新的),为了用不同的方法和灵敏性(比如9·11和Web 2.0报导)区别对待不同的主题,个人的布局是很有必要的。

对这类问题没有放之四海而皆准的解决办法,但我们发现了两个成功的关键:专注于业务问题,而不是技术问题;铭记“创造性冲突”这句话。在这里的例子里,见解有分歧,但双方表达了他们在商业上的动机后,我们就开始在同一个环境里面工作了。该解决方案是创造性的,源于对每个人动机的理解,也因此解决了大家了疑虑。在这种情况下,我们构建了大量的模板,每个都有不同的感觉和影响等,编辑可以从中选择并切换。此外,每个模板的关键区域允许手动选择显示的故事、页面的其余部分自动生成内容(小心不要重复内容),对于该手动区域,如果管理变得繁重,就可以随时关闭,从而使网页完全自动化。

我们发现的第三个挑战是随着业务人员“入乡随俗”,也就是说他们已深深融入技术且牵涉到了要点,以至于他们会忘记对新使用系统的内部用户来说,系统该是什么样。当业务代表发现跟他们的同事很难沟通事情如何运转,或者很难指出价值有限的功能时,就有危险的信号了。KentBeck在《解析极限编程》第一版中说,现场客户与技术团队直接交互绝不会占用他们很多的时间,通过强调这一点,就可以保证在现场的客户。但我们在与有几十个开发人员、多名BA、多名QA的团队一起工作时,我们发现即使有三个全职的业务代表,有时也是不够的。由于业务人员花费了太多的时间与技术人员在一起,以至他们与同事失去联系成为真正的问题。这些都是人力解决方案带来的人力问题。解决方案是要提供个人备份和支持,让新的业务人员轮流加入团队(可能从助理开始着手进行,逐步成长为关键决策角色),允许代表有时间回归他们自己的核心工作,比如一天、一周等。事实上,这还有一个附加的好处,就是能让更多的业务代表接触到软件开发,还可以传播技巧和经验。

4. 进化的领域模型

在本章中,我们看看模型在方案的后期是如何演进的。

4.1. 演进第一步:超越文章

首次发布后不久,编辑要求系统能处理更多的内容类型,而非只有文章一类。尽管这对我们来说毫不稀奇,但在我们构建模型的第一个版本时,我们还是明确决定对此不予考虑太多。

这是个关键点:我们关注整个团队能很好地理解小规模、可管理块中的模型和建模过程,而不是试图预先对整个系统进行架构设计。随着理解的深入或变化,稍后再改变模型并不是个错误。这种做法符合YAGNI编码原则(你不会需要它),因为该做法防止开发人员引入额外的复杂度,因而也能阻止Bug的引入。它还能让整个团队安排时间去对系统中很小的一块达成共识。我们认为,今天产出一个可工作的无Bug系统要比明天产出一个完美的、包括所有模型的系统更为重要。

我们的编辑在下一个迭代中要求的内容类型有音频和视频。我们的技术团队和编辑再次坐在一起讨论了领域建模过程。编辑先跟技术团队谈道,音频和视频很显然与文章相似:应该可以将视频或音频放在一个页面上。每个页面只允许有一种内容。视频和音频可以通过关键字分类。关键字可以属于版面。编辑还指明,在以后的迭代中他们会添加更多类型的内容,他们认为现在该是时候去理解我们应该如何随着时间的推移去演进内容模型。

对我们的开发人员来说,很显然编辑想在语言中明确引入两个新条目:音频和视频。音频、视频和文章有一些共同点:它们都是内容类型,这一点也很明确。我们的编辑并不熟悉继承的概念,所以技术团队可以给编辑讲解继承,以便技术团队能正确表述编辑所看到的模型。

这里有一个明显的经验:通过利用敏捷开发技术将软件开发过程细分为小的块,我们还能使业务人员的学习曲线变得平滑。久而久之他们能加深对领域建模过程的理解,而不用预先花费大量的时间去学习面向对象设计所有的组件。

这是我们的编辑根据添加的新内容类型设计的模型。

这个单一的模型演变是大量更细微的通用语言演进的结果。现在我们有三个外加的词:音频、视频和内容;我们的编辑已经了解了继承,并能在以后的模型迭代中加以利用;对添加新的内容类型,我们也有了以后的扩展策略,并使其对我们的编辑来说是简单的。如果编辑需要一个新的内容类型,而这一新的内容类型与我们已有的内容类型相同、在页面和关键字之间有大致相同的关系,那编辑就能要求开发团队产出一个新的内容类型。作为一个团队,我们正通过逐步生成模型来提高效率,因为我们的编辑不会再详细检查漫长的领域建模过程去添加新的内容类型。

4.2. 演进第二步:

由于我们的模型要扩展到包括更多的内容类型,它需要更灵活地去分类。我们开始在领域模型中添加额外的元数据,但编辑的最终意图是什么还不是非常清楚。然而这并不让我们太过担忧,因为我们对元数据进行建模的方法与处理内容的方法一样,将需求细分为可管理的块,将每个添加到我们的领域中。

我们的编辑想添加的第一个元数据类型是系列这一概念。系列是一组相关的内容,内容则有一个基于时间的隐含顺序。在报纸中有很多系列的例子,也需要将这一概念解释为适用于Web的说法。

我们对此的初步想法非常简单。我们将系列添加为一个领域对象,它要关联到内容和页面。这个对象将用来聚集与系列关联的内容。如果读者访问了一种内容,该内容属于某个系列,我们就能从页面链接到同一系列中的前一条和后一条内容。我们还能链接到并生成系列索引页面,该页面可以显示系列中的所有内容。

这里是编辑所设计的系列模型:

与此同时,我们的编辑正在考虑更多的元数据,他们想让这些元数据与内容关联。目前关键字描述了内容是关于什么的。编辑还要求系统能根据内容的基调对内容进行不同的处理。不同基调的例子有评论、讣告、读者供稿、来信。通过引入基调,我们就可以将其显示给读者,让他们找到类似的内容(其它讣告、评论等)。这像是除关键字或系列外另一种类型的关系。跟系列一样,基调可以附加到一条内容上,也能和页面有关系。

这里是编辑针对基调设计的模型:

完成开发后,我们有了一个能根据关键字、系列或基调对内容进行分类的系统。但编辑对达到这一点所需的技术工作量还有一些关注点。他们在我们下次演进模型时向技术团队提出了这些关注点,并能提出解决方案。

4.3. 演进第三步:重构元数据

模型的下一步演进是我们的编辑想接着添加类似于系列和基调的内容。我们的编辑想添加带有贡献者的内容这一概念。贡献者是创建内容的人,可能是文章的作者,或者是视频的制作人。跟系列一样,贡献者在系统中有一个页面,该页面会自动聚集贡献者制作的所有内容。

编辑还看到了另一个问题。他们认为随着系列和基调的引入,他们已经向开发人员指明了大量非常相似的细节。他们要求构建一个工具去创建系列,构建另一个工具去创建基调。他们不得不指明这些对象如何关联到内容和页面上。每次他们都发现,他们在为这两种类型的领域对象指定非常相似的开发任务;这很浪费时间,还是重复的。编辑更加关注于贡献者,还有更多的元数据类型会加入进来。这看起来又要让编辑再次指明、处理大量昂贵的开发工作,所有这些都非常相似。

这显然成为一个问题。我们的编辑似乎已经发现了模型的一些错误,而开发人员还没有。为什么添加新的元数据对象会如此昂贵呢?为什么他们不得不一遍又一遍地去指定相同的工作呢?我们的编辑问了一个问题,该问题是“这仅仅是‘软件开发如何工作’,还是模型有问题?”技术团队认为编辑熟悉一些事情,因为很显然,他们理解模型的方式与编辑不同。我们与编辑一起召开了另一个领域建模会议,试图找出问题所在。

在会议上我们的编辑建议,所有已有的元数据类型实际上源于相同的基本思想。所有的元数据对象(关键字、系列、基调和贡献者)可以和内容有多对多的关系,而且它们都需要它们自己的页面。(在先前的模型版本中,我们不得不知道对象和页面之间的关系)。我们重构了模型,引入了一个新的超类——Tag(标签),并作为其它元数据的超类。编辑们很喜欢使用“超类”这一技术术语,将整个重构称为“Super-Tag”,尽管最终也回到了现实。

由于标签的引入,添加贡献者和其它预期的新元数据类型变得很简单,因为我们能够利用已有的工具功能和框架。

我们修订后的模型现在看起来是这样的:

我们的业务代表在以这种方式考虑开发过程和领域模型,发现这一点非常好,还发现领域驱动设计有能力促进在两个方向都起作用的共同理解:我们发现技术团队对我们正努力解决的业务问题有良好且持续的理解,而且出乎意料,业务代表能“洞察”开发过程,还能改变这一过程以更好地满足他们的需求。编辑们现在不仅能将他们的需求翻译为领域模型,还能设计、检查领域模型的重构,以确保重构能与我们目前对业务问题的理解保持同步。

编辑规划领域模型重构并成功执行它们的能力是我们领域驱动设计guardian.co.uk成功的一个关键点。

5. 代码级别的演进

前面我们看了领域模型方面的进化。但DDD在代码级别也有影响,不断变化的业务需求也意味着代码要有变化。现在我们来看看这些变化。

5.1. 构建模型

在构建领域模型时,要确认的第一件事就是领域中出现的聚集。聚集可认为是相关对象的集合,这些对象彼此相互引用。这些对象不应该直接引用其它聚集中的其它对象;不同聚集之间的引用应该由根聚集来完成。

看一下我们在上面定义的模型示例,我们开始看到对象成形。我们有Page和Template对象,它们结合起来能给Web页面提供URL和观感。由于Page是系统的入口点,所以在这里Page就是根聚集。

我们还有一个聚集Content,它也是根聚集。我们看到Content有Article、Video、Audio等子类型,我们认为这些都是内容的子聚集,核心的Content对象则是根聚集。

我们还看到形成了另一个聚集。它是元数据对象的集合:Tag、Series、Tone等。这些对象组成了标签聚集,Tag是根聚集。

Java编程语言提供了理想的方式来对这些聚集进行建模。我们可以使用Java包来对每个聚集进行建模,使用标准的POJO对每个领域对象进行建模。那些不是根聚集、且只在聚集中使用的领域对象可以有包范围内使用的构造函数,以防它们在聚集外被构造。

上述模型的包结构如下所示(“r2”是我们应用套件的名称):

 com.gu.r2.model.page 

 com.gu.r2.model.tag 

 com.gu.r2.model.content 

 com.gu.r2.model.content.article

 com.gu.r2.model.content.video

 com.gu.r2.model.content.audio 

我们将内容聚集细分为多个子包,因为内容对象往往有很多聚集特定的支持类(这里的简化图中没有显示)。所有以标签为基础的对象往往要更为简单,所以我们将它们放在了一个包里,而没有引入额外的复杂性。

不过不久之后,我们认识到上述包结构会给我们带来问题,我们打算修改它。看看我们前端应用的包结构示例,了解一下我们如何组织控制器,就能阐述清楚这一问题:

 com.gu.r2.frontend.controller.page

 com.gu.r2.frontend.controller.articl

这里看到我们的代码集要开始细分为片段。我们提取了所有的聚集,将其放入包中,但我们没有单独的包去包含与聚集相关的所有对象。这意味着,如果以后领域变得太大而不能作为一个单独的单元来管理,我们希望将应用分解,处理依赖就会有困难。目前这还没有真正带来什么问题,但我们要重构应用,以便不会有太多的跨包依赖。经过改进的结构如下:

 com.gu.r2.page.model   (domain objects in the page aggregate)

 com.gu.r2.page.controller (controllers providing access to aggregate)

 com.gu.r2.content.article.model

 com.gu.r2.content.article.controller

 ...

 etc

除了约定,我们在代码集中没有其它任何的领域驱动设计实施原则。创建注解或标记接口来标记聚集根是有可能的,实际上是争取在模型包锁定开发,减少开发人员建模时出错的几率。但实际上并不是用这些机械的强制来保证在整个代码集中都遵循标准约定,而是我们更多地依赖了人力技术,比如结对编程和测试驱动开发。如果我们确实发现已创建的一些内容违反了我们的设计原则(这相当少见),那我们会告诉开发人员并让他完善设计。我们还是喜欢这个轻量级的方法,因为它很少在代码集中引入混乱,反而提升了代码的简单性和可读性。这也意味着我们的开发人员更好地理解了为什么一些内容是按这种方式组织,而不是被迫去简单地做这些事情。

5.2. 核心DDD概念的演进

根据领域驱动设计原则创建的应用会具有四种明显的对象类型:实体、值对象、资源库和服务。在本节中,我们将看看应用中的这些例子。

5.2.1 实体

实体是那些存在于聚集中并具有标识的对象。并不是所有的实体都是聚集根,但只有实体才能成为聚集根。

开发人员,尤其是那些使用关系型数据库的开发人员,都很熟悉实体的概念。不过,我们发现这个看似很好理解的概念却有可能引起一些误解。

这一误解似乎跟使用Hibernate持久化实体有点儿关系。由于我们使用Hibernate,我们一般将实体建模为简单的POJO。每个实体具有属性,这些属性可以利用setter和getter方法进行存取。每个属性都映射到一个XML文件中,定义该属性如何持久化到数据库中。为了创建一个新的持久化实体,开发人员需要创建用于存储的数据库表,创建适当的Hibernate映射文件,还要创建有相关属性的领域对象。由于开发人员要花费一些时间处理持久化机制,他们有时似乎认为实体对象的目的仅仅只是数据的持久化,而不是业务逻辑的执行。等他们后来开始实现业务逻辑时,他们往往在服务对象中实现,而不是在实体对象本身中。

在下面(简化)的代码片段中可以看出此类错误。我们用一个简单的实体对象来表示一场足球赛:

 public class FootballMatch extends IdBasedDomainObject

 { 

     private final FootballTeam homeTeam;

     private final FootballTeam awayTeam;

     private int homeTeamGoalsScored;

     private int awayTeamGoalsScored;

 

     FootballMatch(FootballTeam homeTeam, FootballTeam awayTeam) { 

         this.homeTeam = homeTeam; 

         this.awayTeam = awayTeam; 

     } 

 

     public FootballTeam getHomeTeam() { 

         return homeTeam; 

     } 

 

     public FootballTeam getAwayTeam() { 

         return awayTeam; 

     } 

     public int getHomeTeamScore() { 

         return homeTeamScore; 

     } 

 

     public void setHomeTeamScore(int score) { 

         this.homeTeamScore = score; 

     } 

 

     public void setAwayTeamScore(int score) { 

         this.awayTeamScore = score; 

     } 

 }

该实体对象使用FootballTeam实体去对球队进行建模,看起来很像使用Hibernate的开发人员所熟悉的对象类型。该实体的每个属性都持久化到数据库中,尽管从领域驱动设计的角度来说这个细节并不真的重要,我们的开发人员还是将持久化的属性提升到一个高于它们应该在的水平上去。在我们试图从FootballTeam对象计算出谁赢得了比赛的时候这一点就可以显露出来。我们的开发人员要做的事情就是造出另一种所谓的领域对象,就像下面所示:

 public class FootballMatchSummary { 



     public FootballTeam getWinningTeam(FootballMatch footballMatch) { 

         if(footballMatch.getHomeTeamScore() >  footballMatch.getAwayTeamScore()) { 

             return footballMatch.getHomeTeam(); 

         } 

         return footballMatch.getAwayTeam(); 

     } 

 }

片刻的思考应该表明已经出现了错误。我们已经创建了一个FootballMatchSummary类,该类存在于领域模型中,但对业务来说它并不意味着什么。它看起来是充当了FootballMatch对象的服务对象,提供了实际上应该存在于FootballMatch领域对象中的功能。引起这一误解的原因好像是开发人员将FootballMatch实体对象简单地看成了是反映数据库中持久化信息,而不是解决所有的业务问题。我们的开发人员将实体考虑为了传统ORM意义上的实体,而不是业务所有和业务定义的领域对象。

不愿意在领域对象中加入业务逻辑会导致贫血的领域模型,如果不加以制止还会使混乱的服务对象激增——就像我们等会儿看到的一样。作为一个团队,现在我们来检视一下创建的服务对象,看看它们实际上是否包含业务逻辑。我们还有一个严格的规则,就是开发人员不能在模型中创建新的对象类型,这对业务来说并不意味着什么。

作为团队,我们在项目开始时还被实体对象给弄糊涂了,而且这种困惑也与持久化有关。在我们的应用中,大部分实体与内容有关,而且大部分都被持久化了。但当实体不被持久化,而是在运行时由工厂或资源库创建的话,有时候还是会混淆。

一个很好的此类例子就是“标签合成的页面”。我们在数据库中持久化了编辑创建的所有页面的外观,但我们可以自动生成从标记组合(比如USA+Economics或Technology+China)聚集内容的页面。由于所有可能的标记组合总数是个天文数字,我们不可能持久化所有的这些页面,但系统还必须能生成页面。在渲染标记组合页面时,我们必须在运行时实例化尚未持久化的新Page实例。项目初期我们倾向于认为这些非持久化对象与“真正的”持久化领域对象不同,也不像在对它们建模时那么认真。从业务观点看,这些自动生成的实体与持久化实体实际上并没有什么不同,因此从领域驱动设计观点来看也是如此。不论它们是否被持久化,对业务来说它们都有同样的定义,都不过是领域对象;没有“真正的”和“不是真正的”领域对象概念。

5.2.2 值对象

值对象是实体的属性,它们没有特性标识去指明领域中的内容,但表达了领域中有含义的概念。这些对象很重要,因为它们阐明了统一语言。

值对象阐述能力的一个例子可以从我们的Page类更详细地看出。系统中任何Page都有两个可能的URLs。一个URL是读者键入Web浏览器以访问内容的公开URL。另一个则是从应用服务器直接提供服务时内容依存的内部URL。我们的Web服务器处理从用户那里传入的URL,并将它转换为适合后端CMS服务器的内部URL。

一种简化的观点是,在Page类中两个可能的URL都被建模为字符串对象:

 public String getUrl(); 

 public String getCmsUrl(); 

不过,这并没有什么特别的表现力。除了这些方法返回字符串这一事实之外,只看这些方法的签名很难确切地知道它们会返回什么。另外想象一下这种情况,我们想基于页面的URL从一个数据访问对象中加载页面。我们可能会有如下的方法签名:

public Page loadPage(String url);

这里需要的URL是哪一个呢?是公开的那个还是CMS URL?不检查该方法的代码是不可能识别出来的。这也很难与业务人员谈论页面的URL。我们指的到底是哪一个呢?在我们的模型中没有表示每种类型URL的对象,因此在我们的词汇里面也就没有相关条目。

这里还含有更多的问题。我们对内部URL和外部URL可能有不同的验证规则,也希望对它们执行不同的操作。如果我们没有地方放置这个逻辑,那我们怎么正确地封装该逻辑呢?控制URLs的逻辑一定不属于Page,我们也不希望引入更多不必要的服务对象。

领域驱动设计建议的演进方案是明确地对这些值对象进行建模。我们应该创建表示值对象的简单包装类,以对它们进行分类。如果我们这样做,Page里面的签名就如下所示:

 public Url getUrl();

 public CmsPath getCmsPath();

现在我们可以在应用中安全地传递CmsPath或Url对象,也能用业务代表理解的语言与他们谈论代码。

5.2.3 资源库

资源库是存在于聚集中的对象,在抽象任何持久化机制时提供对聚集根对象实体的访问。这些对象由业务问题请求,与领域对象一起响应。

将资源库看成是类似于有关数据库持久化功能的数据访问对象,而非存在于领域中的业务对象,这一点很不错。但资源库是领域对象:他们响应业务请求。资源库始终与聚集关联,并返回聚集根的实例。如果我们请求一个页面对象,我们会去页面资源库。如果我们请求一个页面对象列表来响应特定的业务问题,我们也会去页面资源库。

我们发现一个很好的思考资源库的方式,就是把它们看成是数据访问对象集合之上的外观。然后它们就成为业务问题和数据传输对象的结合点,业务问题需要访问特定的聚集,而数据传输对象提供底层功能。

这里举一小段页面资源库的代码例子,我们来实际看下这个问题:

 private final PageDAO<Page> pageDAO; 

 private final PagesRelatedBySectionDAO pagesRelatedBySectionDAO; 



 public PageRepository(PageDAO<Page> pageDAO,

         EditorialPagesInThisSectionDAO pagesInThisSectionDAO, 

         PagesRelatedBySectionDAO pagesRelatedBySectionDAO) { 

     this.pageDAO = pageDAO; 

     this.pagesRelatedBySectionDAO = pagesRelatedBySectionDAO;

 } 



 public List<Page> getAudioPagesForPodcastSeriesOrderedByPublicationDate(Series series, int maxNumberOfPages) { 

     return pageDAO.getAudioPagesForPodcastSeriesOrderedByPublicationDate(series, maxNumberOfPages); 

 } 



 public List<Page> getLatestPagesForSection(Section section, int maxResults) {

     return pagesRelatedBySectionDAO.getLatestPagesForSection(section, maxResults); 

 }

我们的资源库有业务请求:获取PublicationDate请求的特定播客系列的页面。获取特定版面的最新页面。我们可以看看这里使用的业务领域语言。它不仅仅是一个数据访问对象,它本身就是一个领域对象,跟页面或文章是领域对象一样。

我们花了一段时间才明白,把资源库看成是领域对象有助于我们克服实现领域模型的技术问题。我们可以在模型中看到,标签和内容是一种双向的多对多关系。我们使用Hibernate作为ORM工具,所以我们对其进行了映射,Tag有如下方法:

public List<Content> getContent();

Content有如下方法:

 public List<Tag>  getTags(); 

尽管这一实现跟我们的编辑看到的一样,是模型的正确表达,但我们有了自己的问题。对开发人员来说,代码可能会编写成下面这样:

 if(someTag.getContent().size() == 0){

     ... do some stuff

 }

这里的问题是,如果标签关联有大量的内容(比如“新闻”),我们最终可能会往内存中加载几十万的内容条目,而只是为了看看标记是否包含内容。这显然会引起巨大的网站性能和稳定性问题。

随着我们演进模型、理解了领域驱动设计,我们意识到有时候我们必须要注重实效:模型的某些遍历可能是危险的,应该予以避免。在这种情况下,我们使用资源库来用安全的方式解决问题,会为系统的性能和稳定性牺牲模型个别的清晰性和纯洁性。

5.2.4. 服务

服务是通过编排领域对象交互来处理业务问题执行的对象。我们所理解的服务是随着我们过程演进而演进最多的东西。

首要问题是,对开发人员来说创建不应该存在的服务相当容易;他们要么在服务中包含了本应存在于领域对象中的领域逻辑,要么扮演了缺失的领域对象角色,而这些领域对象并没有作为模型的一部分去创建。

项目初期我们开始发现服务开始突然涌现,带着类似于ArticleService的名字。这是什么呀?我们有一个领域对象叫Article;那文章服务的目的是什么?检查代码时,我们发现该类似乎步了前面讨论的FootballMatchSummary的后尘,有类似的模式,包含了本该属于核心领域对象的领域逻辑。

为了对付这一行为,我们对应用中的所有服务进行了代码评审,并进行重构,将逻辑移到适当的领域对象中。我们还制定了一个新的规则:任何服务对象在其名称中必须包含一个动词。这一简单的规则阻止了开发人员去创建类似于ArticleService的类。取而代之,我们创建ArticlePublishingService和ArticleDeletionService这样的类。推动这一简单的命名规范的确帮助我们将领域逻辑移到了正确的地方,但我们仍要求对服务进行定期的代码评审,以确保我们在正轨上,以及对领域的建模接近于实际的业务观点。

6. 演进架构中DDD的一些教训

尽管面临挑战,但我们发现了在不断演进和敏捷的环境中利用DDD的显著优势,此外我们还总结了一些经验教训:

  • 你不必理解整个领域来增加商业价值。你甚至不需要全面的领域驱动设计知识。团队的所有成员差不多都能在他们需要的任何时间内对模型达成一个共同的理解。
  • 随着时间的推移,演进模型和过程是可能的,随着共同理解的提高,纠正以前的错误也是可能的。

我们系统的完整领域模型要比这里描述的简化版本大很多,而且随着我们业务的扩展在不断变化。在一个大型网站的动态世界里,创新永远发生着;我们始终要保持领先地位并有新的突破,对我们来说,有时很难在第一次就得到正确的模型。事实上,我们的业务代表往往想尝试新的想法和方法。有些人会取得成果,其他人则会失败。是逐步扩展现有领域模型——甚至在现有领域模型不再满足需求时进行重构——的业务能力为guardian.co.uk开发过程中遇到的大部分创新提供了基础。

7. 附录:具体示例

为了了解我们的领域模型如何生成真实的结果,这里给出了一个例子,先看单独的内容……

8. 关于作者

Nik Silver是Guardian News & Media软件开发总监。他于2003年在公司引入敏捷软件开发,负责软件开发、前端开发和质量保证。Nik偶尔会在blogs.guardian.co.uk/inside上写Guardian技术工作相关的内容,并在他自己的站点niksilver.com上写更宽泛的软件问题。

Matthew Wall是Guardian News & Media的软件架构师,深入研究敏捷环境下大型Web应用的开发。他目前最关心的是为guardian.co.uk开发下一代的Web平台。他在JAOO、ServerSide、QCon、XTech和OpenTech上做过关于此及相关主题的各种演讲。


Web架构设计经验分享

作者:朱燚
来源:http://www.cnblogs.com/yizhu2000/archive/2007/12/04/982142.html

 

本人作为一位web工程师,着眼最多之处莫过于 性能与架构,本次幸得参与sd2.0大会,得以与同行广泛交流,于此二方面,有些心得,不敢独享,与众博友分享,本文是这次参会与众同撩交流的心得,有兴趣者可以查看视频

架构设计的几个心得:


一,不要过设计:never over design

这是一个常常被提及的话题,但是只要想想你的架构里有多少功能是根本没有用到,或者最后废弃的,就能明白其重要性了,初涉架构设计,往往倾向于设计大而化一的架构,希望设计出具有无比扩展性,能适应一切需求的增加架构,web开发领域是个非常动态的过程,我们很难预测下个星期的变化,而又需要对变化做出最快最有效的响应。。

ebay的工程师说过,他们的架构设计从来都不能满足系统的增长,所以他们的系统永远都在推翻重做。请注意,不是ebay架构师的能力有问题,他们设计的架构总是建立旧版本的瓶颈上,希望通过新的架构带来突破,然而新架构带来的突破总是在很短的时间内就被新增需求淹没,于是他们不得不又使用新的架构
web开发,是个非常敏捷的过程,变化随时都在产生,用户需求千变万化,许多方面偶然性非常高,较之软件开发,希望用一个架构规划以后的所有设计,是不现实的

二,web架构生命周期:web architecture‘s life cycle


既然要杜绝过设计,又要保证一定的前瞻性,那么怎么才能找到其中的平衡呢?希望下面的web架构生命周期能够帮到你

architecture_life_cycle

所设计的架构需要在1-10倍的增长下,通过简单的增加硬件容量就能够胜任,而在5-10倍的增长期间,请着手下一个版本的架构设计,使之能承受下一个10倍间的增长

google之所以能够称霸,不完全是因为搜索技术和排序技术有多先进,其实包括baidu和yahoo,所使用的技术现在也已经大同小异,然而,google能在一个月内通过增加上万台服务器来达到足够系统容量的能力确是很难被复制的


三,缓存:Cache


空间换取时间,缓存永远计算机设计的重中之重,从cpu到io,到处都可以看到缓存的身影,web架构设计重,缓存设计必不可少,关于怎样设计合理的缓存,jbosscache的创始人,淘宝的创始人是这样说的:其实设计web缓存和企业级缓存是非常不同的,企业级缓存偏重于逻辑,而web缓存,简单快速为好。。

缓存带来的问题是什么?是程序的复杂度上升,因为数据散布在多个进程,所以同步就是一个麻烦的问题,加上集群,复杂度会进一步提高,在实际运用中,采用怎样的同步策略常常需要和业务绑定

老钱为搜狐设计的帖子设计了链表缓存,这样既可以满足灵活插入的需要,又能够快速阅读,而其他一些大型社区也经常采用类此的结构来优化帖子列表,memcache也是一个常常用到的工具

钱宏武谈架构设计视频 http://211.100.26.82/CSDN_Live/140/qhw.flv

Cache的常用的策略是:让数据在内存中,而不是在比较耗时的磁盘上。从这个角度讲,mysql提供的heap引擎(存储方式)也是一个值得思考的方法,这种存储方法可以把数据存储在内存中,并且保留sql强大的查询能力,是不是一举两得呢?

我们这里只说到了读缓存,其实还有一种写缓存,在以内容为主的社区里比较少用到,因为这样的社区最主要需要解决的问题是读问题,但是在处理能力低于请求能力时,或者单个希望请求先被缓存形成块,然后批量处理时,写缓存就出现了,在交互性很强的社区设计里我们很容易找到这样的缓存

四,核心模块一定要自己开发:DIY your core module


这点我们是深有体会,钱宏武和云风也都有谈到,我们经常倾向于使用一些开源模块,如果不涉及核心模块,确实是可以的,如果涉及,那么就要小心了,因为当访问量达到一定的程度,这些模块往往都有这样那样的问题,当然我们可以把问题归结为对开源的模块不熟悉,但是不管怎样,核心出现问题的时候,不能完全掌握其代码是非常可怕的


五,合理选择数据存储方式:reasonable data storage


我们一定要使用数据库吗,不一定,雷鸣告诉我们搜索不一定需要数据库,云风告诉我们,游戏不一定需要数据库,那么什么时候我们才需要数据库呢,为什么不干脆用文件来代替他呢?
首先我们需要先承认,数据库也是对文件进行操作。我们需要数据库,主要是使用下面这几个功能,一个是数据存储,一个是数据检索,在关系数据库中,我们其实非常在乎数据库的复杂搜索的能力,看看一个统计用的tsql就知道了(不用仔细读,扫一眼就可以了)

select   c.Class_name,d.Class_name_2,a.Creativity_Title,b.User_name,(select   count(Id)   from   review   where   Reviewid=a.Id)   as   countNum   from   Creativity   as   a,User_info   as   b,class   as   c,class2   as   d   where   a.user_id=b.id   and   a.Creativity_Class=c.Id   and   a.Creativity_Class_2=d.Id
select   a.Id,max(c.Class_name),(max(d.Class_name_2),max(a.Creativity_Title),max(b.User_name),count(e.Id)   as   countNum   from   Creativity   as   a,User_info   as   b,class   as   c,class2   as   d,review   as   e   where   a.user_id=b.id   and   a.Creativity_Class=c.Id   and   a.Creativity_Class_2=d.Id   and   a.Id=e.Reviewid   group   by   a.Id ..............................................

我们可以看出需要数据库关联,排序的能力,这个能力在某些情况下非常重要,但是如果你的网站的常规操作,全是这样复杂的逻辑,那效率一定是非常低的,所以我们常常在数据库里加入许多冗余字段,来减小简单查询时关联等操作带来的压力,我们看看下面这张图,可以看到数据库的设计重心,和网站(指内容型社区)需要面对的问题实际是有一些偏差的

database

同样其他一些软件产品也遇到同样的问题所以具我了解,有许多特殊的运用都有自己设计的特殊数据存储结构与方法,比如有的大型服务程序采取树形数据存储结构,lucene使用文件来存储索引和文件

从另外一个角度上看,使用数据库,意味着数据和表现是完全分离的(这当然是经典的设计思路),也就是说当需要展示数据时,不得不需要一个转换的过程,也可以说是绑定的过程,当网站具备一定规模的时候,数据库往往成为效率的瓶颈,所以许多网站也采用直接书写静态文件的方法来避免读取操作时的绑定

这并不是说我们从今天起就可以把我们亲爱的数据库打入冷宫,而是我们在设计数据的持久化时,需要根据实际情况来选择存储方式,而数据库不过是其中一个选项


六,搞清楚谁是最重要的人:who's the most important guy


在用例需求分析的时候常常讲到涉众,就是和你的设计息息相关的人,在web中我们一定以为最重要的涉众莫过于用户了。,在一个传统的互动社区开发中,最重要的东西是内容,用户产生内容,所以用户就是上帝,至于内容挑选工具,不就是给坐我后面三排的妹妹们用的吗?凑或行了,实在有问题我就在数据里手动帮你加得了。。这大概是眼下许多小型甚至中型网站技术人员的普遍想法。钱宏武在他的讲座里谈到了这个问题:实际上网站每天产生的内容非常的多,普通人是不可能看完的,而编辑负责把精华的内容推荐到首页上,所以很多用户读到的内容其实都依赖于编辑的推荐,所以设计让编辑工作方便的工具也是非常重要,有时甚至是最重要的。


七,不要执着于文档:don't be crazy about document


web开发的文档重要吗?什么文档最重要?我的看法是web开发中交流>文档,

现在大的软件公司比较流行的做法是:
注重产品设计文档,在这种方法里,产品文档非常详尽,并且没有歧义,开发人员基于设计文档开发,测试人员基于设计文档制定测试方案,任何新人都可以通过阅读产品设计文档来了解项目的概况

而web项目从概念到实现的时间是非常短的,而且越短越好,并且由于变化迅速,要想写出完整的产品和需求文档是几乎不可能的,大多数情况是等你写出完备的文档,项目早就是另外一个样子,但是没有文档的问题是,如果团队发生变化,添加新成员怎样才能了解软件的结构和概念呢,一种是每个人都了解软件的整个结构,除非你的团队整体消失,否则任何一个人都能够担当培养新人的责任,这种face2face交流比文档有效率很多。

于是就有了前office开发者,现任yahoo中国某产品开发负责人的刘振飞所感觉到的落差,他说,我们的项目是吵出来的,我听完会心一笑


八,团队:team


不要专家团队,而要外科手术式的团队,你的团队里一定要有清道夫,需要有弓箭手,让他们和项目一起成长,才是项目负责人的最大成就

总结:

0)架构是一种权衡

architecture

1)web开发的特点是是:没有太复杂的技术难点,一切在于迅速的把握需求,其实这正式敏捷开发的要旨所在,一切都可以非常快速的建立,非常快速的重构,我们的开发工具,底层库和框架,包括搜索引擎和web文档提供的帮助,都提我们供给了敏捷的能力。

2)此外,相应的,最有效率的交流方式必须留给web开发,那就是face2face(面对面),不要太担心你的设计不能被完备的文档所保留下来,他们会以交流,代码和小卡片的方式保存下来

3)人的因素会更加重要,无论是对用户的需求,还是开发人员的素质。

另:有关web效率,有著名的14条规则,由yahoo性能效率小组所总结,并广为流传。业已出现相关插件(YSlow),针对具体网页按彼规则评分,这次该小组负责人Tenni Theurer也受邀来到此次大会,我把Tenni小姐(之前真的没有想到她是个女孩,并且如此年轻)和她的团队的14 rules列在下面

软件架构设计

事实上,经过从上面三个方面审视架构,我们已经建立了一个完整的而且比较良好的架
构。但我们还需要从第四个方面在更高的层次审视我们的架构,需要考虑的又一个问题就是
软件的复用。复用可以大大降低后期成本,提高整个软件系统的可升级性与可维护性。我们
可以考虑哪些结构可以使用已经存在的可复用结构和产品,某些结构可以利用 GoF 的设计模
式设计可复用的构件已备后期使用。还需要根据需求分析得出的易变点仔细设计产品结构,
确保后期的变化不至于对产品带来太大的影响。而复用的一个重要的手段,就是面向构件的
方法。
1,软件复用
软件复用是指重复使用“为了复用目的而设计的软件”的过程。在过去的开发实践中,
我们也可能会重复使用“并非为了复用目的而设计的软件”的过程,或者在一个应用系统的
不同版本间重复使用代码的过程,这两类行为都不属于严格意义上的软件复用。
通过软件复用,在应用系统开发中可以充分地利用已有的开发成果,消除了包括分析、
设计、编码、测试等在内的许多重复劳动,从而提高了软件开发的效率,同时,通过复用高
质量的已有开发成果,避免了重新开发可能引入的错误,从而提高了软件的质量。在基于复
用的软件开发中,为复用而开发的软件架构本身就可以作为一种大粒度的、抽象级别较高的
软件构件进行复用,而且软件架构还为构件的组装提供了基础和上下文,对于成功的复用具
有非常重要的意义。
软件架构研究如何快速、可靠地用可复用构件构造系统的方式,着眼于软件系统自身的
整体结构和构件间的互联。其中主要包括:软件架构原理和风格,软件架构的描述和规范,
特定领域软件架构,构件如何向软件构架的集成机制等。
2,面向构件的方法简述
构件也称为组件,面向构件的方法包含了许多关键理论,这些关键理论解决了当今许多
备受挑剔的软件问题,这些理论包括:
构件基础设施
软件模式
软件架构
基于构件的软件开发
构件可以理解为面向对象软件技术的一种变体,它有四点原则区别于其它思想,封装、
多态、后期绑定、安全。从这个角度来说,它和面向对象是类似的。不过它取消了对于继承
的强调。
在面向构件的思想里,认为继承是个紧耦合的、白盒的关系,它对大多数打包和复用来
说都是不合适的。构件通过直接调用其它对象或构件来实现功能的复用,而不是使用继承来
实现它,事实上,在我们后面的讨论中,也会提到面向对象的方法中还是要优先使用组合而
不是继承,但在构件方法中则完全摒弃了继承而是调用,在构件术语里,这些调用称作“代
理”(delegation)。
实现构件技术关键是需要一个规范,这个规范应该定义封装标准,或者说是构件设计的
公共结构。理想状态这个规范应该是在行业以至全球范围内的标准,这样构建就可以在系统、
企业乃至整个软件行业中被广泛复用。构件利用组装来创建系统,在组装的过程中,可以把
多个构件结合在一起创建一个比较大的实体,如果构件之间能够匹配用户的请求和服务的规
范,它们就能进行交互而不需要额外的代码。
3,面向构件的软件模式
面向构件技术的特色在于:迅速、灵活、简洁,面向构件技术之于软件业的意义正如由
生产流水线之于工业制造,是软件业发展的必然趋势。软件业发展到今天,已经不是那种个
人花费一段时间即可完成的小软件。软件越来越复杂,时间越来越短,软件代码也从几百行
到现在的上百万行。把这些代码分解成一些构件完成,可以减少软件系统中的变化因子。
1)面向构件方法模式
面向构件技术的思想基础在软件复用,技术基础是根据软件复用思想设计的众多构件。
面向构件将软件系统开发的重心移向如何把应用系统分解成稳定、灵活、可重用的构件和如
何利用已有构件库组装出随需而变的应用软件。
基于面向构件的架构可以描述为:系统=框架+构件+组建。框架是所有构件的支撑框架;
每个构件实现系统的每个具体功能;组建,可以视为构件的插入顺序,不同构件的组成顺序不
同,其实现的整体功能也就不同。
面向构件技术将把软件开发分成几种:框架开发设计、构件开发设计、组装,如果用现
代的工业生产做比喻,框架设计就是基本的生产机器的开发研究,构件开发就是零件的生产,
组装就是把零件组装成汽车、飞机等等各种产品。
2)面向构件开发的不足之处
(1)系统资源耗费
从软件性能角度看,用面向构件技术开发的软件并不是最佳的。除了有比较大的代码冗
余外,因为它的灵活性在很大程度上是以空间和时间等为代价实现的。
(2)面向构件开发的风险
从细节来看,构件将构件的实现细节完全封装,如果没有好的文档支持,有可能导致构
件的使用结果不是使用者预期的。比如,构件使用者对某构件的出错机制认识不够
3)开放式系统技术
专用软件是由单个供应商生产的不符合统一标准的产品。这些单个供应商通过版本更换
来控制软件的形式与功能。但是谁这系统越来越复杂,当一个系统建立起来以后,往往更倾
向于依赖于通用的商业软件,这种依赖往往成为内部软件复用的非常有效的形式。正是这种
状态,我们需要讨论一下开放式系统技术这个问题。
商业软件成为复用的有效形式,主要原因是规模经济的作用,通用的商业软件的质量,
往往超过终端用户自主开发能力。
商业软件也可以在某个领域内实现专有,也就是提供应用程序接口(API)为应用软件
提供服务。当软件不断升级的过程中,这些接口的复杂性可能超出用户的需要,这就需要有
复杂性的控制。
一个解决方案就是实现开放式系统技术,开放式系统技术与专有技术有根本的不同。在
开放式系统技术中,由供应商组织来开发独立于专有实现的规范,所有的供应商实现的接口
都严格的一致,并且规定了统一的术语,这样就可以是软件的生命周期得以延长,这种开放
式系统技术特别适合于面向对象的方法。
为了使商业技术能适应各种应用需求,对软件开发和安装就有一定的要求,这种要求称
之为配置(profiling),适当的配置软件的嵌入也称为开放是软件的一个特点。
从架构的角度来看,不少系统结构具有大量的一对一接口和复杂的相互连接关系,这种
模型被称之为“烟囱”模型,当系统规模增大以后,这种关系会以平方律的速度增加,复杂
性的增加会带来相当多的问题,尤其是升级和修改越来越难以进行,而系统的可扩展性恰恰
是开发成本的重要部分。
为此,我们可以建立一个称之为对象管理器的层,用于统一协调各个对象的沟通,从可
维护性角度这是一个比较好的结构,但是在某些特别强调效率的点可以避开它。系统的架构
的一个重要原则就是对软件接口定义一个已经规划的定义,为系统集成方案提供更高的统一
性,软件的子系统部分要由应用程序接口来定义。这样,就削弱了模块之间的依赖性,这样
的系统就比较容易扩展和维护,并且支持大规模的集成。
经过从四个方面审视我们的架构,经过分析、权衡、设计和部分编码,我们就可以得到
一个稳固的架构。这个架构经过经过评审,就可以作为后期开发的基础架构.

综上所述,我们就可以比较条理化的建立软件架构设计的流程了。典型软件架构设计的
流程如下图所示。
一、业务架构概念
在构建软件架构之前,架构师需要仔细研究如下几个问题:
系统是为什么目的而构建的?
系统投运后服务于哪些利益相关者的利益?
什么角色在什么时候操作或者维护系统?
业务系统实现方法是怎样的?
整个业务系统是如何依靠系统而运转的?
为了回答这些问题,需要仔细阅读需求分析文档中的业务模型建立、问题域及其解决构
思、产品模型的构思等等前期文档,站在系统架构的角度,全面清晰的建立业务模型,包括
组织结构关系、业务功能、业务流程、业务信息交互方法、业务地理分布、业务规则和约束
条件。这个阶段的主要活动如下:
建立产品范围、目的、最终用户、业务背景等重要的初始信息。
建立完整的业务和系统的术语字典,确保项目相关人员理解上保持一致。
建立宏观层面业务的总体概念,明确总体流程、业务功能的边界、交互与协作方式,
建立系统的概念模型。
汇总业务总的组织结构与协作职能关系。
分析业务的组成节点,以及节点间交互、协同与信息的依赖关系。
分析业务节点的事件、消息,以及由此引发的状态转换关系。
汇总业务运行的基本数据模型,以便于跟踪信息的流动与格式转换。分析业务数据
的关联关系。
理解问题域以及系统需要解决的问题。
分析业务运作层面的基本业务规则与约束条件。
这个阶段的活动非常重要,架构师只有具备了这些相互关联的业务概念以后,才可能从
这些概念中抽取恰当的架构因素。
二、产品架构概念
在理解业务的基础上,我们需要进一步思考产品架构的概念,这个阶段从活动的层面看
实际上与建立业务架构概念是一样的,但是思维的重心转移到如下几个方面:
新系统投入运行以后,最高层面的业务会怎样运作?
新系统是如何解决原来工作系统的问题的?
新系统的投运,会对原来的组织结构划分发生什么样的影响?
由于新系统取代了原来的一些业务职能,业务节点的分布会发生怎样的变化?变化
后的节点间的信息又是怎样交换与依赖的?
变化后的业务事件传递又会发生怎样的变化?
新的系统加入以后,哪些业务流程将会发生重大变化?哪些不会发生变化?
业务的状态转换关系将会如何随着新系统地加入而改变?
业务的数据模型将会如何随着业务流程的变化而变化的?
新系统地加入,将会如何影响新的业务规则和约束?
从这些角度出发,我们会重新构建未来新系统投运以后的业务规则,相应的新规则也需
要建立,这就实现了业务过程的重构。
三、建立稳定的架构基线
在对业务领域与问题域有深刻的理解以后,我们需要继续研究如下一些问题:
这个复杂系统应该分成多少和哪些子系统?
子系统是如何分布在不同的业务节点或者物理节点上的?
这些分散的子系统将提供哪些接口?这些接口如何进行交互?
各个子系统需要交互哪些数据?
每个单独的子系统,所需要实现的功能有哪些?
整个系统对各个子系统有哪些功能、性能和质量上的要求?
“基线”这个词有两个意义:
这个阶段将会对整体架构策略做出重大的设计上的决策。一旦作出了这些决策,后
续开发没有重大情况不允许变动。
这个阶段完成的工作,本身就是架构阶段的重要成果,需要广泛认同、集体遵守以
及具备强制的约束力。
尽管在后期的演变中,这样的基线实际上还会不断的精化和优化,但最初下功夫构建稳
定的架构基线是十分必要的。这个阶段的活动如下:
校验与确认前期所有的业务架构与产品架构的信息,必要的时候补充相应的信息。
修订和增补术语字典,确保所有的相关人员对术语有相同的认知。
把整个系统功能进行拆分,并且分解到不同的运行节点上,构建不同的系统集和子
系统,在全局范围内划分接口与交互规则。
汇总系统/子系统接口信息,便于检索与浏览。
规划整个系统的通信链路、通信路径、通信网络等传输媒介。
把产品架构概念中的业务职能与系统功能相对应,从而确保满足业务要求。
分析系统/子系统在运行起动态协作需要交互的信息。
构建和模拟整个系统在业务环境下的动态特性,规划全系统内部状态变化过程、触
发的事件及约束条件。
详细汇总各个子系统间信息传递的过程、内容以及其它辅助信息。
根据初始的数据模型构建数据物理模型。
汇总质量上对系统的要求,并把这些质量要求细化分解,量化到各个子系统中去。
构建整个系统与子系统的构建和演化计划,在迭代过程中构建整体项目规划和初始
的迭代规划。
按固定时段预测技术的演化,汇总整个系统的应用技术及其演化。
分辨与汇总整个系统在不同阶段必须遵循的标准。
把业务约束映射到各个子系统,必要时附加 IT 业务约束。
四、子系统架构的设计与实现
通过上述各主要过程,我们已经实现了一个重要的总体架构基线。所有的子系统设计都
是在这个庞大的架构基线约束下展开,至此,首席架构师逐渐淡出,让位于子系统架构设计
师。子系统架构设计师的任务是继续分解、细化、设计各个子系统。在这个阶段,将会考虑
更加细节的问题,为后来的构件设计与单元设计作准备,我们需要考虑的问题如下:
规划给该子系统的功能是否可行?
在整个子系统的范围内,又能分解成什么子功能集?
在整个子系统的范围内,又能把哪些子功能合并到某些构件中去?
这些构件与子功能集是如何通过接口与子系统衔接的?
事实上子系统架构设计本身就是一个完整的系统设计,所区别的是视野集中到子系统的
范围内,这个阶段的活动如下:
校验与确认前期与该子系统相关的业务架构与产品架构的信息,必要的时候补充相
应的信息。
增补与本子系统相关的术语字典,确保所有的相关人员对术语有相同的认知。
把整个子系统功能进行拆分,并且分解到不同的构件节点上,构建不同的子功能集,
在子系统范围内划分接口与交互规则。
汇总子系统/构件接口信息,便于检索与浏览。
规划整个子系统的通信链路、通信路径、通信网络等传输媒介。
把产品架构概念中的子业务职能与构件功能相对应,从而确保满足业务要求。
分析子系统/构件在运行起动态协作需要交互的信息。
构建和模拟整个子系统在业务环境下的动态特性,规划子系统内部状态变化过程、
触发的事件及约束条件。
详细汇总各个构件间信息传递的过程、内容以及其它辅助信息。
根据初始的数据模型构建子系统相关的更详细的数据物理模型。
根据质量上对子系统的要求,并把这些质量要求细化分解,量化到各个构件中去。
构建整子系统的构建和演化计划,在迭代过程中构建子系统项目规划和更详细地的
迭代规划。
按固定时段预测技术的演化,汇总子系统的应用技术及其演化。
分辨与汇总子系统在不同阶段必须遵循的标准。
把业务约束映射到各个构件,必要时附加 IT 业务约束。
五、构件与实现单元的设计
进入构件设计阶段也就是进入了详细设计阶段。这个阶段主要的工作就是接口与功能设
计。在迭代模型下,这个阶段很大程度上是在迭代过程中完成的,由某个设计人员带领全体
开发团队进行分析和设计。
在这个过程中,我们应该考虑在小粒度架构中如何使产品需求变更不至于对产品质量造
成影响,还需要考虑业务概念模型与产品功能块有相应的追溯和回溯关系。这些问题,我们
将会在后面用适当的篇幅进行讨论。
小结:
大型复杂项目的成功依赖于合理的项目组织,这种组织概念包括人力资源的组织和产品
架构的组织两个方面。敏捷项目管理为这两种合理的组织提供了思维基础,为项目的成功提
供了保证。一个典型的例子是 20 世纪 90 年代加拿大空中交通系统项目(首席架构师:Philippe
Kruchten)。150 个程序员被组织到 6 个月的长周期迭代中。但是,即使在 6 个月的迭代中,
10~20 人的子系统,仍然把任务分解成一连串 6 个为期一个月的小迭代。正是这个项目的
实践成功,Philippe Kruchten 才成为 RUP 的首倡者。
敏捷过程的提出直接影响到架构设计的核心思维,正是因为敏捷过程的提出,才有了架
构驱动、弹性架构和骨架系统这些理念。也直接影响到需求分析方法、项目规划和估计方法
等一系列领域的新思维。甚至推动了业务敏捷以及与之相适应的基于服务的架构的提出。这
些观念的提出又更加推动了软件工程学向更高的层次发展。
下面我们将讨论几个专题,但讨论的时候希望研究一种思考问题的方法,从而为大家解
决更广阔的问题提供一个思维的平台。这些专题并不是独立存在,而是融合在本章所讨论的
各个阶段之中。再一次强调,方法和技术是会变化的,但优秀的思维方式是永恒的!


从MVC框架看MVC架构的设计

从MVC框架看MVC架构的设计


尽管MVC早已不是什么新鲜话题了,但是从近些年一些优秀MVC框架的设计上,我们还是会发现MVC在架构设计上的一些新亮点。本文将对传统MVC架构中的一些弊病进行解读,了解一些优秀MVC框架是如何化解这些问题的,揭示其中所折射出的设计思想与设计理念。


MVC回顾


作为一种经典到不能再经典的架构模式,MVC的成功有其必然的道理,这个道理不同的人会有不同的解读,笔者最认同的一种观点是:通过把职责、性质相近的成分归结在一起,不相近的进行隔离,MVC将系统分解为模型、视图、控制器三部分,每一部分都相对独立,职责单一,在实现过程中可以专注于自身的核心逻辑。MVC是对系统复杂性的一种合理的梳理与切分,它的思想实质就是“关注点分离”。至于MVC三元素的职责划分与相互关系,这里不再赘述,下图给出了非常细致的说明。



图1:MVC组件的功能和关系[i]


View与Controller的解耦:mediator+二次事件委派


笔者早年开发基于swing的GUI应用时,在架构MVC的实践过程中深刻体会到了view与controller之间的紧密耦合问题。在很多事件驱动的GUI框架里,如swing,用户对view的任何操作都会触发一个事件,然后在listener的响应方法里进行处理。如果让view自己注册成为事件的listener,则必须要在view中加入对controller的引用,这不是MVC希望看到的,因为这样view和controller就形成了紧密的耦合关系。若将controller注册为listener,则事件响应将由controller承担,这又会导致controller处理其不该涉及的展现逻辑,造成view和controller难以解耦的原因在于:多数的用户请求都包含一定成分的展现逻辑和一定成分的业务逻辑,两种逻辑揉合在一个请求里,在处理的时候,view与controller很难合理地分工。解决这一问题的关键是要在view与controller之间建立一种可以将展现逻辑与业务逻辑进行有效分割的机制,在这方面,PureMVC[ii]的设计非常值得参考,它通过引入mediator+二次事件委派机制很好的解决了view与controller之间的紧耦合问题。


Mediator是一种设计模式,这种模式在组件化的图形界面框架中似乎有着普遍的应用场景,即使是在四人帮的《设计模式》一书中,也使用了一个图形界面程序的示例来讲解mediator。mediator的设计用意在于通过一个媒介对象,完成一组对象的交互,避免对象间相互引用,产生复杂的依赖关系。mediator应用于图形界面程序时,往往作为一组关系紧密的图形组件的交互媒介,完成组件间的协调工作(比如点选某一按钮,其他组件将不可用)。在PureMVC中,mediator被广泛应用,其定位也发生了微妙的变化,它不再只是图形组件间的媒介,同时也成为了图形组件与command之间的媒介,这使得它不再是可选的,而是成为了架构中的必需设施。对应到传统MVC架构中,mediator就是view与controller之间的媒介(当然,也依然是view之间的媒介),所有从view发出的用户请求都经过了mediator再传递给controller,它的出现在一定程度上缓解了view与controller的紧密耦合问题。


当view、mediator和controller三者被定义出来,并进行了清晰的职责划分后,剩下的问题就是如何将它们串联起来,以完成一个用户请求了,在这方面,事件机制起到了至关重要的作用。事件机制可以让当前对象专注于处理其职责范围内的事务,而不必关心超出部分由谁来处理以及怎样处理,当前对象只需要广播一个事件,就会有对此事件感兴趣的其他对象出来接手下一步的工作,当前对象与接手对象之间不存在直接依赖,甚至感知不到彼此的存在,这是事件机制被普遍认为是一种松耦合机制的重要原因。讲到这里插一句题外话,在领域驱动设计(Domain-Driven Design)里,著名的Domain Event模式也是有赖于事件机制的这一特性被创造出来的,其用意正是为了保证领域模型的纯净,避免领域模型对repository和service的直接依赖。回到PureMVC, 我们来看在处理用户请求的过程中,事件机制是如何串联view、mediator和controller的。在PureMVC里,当一个用户请求下达时,图形组件先在自身的事件响应方法中实现与自身相关的展现逻辑,然后收集数据,将数据置入一个新的event中,将其广播出去,这是第一次事件委派。这个event会被一个mediator监听到,如果处理该请求需要其他图形组件的协助,mediator会协调它们处理应由它们承担的展现逻辑,然后mediator再次发送一个event(这次的event在PureMVC里称之为notification),这个event会促使某个command执行,完成业务逻辑的计算,这是第二次事件委派。在两次事件委派中,第一次事件委派让当事图形组件完成“处理其职责范围内的展现逻辑”后,得以轻松“脱身”,免于被“协调其他图件处理剩余展现逻辑”和“选择并委派业务对象处理业务逻辑”所拖累。而“协调其他图形组件处理剩余展现逻辑”显然是mediator的职责,于是第一次广播的事件被委派给了mediator. mediator在完成图形组件的协调工作后,并不会插手“选择并委派业务对象处理业务逻辑”的工作,这不是它的职责,因此,第二次事件委派发生了,一个新的event由mediator广播出去,后被某个command响应到,由command完成了最后的工作——“选择并委派业务对象处理业务逻辑”。


图2:mediator+二次事件委派机制


总结起来,PureMVC是通过在view与controller之间引入mediator,让view与controller变成间接依赖,用户请求从view到mediator,再从mediator到controller均以事件方式委派,mediator+二次事件委派的组合可以说是一种“强力”的解耦机制,它实现了view与controller之间的完全解耦。


从Controller到Command,自然粒度的回归


目前,很多平台的主流MVC框架在设计上都引入了command模式,command模式的引入改变了传统MVC框架的结构,受冲击最大的就是controller。在过去传统的MVC架构里,一个controller可能有多个方法,每个方法往往对应一个user action,因此,一个 controller往往对应多个user action,而在基于command的MVC架构里,一个command往往只对应一个user action。传统MVC架构里将一个user action委派到某个controller的某个方法的过程,在基于command的MVC架构里变成了将useraction与command一一绑定的过程。如果说传统controller的管理方式是在user action与model之间建立“集中式”的映射,那么基于command的管理方式就是在user action与model之间建立“点对点式”的直连映射。


图3:从基于Controller到基于Command的架构演进


主流MVC框架向command转型是有原因的,除了command自身的优势之外,一个非常重要的原因就是:由于缺少合理的组织依据,controller的粒度很难拿捏。controller不同于view与model,view与model都有各自天然的粒度组织依据,view的组织粒度直接承袭用户界面设计,model的组织粒度则是依据某种分析设计思想(如OOA/D)进行领域建模的结果,controller需要同时协调view与model,但是view与model的组织结构和粒度都是不对等的,这就使得controller面临一个“在多大视图范围内沟通与协调多少领域对象”的问题,由于找不出合理的组织依据,设计者在设计controller时往往感到无所适从。相比之下,command则完全没有controller的困惑,因为command有一个天然的组织依据,这就是user action。针对一个user action设计一个command,然后将两者映射在一起,是一件非常自然而简单的事情。不过,需要说明的是这并不意味着所有command的粒度是一样的,因为不同的user action所代表的业务量是不同的,因此也决定了command是有“大”有“小”的。遵循良好的设计原则,对某些较“大”的command进行分解,从中抽离出一些可复用的部分封装成一些较“小”的command是值得推荐的。很多MVC框架就定义了一些相关的接口和抽象类用于支持基于组合模式的命令拼装。


不管是基于controller还是基于command,MVC架构中界定的“协调view与model交互”的控制器职责是不会变的,都需要相应的组件和机制去承载与实现。在基于command的架构里,command承担了过去controller的部分职责,从某种意义上说 command是一种细粒度的controller,但是command的特性是偏“被动”的。一方面,它对于view和model的控制力比controller弱化了很多, 比如,一般情况下command是不会直接操纵view的。另一方面,它不知道自己与什么样的user action映射在了一起,也不知道自己会在何种情况下被触发执行。支撑command的运行需要额外的注册、绑定和触发机制,是这些机制加上command一起实现了controller的职责。由于现在多数基于command的MVC框架都实现并封装了这些重要的机制,所以从某种意义上说,是这些框架自身扮演了controller角色。


小结


本文主要分析了过去传统MVC架构中存在的两大弊病:view与controller的紧密耦合以及controller粒度难以把控的问题,介绍了一些MVC框架是如何应对这些问题的,这些设计方案所体现出的优秀设计思想是非常值得学习的。



[ii] PureMVC是一种MVC框架,最初使用ActionScript 3实现,现在在多种语言平台上有实现版本。官方站点:http://puremvc.org/


更多精彩博文:


领域驱动设计(Domain Driven Design)参考架构详解



关于垂直切分Vertical Sharding的粒度



企业应用集成与开源ESB产品ServiceMix和Mule介绍



论基于数据访问的集合类(Data Access Based Collection)和领域事件(Domain Event)模式



关于系统异常设计的再思考


前车之覆,后车之鉴——开源项目经验谈

前车之覆,后车之鉴

——开源项目经验谈

(本文发表于《程序员》2005年第2期)

随着开源文化的日益普及,“参与开源”似乎也变成了一种时尚。一时间,似乎大家都乐于把自己的代码拿出来分享了。就在新年前夕,我的一位老朋友、一位向来对开源嗤之以鼻的J2EE架构师竟然也发布了一个开源的J2EE应用框架(姑且称之为“X框架”),不得不令我惊叹开源文化的影响力之强大。

可惜开源并非免费的午餐,把源码公开就意味着要承受众目睽睽的审视。仅仅几天之后,国内几位资深的J2EE架构师就得出一个结论:细看之下,X框架不管从哪个角度都只能算一个失败的开源项目。究竟是什么原因让一个良好的愿望最终只能得到一个失败的结果?本文便以X框架为例,点评初涉开源的项目领导者常犯的一些错误,指出投身开源应当遵循的一些原则,为后来的开源爱好者扫清些许障碍。

成熟度

打开X框架在SourceForge的项目站点,我们立刻可以看到:在“Development Status”一栏赫然写着“5 – Production/Stable”。也就是说,作者认为X框架已经成熟稳定,可以交付用户使用。那么,现在对其进行评估便不应该有为时过早之嫌。可是,X框架真的已经做好准备了吗?

打开从SourceForge下载的X框架的源码包,笔者不禁大吃一惊:压缩包里真的只有源码——编译、运行整个项目所需的库文件全都不在其中。从作者自己的论坛得知,该项目需要依赖JBossJDOMCastorHibernate等诸多开源项目,笔者只好自己动手下载了这些项目,好一番折腾总算是在Eclipse中成功编译了整个项目。

不需要对开源文化有多么深刻的了解,只要曾经用过一些主流的开源产品,你就应该知道:一个开源软件至少应该同时提供源码发布包和二进制发布包,源码包中至少应该有所有必需的依赖库文件(或者把依赖库单独打包发布)、完整的单元测试用例(对于Java项目通常是Junit测试套件)、以及执行编译构建的脚本(对于Java项目通常是Ant脚本或者Maven脚本),但这些内容在X框架的发布包中全都不见踪影。用户如果想要使用这个框架,就必须像笔者一样手工下载所有的依赖库,然后手工完成编译和构建,而且构建完成之后也无从知晓其中是否有错误存在(因为没有单元测试)。这样的发布形式,算得上是“Production/Stable”吗?

开源必读:便捷构建

开源软件应该提供最便捷的构建方式,让用户可以只输入一条命令就完成整个项目的编译、构建和测试,并得到可运行的二进制程序。对于Java项目,这通常意味着提供完整的JUnit测试套件和Ant脚本。你的潜在用户可能会在一天之内试用所有类似的开源软件,如果一个软件需要他用半天时间才能完成构建、而且还无从验证正确性、无从着手编写他自己的测试用例,这个软件很可能在第一时间被扔到墙角。

SourceForge的项目页面可以看到,X框架的授权协议是Apache License V2.0APL)。然而在它的发布包中,笔者没有看到任何形式的正式授权协议文本。众所周知,SourceForge的项目描述是可以随时修改的(X框架本身的授权协议就曾经是GPL),如果发布包中没有一份正式的授权协议文本,一旦作者修改了SourceForge的项目描述,用户又该到哪里去寻找证据支持自己的合法使用呢?

X框架的源码中,大部分源文件在开始处加上了APL的授权声明,但有一部分源码很是令人担心。例如UtilCache这个类,开始处没有任何授权声明,而JavaDoc中则这样声明作者信息:

@author     <a href="mailto:jonesde@ofbiz.org">David E. Jones</a>

也就是说,这个类的源码来自另一个开源项目Ofbiz。值得一提的是,Ofbiz一直是“商业开源”的倡导者,它的授权协议相当严格。凡是使用Ofbiz源码,必须将它的授权协议一并全文复制。像X框架这样复制Ofbiz源码、却删掉了授权协议的行为,实际上已经构成了对Ofbiz的侵权。

另外,作者打包用的压缩格式是RAR,而这个压缩格式对于商业用户是收费的。对于一个希望在商业项目中应用的框架项目来说,选择这样一个压缩格式实在算不得明智。而且笔者在源码包中还看到了好几个.jbx文件,这是JBuilder的项目描述文件。把这些JBuilder专用的文件放在源码包中,又怎能让那些买不起或是不想买JBuilder的用户放心呢?更何况,出于朋友的关心,笔者还不得不担心X框架的作者是否会收到Borland公司的律师信呢。

开源必读:授权先行

在启动一个开源项目时,第一件大事就是要确定自己的授权协议,并在最醒目的地方用最正式的方式向所有人声明——当然,在此之前你必须首先了解各种开源授权协议。譬如说,GPLLinux采用的授权协议)要求在软件之上的扩展和衍生也必须继承GPL,因此这种协议对软件的商业化应用很不友好;相反,APL则允许用户将软件的扩展产物私有化,便于商业应用,却不利于开发者社群的发展。作为一个开源项目的领导者,对于各种授权协议的利弊是不可不知的。

除了源码本身的授权协议之外,软件需要使用的类库、IDE、解压工具等等都需要考虑授权问题。开源绝对不仅仅意味着“免费使用”,开源社群的人们有着更加强烈的版权意识和法律意识。如果你的开源软件会给用户带来潜在的法律麻烦,它离着被抛弃的命运也就不远了。

可以看到,不管从法律的角度还是从发布形式的角度,X框架都远够不上“Production/Stable”的水准——说实在的,以它的成熟度,顶多只能算是一个尚未计划周全的开源项目。虽然作者在自己的网站上大肆宣传,但作为一个潜在的用户,我不得不冷静地说:即便X框架的技术真的能够吸引我,但它远未成熟的项目形态决定了它根本无法在任何有实际意义的项目中运用。要让商业用户对它产生兴趣,作者需要做的工作还很多。

我刚才说“即便X框架的技术真的能够吸引我”,这算得上是一个合理的假设吗?下面,就让我们进入这个被作者寄予厚望的框架内部,看看它的技术水平吧。

整体架构

X框架的宣传页面上,我们看到了这样的宣传词:

X框架解决了以往J2EE开发存在的诸多问题:EJB难用、J2EE层次复杂、DTO太乱、Struts绕人、缓存难做性能低等。X框架是Aop/Ico[注:应为“IoC”,此处疑似笔误]的实现,优异的缓存性能是其优点。

下面是X框架的整体架构图:

可以看到,在作者推荐的架构中,EJB被作为业务逻辑实现的场所,而POJO被用于实现Façade。这是一个好的技术架构吗?笔者曾在一篇Blog中这样评价它[1]

让我们先回想一下,使用EJB的理由是什么?常见的答案有:可分布的业务对象;声明性的基础设施服务(例如事务管理)。那么,如果在EJB的上面再加上一层POJOFaçade,显然你不能再使用EJB的基础设施了,因为完整的业务操作(也就是事务边界)将位于POJO Façade的方法这里,所以你必须重新——以声明性的方式——实现事务管理、安全性管理、remoting、缓存等基础设施服务。换句话说,你失去了 session bean的一半好处。另一方面,“可分布的业务对象”也不复存在,因为POJO本身是不能——EJB那样——分布的,这样你又失去了session bean的另一半好处。

继续回想,使用基于POJO的轻量级架构的理由是什么?常见的答案有:易于测试;便于移植;“开发-发布”周期短。而如果仅仅把POJO作为一层Façade,把业务逻辑放在下面的EJB,那么你仍然无法轻易地测试业务逻辑,移植自然也无从谈起了,并且每次修改EJB之后必须忍受漫长的发布周期。即便是仅仅把EJB作为O/R mapping,而不是业务逻辑的居所,你最多只能通过DAO封装获得比较好的业务可测性,但“修改-发布”的周期仍然很长,因为仍然有entity bean存在。也就是说,即使是往最好的方面来说,这个架构至少损失了轻量级架构的一半优点。

作为一个总结,X框架即便是在使用得最恰当的情况下,它仍然不具备轻量级架构的全部优点,至少会对小步前进的敏捷开发造成损害(因为EJB的存在),并且没有Spring框架已经实现的基础设施(例如事务管理、remoting等),必须重新发明这些轮子;另一方面,它也不具备EJB的任何优点,EJB的声明性基础设施、可分布业务对象等能力它全都不能利用。因此,可以简单地总结说,X框架是一个这样的架构:它结合了EJB和轻量级架构两者各自的短处,却抛弃了两者各自的长处

在不得不使用EJB的时候,一种常见的架构模式是:用session bean作为Façade,用POJO实现可移植、可测试的业务逻辑。这种模式可以结合EJBPOJO两者的长处。而X框架推荐的架构模式,虽然乍看起来也是依葫芦画瓢,效果却恰恰相反,正可谓是“取其糟粕、去其精华”。

开源必读:架构必须正确

在开源软件的初始阶段,功能可以不完善,代码可以不漂亮,但架构思路必须是正确的。即使你没有完美的实现,参与开源的其他人可以帮助你;但如果架构思路有严重失误,谁都帮不了你。从近两年容器项目的更迭就可以看出端倪:PicoContainer本身只有20个类、数百行代码,但它有清晰而优雅的架构,因此有很多人为它贡献外围的功能;Avalon容器尽管提供了完备的功能,但架构的落伍迫使Apache基金会只能将其全盘废弃。

所以如果你有志于启动一个开源项目(尤其是框架性的项目),务必先把架构思路拿出来给整个社群讨论。只要大家都认可你的架构,你就有机会得到很多的帮助;反之,恐怕你就只能得到无尽的嘲讽了。

技术细节

既然整体架构已经无甚可取之处,那么X框架的实现是否又像它所宣称的那样,能够解决诸多问题呢?既然X框架号称是“AOP/IoC的实现”,我们就选中这两项技术,看看它们在X框架中的实现和应用情况。

IoC

X框架宣称自己是一个“基于IoC的应用框架”。按照定义,框架本身就具有“业务代码不调用框架,框架调用业务代码”的特性,因此从广义上来说,所有的框架必然是基于IoC模式的。所以,在框架这里,“基于IoC”通常是特指“对象依赖关系的管理和组装基于IoC”,也就是Martin Fowler所说的Dependency Injection模式[2]:由容器统一管理组件的创建和组装,组件本身不包含依赖查找的逻辑。那么,X框架实现IoC的情况又如何呢?

我们很快找到了ContainerWrapper这个接口,其中指定了一个POJO容器核心应该具备的主要功能:

public interface ContainerWrapper {

  public void registerChild(String name);

  public void register(String name, Class className);

  public void register(String name, Class className, Parameter[] parameters);

  public void register(String name, Object instance);

  public void start();

  public void stop();

  public Collection getAllInstances();

  public Object lookup(String name);

}

在这个接口的默认实现DefaultContainerWrapper中,这些功能被转发给PicoContainer的对应方法。也就是说,X框架本身并没有实现组件容器的功能,这部分功能将被转发给其他的IoC组件容器(例如PicoContainerSpringHiveMind等)来实现。在ContainerWrapper接口的注释中,我们看到了一句颇可玩味的话:

/**

 * 封装了Container,解耦具体应用系统和PicoContainer关系。

了解IoC容器的读者应该知道,在使用PicoContainerSpring等容器时,绝大多数POJO组件并不需要对容器有任何依赖:它们只需要是最普通的JavaBean,只需要实现自己的业务接口。既然对容器没有依赖,自然也不需要“解耦”。至于极少数需要获得生命周期回调、因此不得不依赖容器的组件,让它们依赖PicoContainer和依赖X框架难道有什么区别吗?更何况,PicoContainer是一个比X框架更成熟、更流行的框架,为什么用户应该选择X框架这么一个不那么成熟、不那么流行的框架夹在中间来“解耦”呢?

不管怎么说,至少我们可以看到:X框架提供了组件容器的核心功能。那么,IoC(或者说,Dependency Injection)在X框架中的应用又怎么样呢?众所周知,引入IoC容器的目标就是要消除应用程序中泛滥的工厂(包括Service Locator),由容器统一管理组件的创建和组装。遗憾的是,不论在框架内部还是在示例应用中,我们仍然看到了大量的工厂和Service Locator。例如作者引以为傲的缓存部分,具体的缓存策略(即Cache接口的实现对象)就是由CacheFactory负责创建的,并且使用的实现类还是硬编码在工厂内部:

  public  CacheFactory() {

cache = new LRUCache();

也就是说,如果用户需要改变缓存策略,就必须修改CacheFactory的源代码——请注意,这是一个X框架内部的类,用户不应该、也没有能力去修改它。换句话说,用户实际上根本无法改变缓存策略。既然如此,那这个CacheFactory又有什么用呢?

开源必读:开放-封闭原则

开源软件应该遵守开放-封闭原则(Open-Close PrincipleOCP):对扩展开放,对修改封闭。如果你希望为用户提供任何灵活性,必须让用户以扩展(例如派生子类或配置文件)的方式使用,不能要求(甚至不能允许)用户修改源代码。如果一项灵活性必须通过修改源码才能获得,那么它对于用户就毫无意义。

在示例应用中,我们同样没有看到IoC的身影。例如JdbcDAO需要使用数据源(即DataSource对象),它就在构造子中通过Service Locator主动获取这个对象:

  public JdbcDAO() {

      ServiceLocator sl = new ServiceLocator();

      dataSource = (DataSource) sl.getDataSource(JNDINames.DATASOURCE);

同样的情况也出现在JdbcDAO的使用者那里。也就是说,虽然X框架提供了组件容器的功能,却没有(至少是目前没有)利用它的依赖注入能力,仅仅把它作为一个“大工厂”来使用。这是对IoC容器的一种典型的误用:用这种方式使用容器,不仅没有获得“自动管理依赖关系”的能力,而且也失去了普通Service Locator“强类型检查”的优点,又是一个“取其糟粕、去其精华”的设计。

开源必读:了解你自己

当你决定要在开源软件中使用某项技术时,请确定你了解它的利弊和用法。如果仅仅为了给自己的软件贴上“基于xx技术”的标签而使用一种自己不熟悉的技术,往往只会给你的项目带来负面的影响。

AOP

X框架的源码包中,我们找到了符合AOP-Alliance API的一些拦截器,例如用于实现缓存的CacheInterceptor。尽管——毫不意外地——没有找到如何将这些拦截器织入(weave in)的逻辑或配置文件,但我们毕竟可以相信:这里的确有AOP的身影。可是,甫一深入这个“基于AOP的缓存机制”内部,笔者却又发现了更多的问题。

单从CacheInterceptor的实现来看,这是一个最简单、也最常见的缓存拦截器。它拦截所有业务方法的调用,并针对每次方法调用执行下列逻辑:

    IF 需要缓存

       key = (根据方法签名生成key);

       IF (cache.get(key) == null)

           value = (实际调用被拦截方法);

            cache.put(key, value);

       RETURN (cache.get(key));

    ELSE

       RETURN (实际调用被拦截方法);

看上去很好,基于AOP的缓存实现就应该这么做……可是,清除缓存的逻辑在哪里?如果我们把业务方法分为“读方法”和“写方法”两种,那么这个拦截器实际上只照顾了“读方法”的情况。而“写方法”被调用时会改变业务对象的状态,因此必须将其操作的业务对象从缓存中清除出去,但这部分逻辑在CacheInterceptor中压根不见踪影。如果缓存内容不能及时清理的话,用户从缓存中取出的信息岂不是完全错误的吗?

被惊出一身冷汗之后,笔者好歹还是从几个Struts action(也就是调用POJO Façadeclient代码)中找到了清除缓存的逻辑。原来X框架所谓“基于AOP的缓存机制”只实现了一条腿:“把数据放入缓存”和“从缓存中取数据”的逻辑确实用拦截器实现了,但“如何清除失效数据”的逻辑还得散布在所有的客户代码中。AOP原本就是为了把缓存这类横切性(crosscutting)的基础设施逻辑集中到一个模块管理,像X框架的这个缓存实现,不仅横切性的代码仍然四下散布,连缓存逻辑的相关性和概念完整性都被打破了,岂不是弄巧成拙么?

开源必读:言而有信

如果你在宣传词中承诺了一项特性,请务必在你的软件中完整地实现它。不要仅仅提供一个半吊子的实现,更不要让你的任何承诺放空。如果你没有把握做好一件事,就不要承诺它。不仅对于开源软件,对于任何软件开发,这都是应该记住的原则。

更有趣的是,X框架的作者要求领域模型对象继承Model基类,并声称这是为了缓存的需要——事实也的确如此:CacheInterceptor只能处理Model的子对象。但只要对缓存部分的实现稍加分析就会发现,这一要求完全是作者凭空加上的:用于缓存对象的Cache接口允许放入任何Object;而Model尽管提供了setModified()setCacheable()等用于管理缓存逻辑的方法,却没有任何代码调用它们。换句话说,即便我们修改CacheInterceptor,使其可以缓存任何Object,对X框架目前的功能也不会有任何影响。既然如此,又为什么要给用户凭空加上这一层限制呢?

退一万步说,即使我们认为X框架今后会用Model的方法来管理缓存逻辑,这个限制仍然是理由不足的。毕竟,目前X框架还仅仅提供了缓存这一项基础设施(infrastructure)而已。如果所有基础设施都用“继承一个基类”的套路来实现,当它真正提供企业级应用所需的所有基础设施时,Model类岂不是要变得硕大无朋?用户的领域对象岂不是再也无法移植到这个框架之外?况且,“由领域对象判断自己是否需要缓存”的思路本身也是错误的:如果不仅要缓存领域对象,还要缓存StringInteger等简单对象,该怎么办?如果同一个领域对象在不同的方法中需要不同的缓存策略,又该怎么办?X框架的设计让领域对象背负了太多的责任,而这些责任原本应该是通过AOP转移到aspect中的。在X框架这里,AOP根本没有发挥它应有的效用。

开源必读:避免绑定

开源软件(尤其是框架类软件)应该尽量避免对你的用户造成绑定。能够在POJO上实现的功能,就不要强迫用户实现你的接口;能够通过接口实现的功能,就不要强迫用户继承你的基类。尤其是Java语言只允许单根继承,一旦要求用户的类继承框架基类,那么前者就无法再继承其他任何基类,这是一种非常严重的绑定,不论用户和框架设计者都应当极力避免。

写在最后

看完这篇多少有些尖刻的批评,恐怕读者难免要怪责我“不厚道”——毕竟,糟糕的开源软件堪比恒河沙数,为什么偏要选中X框架大加挞伐呢?在此,我要给各位读者、各位有志于开源的程序员一个最后、却是最重要的建议:

开源必读:切忌好大喜功

开源是一件长期而艰巨的工作,对于只能用业余时间参与的我们更是如此。做开源务必脚踏实地,做出产品首先在小圈子里内部讨论,然后逐渐扩大宣传的圈子。切勿吹大牛、放卫星,把“未来的愿景”当作“今天的承诺”来说——因为一旦工作忙起来,谁都不敢保证这个愿景到哪天才能实现。

国人还有个爱好:凡事喜欢赶个年节“献礼”,或是给自己绑上个“民族软件”的旗号,这更是开源的大忌。凡是做过政府项目的程序员,想必都对“国庆献礼”、“新年献礼”之类事情烦不胜烦,轮到自己做开源项目时,又何苦把自己套进这个怪圈里呢?当然,如果你的开源项目原本就是做给某些官老爷看的,那又另当别论。

所以,我的这位朋友怕也不能怪我刻薄:要不是他紧赶着拿出个远未完善的版本“新年献礼”,要不是他提前放出“AOP/IoC”的卫星,要不是他妄称这个框架“代表民族软件水平”,或许我还会夸他的代码颇有可看之处呢。有一句大家都熟悉的老话,笔者私以为所有投身开源者颇可借鉴,在此与诸位共勉:

长得丑不是你的错……



[1] 这篇Blog的原文请看:http://gigix.blogdriver.com/gigix/474041.html

[2] 关于IoC模式和Dependency Injection模式,详见Martin Fowler的《Dependency Injection与模式IoC容器》一文。(中译本发表于《程序员》2004年第3期。


软件的架构与设计模式之什么是架构

什么是软件系统的架构(Architecture)?一般而言,架构有两个要素:

  ·它是一个软件系统从整体到部分的最高层次的划分。

  一个系统通常是由元件组成的,而这些元件如何形成、相互之间如何发生作用,则是关于这个系统本身结构的重要信息。

  详细地说,就是要包括架构元件(Architecture Component)、联结器( Connector)、任务流( Task-flow)。所谓架构元素,也就是组成系统的核心"砖瓦",而联结器则描述这些元件之间通讯的路径、通讯的机制、通讯的预期结果,任务流则描述系统如何使用这些元件和联结器完成某一项需求。

  ·建造一个系统所作出的最高层次的、以后难以更改的,商业的和技术的决定。

  在建造一个系统之前会有很多的重要决定需要事先作出,而一旦系统开始进行详细设计甚至建造,这些决定就很难更改甚至无法更改。显然,这样的决定必定是有关系统设计成败的最重要决定,必须经过非常慎重的研究和考察。

  计算机软件的历史开始于五十年代,历史非常短暂,而相比之下建筑工程则从石器时代就开始了,人类在几千年的建筑设计实践中积累了大量的经验和教训。建筑设计基本上包含两点,一是建筑风格,二是建筑模式。独特的建筑风格和恰当选择的建筑模式,可以使一个独一无二。

  下面的照片显示了 中美洲古代玛雅建筑,Chichen-Itza大金字塔,九个巨大的石级堆垒而上,九十一级台阶(象征着四季的天数)夺路而出,塔顶的神殿耸入云天。所有的数字都如日历般严谨,风格雄浑。难以想象这是石器时代的建筑物。


图1、位于 墨西哥Chichen-Itza(在玛雅语中chi意为嘴chen意为井)的古玛雅建筑。(摄影:作者)

  软件与人类的关系是架构师必须面对的核心问题,也是自从软件进入历史舞台之后就出现的问题。与此类似地,自从有了建筑以来,建筑与人类的关系就一直是建筑设计师必须面对的核心问题。 英国首相丘吉尔说,我们 构造建筑物,然后建筑物构造我们(We shape our buildings, and afterwards our buildings shape us)。英国下议院的会议厅较狭窄,无法使所有的下议院议员面向同一个方向入座,而必须分成两侧入座。丘吉尔认为,议员们入座的时候自然会选择与自己政见相同的人同时入座,而这就是英国政党制的起源。Party这个词的原意就是"方"、"面"。政党起源的关键就是建筑物对人的影响。

  在软件设计界曾经有很多人认为功能是最为重要的,形式必须服从功能。与此类似地,在建筑学界,现代主义建筑流派的开创人之一Louis Sullivan也认为形式应当服从于功能(Forms follows function)。

  几乎所有的软件设计理念都可以在浩如烟海的建筑 学历史中找到更为遥远的历史回响。最为著名的,当然就是模式理论和XP理论。

   架构的目标是什么

  正如同软件本身有其要达到的目标一样,架构设计要达到的目标是什么呢?一般而言,软件架构设计要达到如下的目标:

  ·可靠性(Reliable)。软件系统对于用户的商业经营和管理来说极为重要,因此软件系统必须非常可靠。

  ·安全行( Secure)。软件系统所承担的交易的 商业价值极高,系统的 安全性非常重要。

  ·可扩展性(Scalable)。软件必须能够在用户的使用率、用户的数目增加很快的情况下,保持合理的性能。只有这样,才能适应用户的市场扩展得可能性。

  ·可定制化(Customizable)。同样的一套软件,可以根据客户群的不同和市场需求的变化进行调整。

  ·可扩展性(Extensible)。在新技术出现的时候,一个软件系统应当允许导入新技术,从而对现有系统进行功能和性能的扩展

  ·可维护性(Maintainable)。软件系统的维护包括两方面,一是排除现有的 错误,二是将新的软件需求反映到现有系统中去。一个易于维护的系统可以有效地降低技术支持的花费

  ·客户体验(Customer Experience)。软件系统必须易于使用。

  ·市场时机( Time to Market)。软件用户要面临同业竞争,软件提供商也要面临同业竞争。以最快的速度争夺市场先机非常重要。

   架构的种类

  根据我们关注的角度不同,可以将架构分成三种:

  ·逻辑架构、软件系统中元件之间的关系,比如用户界面,数据库,外部系统接口,商业逻辑元件,等等。

  比如下面就是笔者亲身经历过的一个软件系统的逻辑架构图


图2、一个逻辑架构的例子

  从上面这张图中可以看出,此系统被划分成三个逻辑层次,即表象层次,商业层次和数据持久层次。每一个层次都含有多个逻辑元件。比如 WEB服务器层次中有 HTML服务元件、Session服务元件、 安全服务元件、 系统管理元件等。

  ·物理架构、软件元件是怎样放到硬件上的。

  比如下面这张物理架构图描述了一个分布于北京和上海的 分布式系统的物理架构,图中所有的元件都是物理设备,包括网络分流器、代理服务器、 WEB服务器、应用服务器、报表服务器、整合服务器、 存储服务器、主机等等。


图3、一个物理架构的例子

  ·系统架构、系统的非功能性特征,如可扩展性、可靠性、强壮性、灵活性、性能等。

  系统架构的设计要求架构师具备软件和硬件的功能和性能的过硬知识,这一工作无疑是架构设计工作中最为困难的工作。

  此外,从每一个角度上看,都可以看到架构的两要素:元件划分和设计决定。

  首先,一个软件系统中的元件首先是逻辑元件。这些逻辑元件如何放到硬件上,以及这些元件如何为整个系统的可扩展性、可靠性、强壮性、灵活性、性能等做出贡献,是非常重要的信息。

  其次,进行软件设计需要做出的决定中,必然会包括逻辑结构、物理结构,以及它们如何影响到系统的所有非功能性特征。这些决定中会有很多是一旦作出,就很难更改的。

  根据作者的经验,一个基于数据库的系统架构,有多少个数据表,就会有多少页的架构设计文档。比如一个中等的 数据库应用系统通常含有一百个左右的数据表,这样的一个系统设计通常需要有一百页左右的架构设计文档。

   架构师

  软体设计师中有一些技术水平较高、经验较为丰富的人,他们需要承担软件系统的架构设计,也就是需要设计系统的元件如何划分、元件之间如何发生相互作用,以及系统中逻辑的、物理的、系统的重要决定的作出。

  这样的人就是所谓的架构师(Architect)。在很多公司中,架构师不是一个专门的和正式的职务。通常在一个开发小组中,最有经验的程序员会负责一些架构方面的工作。在一个部门中,最有经验的项目经理会负责一些架构方面的工作。

  但是,越来越多的公司体认到架构工作的重要性,并且在不同的组织层次上设置专门的架构师位置,由他们负责不同层次上的逻辑架构、物理架构、系统架构的设计、配置、维护等工作。



OO系统设计师之路--设计模型系列(1)--软件架构和软件框架



  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值