架构设计中的方法学(四)
七、组合使用模式
我们已经讨论了敏捷架构设计的4种过程模式,在这一章中,我们对这四种过程模式做一个小结,并讨论4者间的关系以及体现在模式中的敏捷方法论特色。通过这一章的描述,大家能够对前面的内容有更进一步的了解。
四种模式的着重点
我把源自需求、团队设计、简单设计、迭代设计这4种过程模式归类为架构设计的第一层次,这4种模式能够确定架构设计过程的框架。这里需要对框架的含义进行澄清:架构设计的框架并不是说你要严格的按照文中介绍的内容来进行架构设计,在文章的一开始我们就指出,模式能够激发思考。因此,这一框架是需要结合实际,进行改造的。实际,我们在这一个部分中介绍的,比较偏向于原则,我们花了很大的时间来讨论原则的来龙去脉,而原则的度,则要大家自己去把握。为什么我们不讨论原则的度呢?这里有两个原因,一个是软件开发团队各有特色,很难定义出一个通用的度。第二个原因是我的水平不够,实践经验也不够丰富。
前面提到的四种模式其实是从四个侧面讨论了架构设计中的方法问题。源自需求提供了架构设计的基础。在软件过程中,架构设计是承接于需求分析的,如果没有良好的需求分析活动的支持,再好的架构设计也没有用。因此我们把这一模式放在首位,做为架构设计的目标。
有了确定的目标,还需有组织的保证,这也就是第二种模式――团队设计的由来。敏捷方法提倡优秀的沟通,因此团队设计是必要且有效的。而团队设计的另一个意图,是保证架构设计的下游活动得以顺利的进行,例如详细设计、编码、测试等。由于开发团队中的人大都加入了架构设计,因此最大程度的减小了不同的活动间的信息损耗和沟通效率低下的问题。如果说源自需求模式是起承上的作用,那么团队设计模式则是扮演了启下的角色。
在软件设计的过程中,沟通往往扮演着非常重要的角色。从团队设计开始的几种模式所要解决的都是沟通的问题。团队设计对沟通的贡献在于它能够把设计意图以最小的代价传播到开发团队的每个角落。这样,设计和下游的活动间由于沟通不畅产生的问题就能够得到缓解。一般而言,设计到编码会经历一个信息损失的过程,编码人员无法正确理解设计人员的意图,设计人员却往往无法考虑到一些编码的细节。虽然我们可以通过共通的设计符号来提高沟通的质量,例如UML。但是实践证明,只要能够保证畅通的沟通,即便没有优秀的开发方法,项目成功的概率依然很高。因此对于单个的项目来说,最关键的问题还是在于沟通。只要组织得当,团队设计是一个值得应用的模式。当然,配合以UML为代表的建模语言,更能够提高沟通的效果。
在设计中,我们发现,当设计信息转换为编码信息需要一定的时间,这个时间包括设计的组织时间,设计被理解的时间。如果设计比较复杂,或者说设计的文档比较复杂,编码人员花在理解上的时间就会大大增加。因此,权衡后的结果是,相对于详细的设计说明书而言,简单的设计说明书再配合一定程度的面对面沟通能够起到更好的效果。"简单要比复杂有效",这就是简单设计模式的基本思路。
同样,简单的思路还会用在软件开发的各个方面,例如文档、设计、流程。坚持简单的原则,并不断的加以改进,是降低软件开发成本的一种很有效的做法。
在有了以上的思路之后,我们还需要面对两个现实的问题。需求的变化将会导致设计的不稳定,而需求的复杂性又会导致简单架构设计的困难。为了解决这个问题,我们引入了迭代的方法,将问题分割为多个子问题(把一个复杂的问题分解为多个较简单的子问题是计算机领域最常见的处理方法)。这样,问题的范围和难度都大大降低了。而更关键的是,由于对用户需求理解不充分或用户表达需求有错导致的设计风险被降到最低点。迭代和前面几个模式都有关系。
需求和迭代
源自需求模式是架构设计中的起手式,没有这一模式的支持,架构设计只能是空中楼阁。其实,源自需求模式严格意义上并不能算是敏捷方法论的特色,而应该算是软件开发的天然特性。不幸的是,就是这么一个基本的原则,却没能够引起开发者足够的重视。
敏捷方法论中把需求摆在一个非常重要的位置,我们把源自需求模式作为架构设计的第一个模式,主要的目的是承接架构设计的上游工作――需求。需求决定架构,因此,我们在经典的瀑布模型中可以看到需求到设计的严格的分界线,但是在实际的开发中,按照瀑布模型的理论往往会遇到很多的问题,所以,我们尝试着把需求和(架构)设计之间的界限打破,形成一个重叠的地带,从而提高软件开发的速度。因此,我们在源自需求模型中指出,架构设计是紧随着需求开始的。
需求对软件开发最具影响就是需求的不稳定性。我们都非常的清楚软件开发的曲线,越到软件开发的后期,修改软件的成本越高。因此,在软件开发上游的需求的变动将会对软件开发的下游产生天翻地覆的影响。为了协调这一矛盾,软工理论提出了螺旋开发模型,这就是我们在迭代开发模式中的讨论的理论基础。把软件开发过程分为多个的迭代周期,每一次的迭代周期最后都将生成一个可交付的软件,用户在每一次的迭代结束后,可以试用软件,提出下一步的需求或是改变原先的需求。通过这样的方式,把客户、开发商的风险降到一个可以接受的水平上。
请注意迭代的前提:需求的易变性。因此,对于那些需求容易发生变化的项目,我们就可以使用迭代式的开发过程,虽然我们会付出一些额外的成本(刚开始这个成本会比较大,但可以用较长的迭代周期来降低这种成本),但是风险减小了。而对于需求比较固定的项目,是不是有必要使用迭代的方法,就要看具体的环境了。因此,我们是根据实际的情况选用开发方法,而不是因为先进或是流行的原因。
实际上,由于现代社会的特性,大部分的项目都是可以采用迭代方法。因此,我们的选择就变成了了迭代周期应该要多长。迭代周期在理论上应该是越短越好,但是并没有一个绝对的数值,时间的跨度一般从几周到几个月。一般来说,迭代周期会受到几个因素的影响:
·各模块的关联程度。在软件开发中,我们有时候很难把一些模块分离开来,要开发模块A,就需要模块B,而模块B又需要模块C。各模块的关联程度越高,迭代周期越长。当然,也相应的解决方法,我们可以在各模块的功能中选取出一些关键点,作为里程碑,以里程碑作为迭代完成点。
·人员技能、经验的平均程度。团队中成员的开发能力、开发经验良莠不齐,这也是造成迭代周期延长的一个原因。能力低、经验少的开发人员会拖后每一次迭代的时间。针对这种情况,做好统筹规划就显得非常的重要,可以通过一两次的迭代,找出队伍中的瓶颈人员,安排相应的对策。
·工具的缺乏。迭代周期越短,就意味着build、发布的次数越多,客户也就有更多的机会来修改需求。这要求有相关的工具来帮助开发人员控制软件。最重要的工具是回归测试工具。每一次迭代都需要增加新的功能,或是对原先的功能进行改动,这就有可能引入新的bug,如果没有回归测试,开发人员就需要花费时间重新测试原先的功能。
·计划、控制的能力。迭代周期越短,所需要的计划、控制的能力就越强。因为短时间内的计划制定和实施需要高度的细分,这就要求开发团队的管理者对开发能力、工作量、任务分配有很强的认识,才能做好这项工作。不过,迭代周期越短,同样开发时间的迭代次数就越多,而团队调整、改进计划控制的机会就越多。因此,后期的迭代一般都能够做到比较精确的控制。而这样的做法,要比问题堆积到软件交付日才爆发出来要好的多。没有突然落后的软件,只有每天都在落后的软件。
简单和迭代
简单和迭代关系是双向的。
在现实设计我们很难界定出简单设计的程度。怎样的架构设计才算是简单?按照我们在简单设计模式中的讨论,刚好满足目前的需求的架构设计就算是简单的设计。但是,从另外一个方面考虑,需求的易变性限制我们做出简单的设计,因为我们不能够肯定目前的需求将来会发生什么样的变化。因此,为了克服对未知的恐惧,我们花了很大的力气设计一些灵活的、能够适应变化的架构。这是源自需求模式对简单设计模式的影响。
源自需求和迭代设计的关系的讨论建议我们把需求分为多个迭代周期来实现。那么,相应的架构设计也被分在多个迭代周期中。这样的方法可以降低架构设计的复杂程度。因为设计人员不需要考虑软件的全部需求,而只需要考虑当前迭代周期的需求。复杂性的降低将会有助于架构设计的简单化,从而达到简单设计的一系列的好处(参见简单设计)。
我们从迭代设计中的最后一个例子可以清楚的看到迭代设计是如何把复杂的需求给简单化的。把握迭代设计有助于我们避免过分设计的毛病。这是个技术人员经常犯的毛病。我所在的团队很多时候也无法避免。例如,在很多的项目中,我们都会花费大量的时间来设计数据库到业务实体的映射。诸如此类的技术问题对开发人员的吸引程度是不言而喻的,但是必须看到,这种的设计会导致开发成本的大幅度上升。更为糟糕的是,除非有丰富的经验,这种类型的设计给开发工作带来的价值往往无法超过其成本。
因此,我们需要学会权衡利弊,是否有必要投入大量的资源来开发其实并没有那么有用的功能。因此,迭代设计和简单设计的结合有助于我们摆脱过度设计的困扰,把精力集中在真正重要的功能之上。
此外,简单的设计并不等同于较少的付出。简单的设计往往需要对现实世界的抽象,回忆我们在简单设计中讨论的测量模式的例子,它看似简单,但实现起来却需要大量的业务知识、很强的设计能力。因此,做到简单是程序员不断追寻的目标之一。
在很多的方法论中,一般并不过分注意代码重复的问题,要么是不关注,要么认为适当的代码重复是允许的。而XP却把代码重复视为良好代码的大敌。"只要存在重复代码,就说明代码仍有Refactoring的可能。"这种观点看起来非常的绝对,这可能也正是其名字中Extreme的来历(英文中的Extreme属于语气非常重的一个单词)。从实践的角度上来看,追求不重复的代码虽然很难做到,但是其过程却可以有效的提高开发团队代码的写作质量,因为它逼迫着你在每次迭代重对代码进行改进,不能有丝毫的怠惰。而这种迭代的特性,促进了简单的实现。
团队和简单
我们在简单设计中提过简单设计需要全面的设计师。除此之外,它还需要团队的配合。简单意味着不同活动间交付工件的简单化。也就是说,类似于需求说明书、设计文档之类的东西都将会比较简单。正因为如此,我们很难想象一个地理上分布在不同地点的开发团队或一个超过50人的大团队能够利用这种简单的文档完成开发任务。
因此,简单的设计是需要团队的组织结构来保证的。简单的设计要求团队的相互沟通能够快速的进行。架构设计完成后,架构的设计思路传达给所有的编码人员的速度要块,同样,编码中发现问题,回馈给设计者,设计者经过改进之后再传达给收到影响的编码人员的速度也要快。象这样高效率的传播我们可以称之为"Hot Channel"。
为了保证"Hot Channel"的高沟通效率,最好的组织单位是开发人员在3到6人之间,并处于同间工作室中。这样的结构可以保证讯息的交互速度达到最高,不需要付出额外的沟通成本,也不需要过于复杂的版本控制工具或权限分配。根据我的经验,一个共享式的小型版本控制工具、网络共享、再加上一个简单的网络数据库就能够解决大部分的问题了。
在理论上,我们说只要分配得当,大型的团队同样可以组织为金字塔式的子团队,以提高大型团队的工作效率。但是实际中,随着团队的人数的增加,任务的正确分配的难度也随之加大,沟通信息上传下达的效率开始下降,子团队间的隔阂开始出现,各种因素的累加导致敏捷方法并不一定适合于大型的团队,因此我们讨论的敏捷方法都将受到团队的特性的限制。
模式的源头
如果你对XP有一定的了解的话,那么你可能会感觉到我们讨论的模式中应用了XP的实践。确实如此,XP中有很多优秀的实践,如果组织得当的话,这些微小的实践将会组合成为一套了不起的开发方法。不过,目前的软件开发混乱的现状阻止了先进的软件方法的应用,对一个身体虚弱的病人施以补药只会适得其反。因此,在前面讨论的模式中,我们应用了一些容易应用、效果明显的实践方法。在实践中适当的应用这些方法,并不需要额外的投入,却能够有很好的效果,同时还会为你的团队打下一个良好的基础。
八、架构愿景
从这一篇开始,我们将会进入另一个不同的主题,和前面所讨论的模式专注于组织、过程、方法不同,以后介绍的模式更偏重于设计。但是过程、方法的影子依然在我们的讨论中隐约可见。
架构愿景是一个很简单的模式,在软件开发中所占的时间也很短。但是这并不意味着架构愿景不重要。相反,它会是设计过程不可或缺的一环。
Context
在单次的迭代开始阶段,我们已经收集好了单次迭代的需求。
Problem
架构和分析设计是密不可分的,有时候很难说得清楚架构的定义,但架构应该能够描述软件的整体。架构包括了软件的各个方面,但是每一个设计细节总是需要单独考虑,这时候就会出现设计细节之间、以及设计细节和架构之间的不一致。
架构设计的各个部分之间的设计冲突是很容易发生的。发生的概率及频率和团队的规模成正比、和沟通的频度及效果成反比。在很多次的项目开发过程中,我们发现了多处的相同功能的代码,原因是代码的作者并不知道别人已经实现了这个功能了。这可能只是浪费了一点精力,可如果不同模块间的设计冲突导致了软件无法正常运行的时候,我们就需要坐下来好好的审视,究竟发生了什么。
Solution
我们需要建立一个架构愿景。架构愿景应该能够提供软件全局视图,包括所有的重要部分,定义了各个部分的责任和之间的关系,而且还定义了软件设计需要满足的原则。而这个架构愿景的设计,应该是满足源自需求模式的,也就是说,部分的划分和部分的设计,都是根据需求而进行的。同时,架构愿景应该要能够满足架构的其它各种特点,例如简单、可扩展性、抽象性。简单来说,我们把架构愿景当作是一个mini的架构设计。
由于我们是在单次的迭代中讨论架构愿景,因此从整体上考虑,架构愿景是也是在不断的变化的。这是很自然的,因为架构愿景代表了架构的设计,架构愿景的演进代表了架构设计的演进。
架构的愿景是相对于一个范围来说的,在一个特定的软件功能范围之内,谈架构愿景才有实际的意义,例如针对软件的全局或某个子模块。在这个特定的范围中,订立了架构愿景之后,这个范围内的所有设计原则将不能违背架构愿景。这是非常重要的,是架构愿景的最大的用处。有了这样的保证,我们就可以保证设计的一致性和有效性。任何一项设计的加入,都能够融入到原先的架构中,使得软件更加的完善,而不是更加的危险。
当然,要做到这一点并不是一件容易的事情。特别需要指出的是,架构愿景模式仅仅是实现该目的的一条道路,并不是一个充分条件。如果在设计中愿景不能够贯彻其意志,或是愿景制定本身就存在问题,那么要想达到上述的效果,几乎是不可能的事情。此外,该模式仅仅只是达到该目的的第一步,我们在接下去的模式中会发现还需要很多方面的配合。
架构愿景的层次
我们根据架构适用范围的不同,把架构愿景分为几个类别讨论:
软件全局
软件全局的架构是我们所最关心的,因此也会花费最多的笔墨。
软件全局中的架构愿景一般很难具体化到代码级别,其实你会发现,就算是具体化到了代码级别,也会因为实际中存在的问题,导致代码没有太多的价值。因此,为软件全局设置的架构愿景可以以原则、或是模式名的方式体现,并用自然语言或伪代码描述。例如,可以为一个系统规定三层架构作为其愿景,并指出三层的分类原则。注意,我们需要指出分类原则,否则规定三层架构并没有太大的意义,因为三层架构随着实现平台的不同、开发人员的不同而有很大的差异,如果不能够规定一个可操作的规范,那么愿景是没有意义的。在Java环境下,我们可以这样说:
"客户端采用前端浏览器界面,业务逻辑采用servlet,配合JSP编写,浏览器到服务器的数据采用集中处理,具体的方法是在业务逻辑和前端浏览器之间采用Front Control模式,接受前端浏览器传送过来的数据,并指派给相应的业务逻辑处理。数据的合法性检验分为两个部分:和业务逻辑无关的基本合法性验证在前端使用Java Script处理,和业务逻辑相关的合法性验证在业务逻辑层处理,可以使用一个集中的servlet专门处理错误情况。"
而在Windows环境下,我们知道三层结构的分界和Java中的并不相同(关于这一点,下一篇的文章中将会有详细的描述),我们可以在业务逻辑层或显示层直接操纵数据,非常的方便,因此,在Windows中的架构愿景的描述又不一样。另外,在非面向对象的环境中,在分布式的环境中,在Lotus Notes的环境中,其架构的描述都不一样。
注意到,架构愿景的制定是根据不同的应用环境而变化的,这一点我们在文章的一开始就强调过,这里又出现了相同的问题。我们可以通过一个简单的例子来加深对这个问题的理解。在Java的环境下,特别是J2EE的环境下面,经典的设计思路是把数据库的一张表视作一个类,表中的每一行都是这个类的一个具体的实例,表的每一个字段对应了类的一个变量,类一般支持Find方法,来获取数据不同的实例。这是一种很自然的设计思路,而且可以通过CMP等技术来简化设计的繁琐步骤。可是,在Windows环境下,相信没有多少人会这么做。常用的处理方法是使用记录集(RecordSet)的方式,也就是说针对一组的记录来进行操作,而不是单个的记录。这些记录可以是跨表的(使用SQL的连接),也可以是单表的。由于Windows环境下大多数的编程语言都能够支持记录集方式,因此编写基于记录集方式的程序是相当简单的。当然,我们也可以采用J2EE平台的编程方式,但是会导致编程量的激增,同时也难以达到预计的效果。这种平台的差别导致了架构愿景的差别。
此外,我们注意到我们对架构的描述其实还是不够仔细,这是非常正常的,因为随着开发的深入,我们会不断的完善架构的描述。例如,我们可以写出Front Controll的类框架,写出主要的几个Servlet,以及他们之间的关系。这个时候,使用UML类图会是一个不错的选择,实际上,我非常推崇在架构设计中大量的使用类图。不过在实际的运作中,每个软件团队大多数都有自己熟悉的架构设计思路,使用何种工具并不是主要的问题。
我们从源自需求模式中,学习到架构的设计是来自于需求的,而应用于软件全局的架构则来自于最重要的需求。还记得我们在那个模式中提到的网上宠物店的例子吗?系统采用了MVC模式,sun的官方文档一开始说明了为什么采用MVC模式,MVC模式解决了什么问题,然后开始分析MVC模式的几个组成部分:Model、View、和Controll。其实,MVC中的每一个部分,在真正的代码中,大都代表了一个子系统,但是在目前,我们就非常的清楚系统大致上会是一个什么样子,虽然这时候它还十分的朦胧。
不要视图在全局的架构愿景中就制定出非常细致的规划,更不要视图生成大量的实际代码。因为,你的架构愿景还没有稳定(我们在其后的稳定化的模式中将会讨论稳定的问题),还没有获得大家的同意,也没有经过证明。因此,从整个的开发周期来看,全局架构愿景是随着迭代周期的进行不断发展、修改、完善的。
我们如何确定全局架构愿景工作的完成?一般来说,你的架构设计团队取得了一致的意见就可以结束了,如果问题域是团队所熟悉的,一两个小时就能够解决问题。接下来设计团队把架构愿景传播到整个的开发团队,大家形成一致的认识,不同的意见将会被反馈会来,并在本次的迭代周期(如果时间比较紧迫)或下一次的迭代周期中(如果时间比较宽松)考虑。
子模块级、或是子问题级的架构愿景
这时候的架构愿景已经是比较明确的了,因为已经存在明确的问题域。例如界面的设计、领域模型的设计、持久层的设计等。这里的愿景制定本质上和全局的愿景制定差不多,具体的例子我们也不再举了。但是要注意一点,你不能够和全局愿景所违背。在操作上,全局愿景是设计团队共同制定出来的,而子模块级的架构愿景就可以分给设计子团队来负责,而其审核则还是要设计团队的共同参与。这有两个好处,一是确保各个子模块(子问题)间不至于相互冲突或出现空白地带,二是每个子设计团队可以从别人那里吸取设计经验。
在设计时,同样我们可以参考其它的资料,例如相关的模式、或规范(界面设计指南)。在一个有开发经验的团队,一般都会有开发技术的积累,这些也是可供参考的重要资料。
我们在这个层次的愿景中主要谈一谈子模块(子问题)间的耦合问题。一般来说,各个子模块间的耦合程度相对较小,例如一个MIS系统中,采购和销售模块的耦合度就比较小,而子问题间的耦合程度就比较大,例如权限设计、财务,这些功能将会被每个模块使用。那么,我们就需要为子模块(子问题)制定出合同接口(Contact Interface)。合同的意思就是说这个接口是正式的,不能够随意的修改,因为这个结构将会被其它的设计团队使用,如果修改,将会对其它的团队产生无法预计的影响。合同接口的制定、修改都需要设计团队的通过。此外,系统中的一些全局性的子问题最好是提到全局愿景中考虑,例如在源自需求模式中提到的信贷帐务的例子中,我们就把一个利息计算方式的子问题提到了全局愿景中。
代码级的愿景
严格的说这一层次的愿景已经不是真正的愿景,而是具体设计了。但是我们为了保证对架构设计理解的完整性,还是简单的讨论一下。这一个层次的愿景一般可以使用类图、接口来表示。但在类图中,你不需要标记出具体的属性、操作,你只需要规定出类的职责以及类之间的相互关系就可以了。该层次愿景的审核需要设计子团队的通过。
而设计细分到这个粒度上,执行愿景设计的开发人员可能就只有一两个左右。但是比较重要的工作在于问题如何分解和如何归并。分解主要是从两个维度来考虑,一个是问题大小维,一个是时间长短维。也就是说,你(设计子团队负责人)需要把问题按大小和解决时间的长短分解为更细的子问题,交给不同的开发人员。然后再把开发人员提出的解决方法组合起来。
架构愿景的形成过程
架构愿景的形成的源头是需求,需要特别指出的是,这里的需求主要是那些针对系统基本面的需求。比如说,系统的特点是一个交互式系统,还是一个分布式系统。这些需求将会影响到架构愿景的设计。在收集影响架构愿景的各项需求之后,按照需求的重要性来设计架构愿景。
架构愿景的设计并不需要很复杂的过程,也不需要花费很多的时间。我们已经提过,架构远景的主要目的就是为了能够在开发团队中传播设计思路,因此,架构愿景包括基本的设计思路和基本的设计原则。
值得注意的是,架构远景可能会有多种的视角,下文讨论了一种设计模式的视角。但是实际设计中还可能会基于数据库来设计架构愿景。但在企业信息系统的设计中,我推荐使用领域类的设计,也就是下文中讨论的例子。
架构愿景设计好之后,问题的焦点就转到如何传播架构愿景上来,为了达到在开发团队中取得统一设计意图的效果,可以考虑援引团队设计模式。除此之外,针对性的项目前期培训也会是一种有效的做法。
使用架构模式
架构模式也是一种很好的架构愿景设计思路的来源。随着对设计模式的研究的深入,人们发现其中的一些设计模式可以扩展、或变化为软件设计的基础。在这个基础上再实现更多的设计,这些模式就形成了架构模式。当然,不同的软件,它们的架构模式也是不一样的。在《Applying Pattern》一文中,有一个很典型的架构愿景的例子:
假设我们需要设计分布式的交互式系统。分布式系统和交互式系统都有特定的架构模式,前者为Broker模式,后者为MVC模式。首先我们先要根据系统的特点的重要程度来排列模式的顺序。这里假设需求中分布式特性更重要一些。那么我们首先选择Broker模式作为架构的基本模式:
再考虑交互式的特点,根据MVC模式的特点,我们需要从目前的基本架构中识别出Model、Controller、以及View。Model和View都很简单,分别分布在上图中的Server和Client中。而Controller则有两种的选择,假设这里的Controller部署在客户端,上图则演化为下图:
这样,基础的架构愿景就已经出现了。如果我们还有更多的需求,还可以继续改进。但是,记住一点,架构愿景不要过于复杂。正如我们在上一节中所讨论的,这里我们虽然是基于设计模式来讨论架构愿景,但是实际中还有很多从其它的视角来看待架构愿景的。至于要如何选择架构愿景的视角,关键的还是在于需求的理解。