我希望告别冗长的前言,仅述说第二版的变更。写作第二版的我,疯狂地吸收了诸多大师的设计思想,这一点可以从参考文献的前后差别看到端倪。这两年以来,我又参与了几个项目的设计与开发工作,所谓“实践出真知”,在佐证大师观点的同时,自己对设计的认识更进了一步。或许,第二版不会比第一版优秀太多,但至少会减少诸多不足。囿于版本,我无法做出新的突破。我期待能创作一本全新的书,全面论述我对软件设计的认识。现在的我,还不足以写出梦想中的软件设计之道。
言归正传。
整体而言,我对第一版的所有章节都进行了一定程度的修订。或者更正了过去的错误,或者进一步完善了原有内容。本书的内容仍然是散漫而自由的,然而形散而神不散,大体遵循了设计的基本原则。
在第一篇《设计之要》中,我新增了《对象法则》一章,言简意赅地介绍了面向对象思想的核心要素与设计原则。这基于我的一贯理念,即设计模式的核心本质是面向对象设计思想的运用。只有掌握了面向对象设计思想,才能真正体会设计模式的精髓,并将其运用在实际的项目开发过程中。《对象法则》一章可以有机地与《封装变化》一章结合起来,再加上第23章《软件体系架构》的内容,基本上勾勒出软件设计的脉络,从面向对象思想到设计模式,再到软件体系架构。
在《封装变化》一章中,我不仅完善了项目实例,还增加了关于如何“解耦具体依赖”的几种技巧。对于软件设计而言,这是非常有益的指导。我整个儿删去了第一版的第5章《设计,由你掌握》,并将其中的部分内容转移到《封装变化》一章中。这使得第一篇的内容更为紧凑,虽然删去了讨论极限编程的相关内容,却可以使得我们能够更加关注于设计,而不是方法学。
第二篇《.NET框架与设计模式》增加了对.NET 3.X的源代码分析。我无法做到与时俱进,因为.NET 4.0即将走进.NET开发人员的程序生活。或许在本书出版之后的不久,还会有5.0,6.0……我只是希望我的书不要被时代抛弃得太远。好在设计模式本身属于经典,而经典总是能够经得起时间考验的。本书讲述经典,自然能讨得一定好处。
更新最明显的是迭代器模式在.NET中的实现。C# 2.0引入的yield return以及.NET 3.0引入的Lambda表达式都为迭代器模式在.NET中成为一种惯用法贡献了一份心力。我对此的分析,可以在一定程度上帮助读者更好地理解迭代器模式。在第二篇中,我新增了一章《.NET中的命令模式》,通过解析.NET 3.0引入的WF(Windows Workflow Foundation),展现命令模式的非凡价值。第二篇的内容虽然与.NET平台息息相关,但对于其他平台的开发人员而言,仍有可观之处。我在撰写本书第二版时,同样参考了Java平台的设计理念,以及Ruby中的设计模式。
从章节名称来看,我对第三篇《媒体播放器的设计之旅》进行了颠覆性的革新。事实不然,虽然内容仍有调整,但并未动摇其根本。在对本篇进行修订时,我扮演了一名重构者的角色,利用重命名和搬移内容的方法,极大地改善了既有章节的合理性。我抛开原有的以设计模式为核心的论述方式,转而从软件设计的角度看待问题。模拟真实的软件开发,我讨论了如何运用面向对象设计思想,如何对接口进行分离。当客户需要引入第三方软件时,我提出了接口适配的方案。当需求发生变化时,我则对接口行为进行了扩展或装饰。
第四篇《设计模式应用实践》仍然体现了本书的重要价值。我对第18章《命令模式应用》的实例进行了极大地完善,使得该实例在表现命令模式方面,更加丰富与完整。第19章《职责链模式应用》完全面貌一新,替换为最近完成的一个项目实例,并通过对领域进行建模,辅以用例图、时序图、通信图和类图推导详细设计,展现了“用例驱动开发”设计思想的冰山一角。
本书对软件架构着墨不多,主要的架构思想均放在第五篇《.NET体系架构》中。利用PetShop实例,对于指导读者初窥架构之美,仍有不可低估的作用。第二版对软件体系架构的内容有所补充与增强,更多地引入了企业应用架构模式和领域驱动设计的内容。第23章《软件体系架构》算得上是技术架构的入门读物,主要介绍了分层架构模式与相关设计要素。在第24章《数据访问层》中,我特别引入了.NET 4.0中的Entity Framework,算是一次有益的尝鲜。利用这样的ORM框架,还可以极度方便地实现资源库模式与工作单元模式,在诸多分层架构中,我们都可以看到它们的身影。在第28章《表现层》中,我设想了如何在PetShop中引入ASP.NET MVC框架。我本希望能有大量篇幅介绍Silverlight,以及MVP模式的运用,如此对于.NET的表现层设计方才显得完整,可惜我对Silverlight所知不多,心有余而力不足。
第二版还有诸多变化不能体现在目录中。例如我对本书的全部设计图进行了更新,更加准确、完善和美观,并保持了图形风格的一致性。第二版加入了诸多注解,大多数内容都是正文的补充与扩展,乃至思想点滴。阅读这些注解,可以帮助读者更好地理解我的设计意图,获得更多的模式知识。
本书面对哪些读者?读者又该如何阅读本书?第一版前言已经给出了答案。本书的再版并不打算彻底地改头换面。除了致谢,我不打算重复唠叨了。
钱锺书先生认为,献书仿佛魔术家玩的飞刀,放手而并没有脱手。随你怎样把作品奉献给人,作品总是作者自己的。可我还是希望把本书献给我的孩子——子瞻。当他宁静地呆在母亲肚子里时,本书的第二版同样也在孕育之中。现在,子瞻已经过了周岁生日,没有什么礼物可以比得上这本书更让我感到自豪。我还要把本书献给我亲爱的妻子。写作虽然痛苦,可哪里及得上你分娩痛楚的万分之一。抚养子瞻的辛劳,更让虚弱的你身心憔悴。本书献给你,可否给你一丝安慰?
感谢我的父母。尤其感谢我的母亲。这一年多以来,调皮的子瞻折磨得您腰酸背痛,您却没有任何怨言,反而甘之如饴。我能有时间写作本书,您功不可没。
- 添加新评论
- 阅读次数:
1. Scrum敏捷框架
1.1 Scrum概述
Scrum是一种敏捷过程,它使用迭代和增量方式管理和控制复杂的软件与产品开发。Scrum的开发流程非常简单。首先,Product Owner根据客户的需求编写Product Backlog,然后召开计划会议,评估各项功能的相对工作量,并确定Sprint的愿景和目标,生成Sprint Backlog。然后,在Sprint(即迭代)的开发过程中,召开每日会议,Scrum Master通过它了解开发的进展以及出现的问题,检查团队成员的工作与进度。迭代结束后,团队会召开评审会议,向项目关系人展示可运行的增量版本,并检查是否达到了Sprint的目标。评审会议之后的回顾会议则会总结以往的实践经验,以提高团队生产力。
Scrum的核心在于迭代。团队首先浏览开发需求,考虑可用技术,并对自身技术及能力做出评估。然后共同确定构建功能的方案,并每日调整方法,以应对新的复杂问题、困难和出乎意料的情况。团队找出并选择最佳方案去完成任务。此创造性过程便是Scrum生产力的核心[1]。Scrum的所有实践就是围绕着一个迭代和增量的过程开展的。
1.2 Scrum的不足
与XP(eXtreme Programming,极限编程)不同,Scrum并没有提供核心的价值观与指导原则,也缺乏具体的实践方法,例如结队编程、测试驱动开发。Scrum仅仅规定了实施的基本流程与检查表,它是一个开放的管理框架,重心在于项目管理,而不是指导团队成员如何进行开发。这既是Scrum的优点,因为它很灵活,能够适应大多数场景,也可以兼容并包地引入其他方法学所提倡的实践;同时也是Scrum存在的固有缺陷,使得它难以被实践。如果没有一位优秀的Scrum Master,而团队成员又缺乏自我组织和管理的能力,就会让开发过程变得一团糟,团队成员将会无所适从。
在开发实践方面,Scrum可以借鉴XP提倡的结队编程以及测试驱动开发实现编码,通过重构对编码进行调整以适应需求的变化。但是,Scrum在建模方面却是一片空白。例如,Scrum对于如何创建Product Backlog,如何建立架构模型,以及如何在编码之前进行必要的模型设计,都没有给出具体的解决方案。缺乏正确的建模活动,就可能会对Scrum开发过程造成阻碍,影响团队达成Sprint的目标。
2. 敏捷建模与Scrum的契合
敏捷建模(Agile Modeling)是一个基于实践的建模方法,包括一系列以特定原则和价值为导向的,可被软件专业人员应用到日常工作中的实际做法[2]。敏捷建模有效地将建模敏捷化,利用敏捷方法的思想对传统的建模理念进行了重新梳理,使其更加适用于敏捷开发。敏捷建模描述了一种建模的风格。当它应用于敏捷的环境中时,能够提高开发的质量和速度,同时避免过度简化和不切实际的期望。
敏捷建模可以弥补Scrum在建模方面的不足。如果说Scrum是一个对开发过程的所有活动进行了规定的基本框架,则敏捷建模由于其对建模活动的核心关注,极大地丰富和增强了Scrum的软件过程。建模在所有的软件开发中都是不可缺少的一个重要环节。传统的建模活动,常常会重视对文档与工具的使用,要求创建的模型涵盖软件开发过程的方方面面。这种重量级的建模活动与敏捷开发方法在核心思想上是相悖的。敏捷方法需要敏捷的建模,Scrum自然也不例外。
敏捷建模定义了一组与轻量级的建模有关的价值观、原则和实践,并说明如何把它们付诸实施。本文将从敏捷建模的价值观、核心原则和核心实践三个方面讨论敏捷建模与Scrum的契合。
2.1 敏捷建模的价值观与Scrum的契合
敏捷建模的价值观包括交流、简单、反馈、勇气和谦虚。前面四条来自于XP的价值观,但完全可以说是敏捷开发的价值观。敏捷软件开发宣言强调与客户交流和团队的合作。宣言对可工作软件的重视甚于详尽的文档,凸现了简单的价值观。宣言对变更的重视体现了反馈的重要性,以及拥抱变化的勇气。Scrum同样体现了敏捷建模的第五条原则——谦虚。Scrum将整个团队定义为一种角色,作为一个整体负责将Sprint Backlog转化为可运行的产品。在开发过程中,团队成员需要管理自身的工作,同时对每次迭代和整个项目共同负责。如果没有谦虚的精神,Scrum的团队是无法运作的。
2.2 敏捷建模的核心原则与Scrum的契合
敏捷建模提出了十一条核心原则。Ambler认为,只有完全采纳这些原则,才能真正地宣称自己在进行敏捷建模。Scrum虽然没有提出具体的指导原则,但在Scrum框架和实施流程中,仍然体现了部分敏捷建模的核心原则。表1展现了在Scrum项目中敏捷建模核心原则的适用性。
表1 在Scrum项目中敏捷建模核心原则的适用性
| 敏捷建模的核心原则 | 与Scrum的契合 |
| 软件是你的首要目标 | Scrum坚持所有的Sprint都结束于演示,其目的就是要交付可工作的软件。 |
| 支持后续工作是你的第二目标 | Scrum认为,需求列表是推动迭代的主要力量,只要项目有资金,迭代就不会停止。项目的后续工作属于需求列表的内容。 |
| 轻装前进 | Scrum的最终产出物除了可工作的软件外,只包括Product Backlog和Sprint Backlog。 |
| 主张简单 | Scrum主张在一开始就要保持设计尽可能简单。 |
| 包容变化 | Scrum要求Product Owner根据不断变化的商业环境对产品作出调整。 |
| 递增的变化 | Scrum属于增量式开发,要求团队在每个Sprint周期内完成一部分产品功能增量。 |
| 有目的地建模 | 与建模相关的原则,Scrum并未要求 |
| 多种模型 | 与建模相关的原则,Scrum并未要求 |
| 你需要一个技术知识工具箱 | 团队的基本要求。 |
| 高质量的工作 | Scrum要求开发过程具有可视性,提倡对最后结果会产生影响的各个方面必须是清晰可见的,同时要求频繁的检查,以及对不合格的内容进行调整。 |
| 快速反馈 | Scrum每日会议、评审会议与回顾会议反映了这一原则。 |
2.3 敏捷建模的核心实践与Scrum的契合
敏捷建模的精华在于它的实践,但敏捷建模的实践是在价值观和原则指引下体现的。它的核心实践分为四类,即迭代和增量建模、团队协作、简单性和验证。实际上,敏捷建模的实践并没有超出敏捷开发的范围之外,只不过它的关注对象被界定为建模活动而已。因此,敏捷建模的实践完全可以应用在Scrum的开发过程中。
迭代和增量建模实践与Scrum完全吻合,因为Scrum本身就是一种迭代和增量开发。既然建模活动贯穿整个项目开发周期,因而建模采用迭代与增量的方式自然顺理成章。Scrum定义了团队角色,从而突出了团队成员的协作,成员作为一个整体参与到软件开发过程中。在Scrum中,每个成员都可能是建模人员,例如Product Owner对需求进行建模,对用户界面进行建模,团队成员对设计进行建模。简单性实践要求建模人员使用最简单的工具,创建简单的内容,简单地描述模型。归根结底,模型只需要传达它应该展现的内容,不管是需求分析,还是架构设计,都应该尽可能地保持简单,既不需要考虑格式,也不需要考虑完整,甚至可以丢弃那些已经实现了的模型。Scrum大量使用了白板、索引卡、即时贴等简单工具,创建的模型非常简单,甚至是临时的。Scrum同样重视对产品的验证,避免出现错误或与需求产生偏差。
3. 贯穿Scrum敏捷过程的敏捷建模
3.1 Scrum软件生命周期
Scrum并没有明确划分项目开发过程的阶段,而是将几种会议(计划会议、评审会议和回顾会议)定义为软件开发的里程碑。如果借用软件生命周期的概念,我们可以将Scrum划分为初始阶段、计划阶段、冲刺阶段与发布阶段。初始阶段的活动主要包括组建团队、准备资源和编写Product Backlog。计划阶段包括了Sprint的两次计划会议。冲刺阶段即一个完整的Sprint迭代,周期通常不超过六个星期。发布阶段包括评审会议与回顾会议。此阶段结束后,将发布一个达到Sprint目标的增量版本。至于产品的维护,则属于Product Backlog的一部分,列入每次迭代的范围。
3.2 初始阶段的敏捷建模
Product Backlog的编写与建模活动息息相关。Product Backlog是Scrum的核心,也是一切的起源。从根本上说,它就是一个需求、或故事、或特性等组成的列表,按照重要性的级别进行了排序。它里面包含的是客户想要的东西,并用客户的术语加以描述[3]。编写Product Backlog就是对需求进行建模。根据敏捷建模“主张简单”的原则,我们在描述Backlog的条目时,通常借鉴XP对用户故事的描述方式,而不是采用用例驱动的模式。可以使用Excel工具来创建一个Backlog表。敏捷建模认为“内容比形式更重要”,在表示Backlog时,我们甚至可以使用即时贴,将其展示在白板上,使每个人都能直接看到需求模型。
编写Product Backlog时,项目的利益相关人必须积极参与,和Product Owner一起确定Backlog的条目以及优先级。Product Backlog应该能够“包容变化”,Product Owner通过与项目关系人的讨论,可以增加新的功能,或者根据新的需求变化对其进行修改。根据敏捷建模的“多种模型”核心原则,我们也可以在Product Backlog基础上,使用用例模型或用户界面模型,帮助说明Backlog的业务流程,促进开发人员对需求的理解。
3.3 计划阶段的敏捷建模
计划阶段仅仅包括两次计划会议,每次计划会议大约持续四个小时。第一个计划会议主要确定Sprint的目标以及Sprint Backlog。第二个计划会议则需要确定Sprint Backlog中每个任务的承担人,并根据实际情况裁减Sprint Backlog,生成最终的Sprint Backlog。
敏捷建模认为,项目关系人应积极参与到需求建模活动中。Scrum负责需求建模的主要是Product Owner和客户。在计划会议中,Product Owner必须出席会议,以便对Backlog的需求条目进行解释,帮助团队理解需求。
敏捷建模的核心实践要求“与他人一起建模”。在计划会议中,团队常常会对功能需求进行拆分,其目的主要是为了更容易对工作量进行估算,但另一方面也是对需求的一种细化。一种最佳实践是将需求条目细分为任务。任务与需求条目的区别在于,需求条目属于可交付的内容,是Product Owner以及他所代表的利益相关人所关心的。而任务则不可交付,它常常代表了分析、建模、编码、测试等实现需求条目的各个环节。在拆分任务期间,并不会真正开展建模活动,但团队成员在了解到需求的具体细节时,实际上已经开始考虑需求的实现。
计划会议会对Sprint Backlog进行工作量估算。Scrum建议发挥集体的智慧。方法是利用计划纸牌。团队中的每个人都可以在深思熟虑之后,出示自己手里的纸牌,根据出示纸牌的数值取平均值,就可以大致获得该需求条目的工作量。这种方式符合敏捷建模“简单”的价值观。在讨论Sprint Backlog的需求细节时,则可以使用索引卡。根据每个需求的重要程度与优先级依次将索引卡张贴在墙上。索引卡属于敏捷建模的临时模型,在失去价值之后可以考虑丢弃,而只保留更新后的Sprint Backlog。
3.4 冲刺阶段的敏捷建模
整个冲刺阶段就是一个迭代周期,即一次Sprint。在 Sprint的开始阶段,我们可以根据Sprint Backlog建立一个任务列表模型,以及一个能够直观反映开发进度和开发效率的Burndown(燃尽)模型,并形成一个任务板。任务板要尽量简单,只需要保留必要的列。Scrum要求召开站立的每日会议,通常就在任务板前完成。团队成员一边描述昨天已经做的和今天要做的事情,一边移动任务板上对应的即时贴。每日会议结束,则任务模型也随之更新,最后由Scrum Master负责更新Sprint Backlog和Burndown模型。
在冲刺阶段引入敏捷建模非常必要,它有助于解决团队在开发过程中遇到的需求、设计以及开发方面的问题。一个方法是召集相关人员举行简短的设计会议,这在敏捷建模中称为专门或即兴建模会议。通常的流程是:首先与项目关系人探讨相关的需求,可能需要创建基本用户界面模型或者讨论业务规则的逻辑;随后继续前进讨论这些需求潜在的解决方案,这时常常会画一张白板草图来帮助讨论;再往后就是继续前进编码并测试这个解决方案[4]。
Scrum团队没有设计人员、建模人员和编码人员之间的区别,它是自组织、自管理的团队。团队的每一个成员都具有项目中所有方面的参与权力,不存在单一的团队成员对系统架构、需求或者测试负责的情况[5]。这正是敏捷建模“有效团队协作的实践”的运用。
在冲刺阶段,通过引入敏捷建模,我们可以在开发过程中创建架构模型、类结构模型和测试用例模型等内容。根据项目的实际情况,我们可以选择使用UML建模工具,或直接利用简单的白板工具创建架构模型,利用CRC卡展现类的结构模型。我们还可以借助一些需求模型以及用户界面模型,深入对需求的理解。
3.5 发布阶段的敏捷建模
Scrum评审会议实际上就是一次增量产品的演示和发布。在进行Sprint演示时,需要确保清晰阐述了Sprint目标,并让演示关注于业务层次,而不要考虑技术细节。如果我们在冲刺阶段严格地遵循了持续集成原则,就可以在每次Sprint之后发布一个增量版本,供用户使用。这实际上是“快速反馈”和“包容变化”核心原则的体现。通过每次迭代发布的版本,可以及时获得客户的反馈,验证实现是否与需求相符。如果出现偏差,或者客户提出新的建议和变化,就可以将其列入到下次Sprint的范围和目标中。
回顾会议在Scrum中是一项特殊的活动。审视和适应的能力是Scrum的基础,这也是开展回顾会议的目的。在回顾会议期间,项目团队会分析Sprint中的成功经验和遇到的障碍。Scrum回顾会议不会涉及建模活动,但它对于敏捷建模而言却具有促进作用,因为我们可以在回顾会议中总结敏捷建模应用的得与失。例如我们可以讨论建模活动是否过于复杂,是否需要引入其它的建模工具,哪些模型属于临时模型或契约模型。简而言之,在回顾会议中,我们可以检查团队的建模活动是否背离了敏捷建模的价值观、原则和实践。
4. 结束语
在Scrum项目中,建模活动仍然属于必不可少的一个环节,但却是很多Scrum实践容易忽视或轻视的一部分。Scrum敏捷框架的不足主要体现于此。如果将敏捷建模的价值观、原则与实践应用到Scrum的整个开发过程中,将有利于规范Scrum的建模活动。二者的关系是框架与细节的有机契合。Scrum提供了一个基础的框架,对敏捷开发过程中的所有活动进行了规定,而敏捷建模的重点则是全部软件过程的一部分,因而需要与另一个完整的过程结合,以增强这些过程。敏捷建模是Scrum的有效补充,在Scrum中实施敏捷建模,能够提高Scrum的可操作性,并在建模活动方面给与指导与规范。敏捷建模帮助Scrum团队找到建模的最佳点,保证我们既进行了足够的建模,以保证有效地研究和记录系统,但又没有过多地建模以致变成减慢项目进展的负担。
参考文献
[1] Ken Schwaber. Agile Project Management with Scrum[M]. 上海:世界图书出版公司,2007:6.
[2] 王毅嘉,张为群. 一种基于UML和敏捷建模的JEMM方法研究[J]. 西南师范大学学报(自然科学版),2005,30(3):426.
[3] Henrik Kniberg. Scrum and XP from the Trenches[M/OL].C4Media,2008, http://infoq.com/minibooks/scrum-xp-from-the-trenches
[4] Scott W Ambler. 敏捷建模——极限编程和统一过程的有效实践[M]. 张嘉路,朱鹏,程宾,译. 北京:机械工业出版社,2003:120.
[5] Robert C Martin. 敏捷软件开发——原则、模式与实践[M]. 邓辉,译.北京:清华大学出版社,2003:7.
- 添加新评论
- 阅读次数:
一个外部具体对象的引入,必然会给一个模块带来与外部模块之间的依赖。而具体对象的创建始终是我们无法规避的。即使我们可以利用设计模式的工厂方法模式或抽象工厂封装具体对象创建的逻辑,但却又再次引入了具体工厂对象的创建依赖。虽然在设计上有所改进,但没有彻底解除具体依赖,仍让我心有戚戚焉。 以一个电子商务网站的设计为例。在该项目中要求对客户的订单进行管理,例如插入订单。考虑到访问量的关系,系统为订单管理提
- 添加新评论
- 阅读次数:
《编程絮语》之二
没有对象协作的系统是不可想象的,因为此时的系统就是一个庞大的类,一个无所不知的“上帝类”。每个对象都有自己的自治领域,“各人自扫门前雪”,对象定义的法则就是这么自私。单一职责原则(SRP)[1]体现的正是这样的道理。对象的职责越少,则对象之间的依赖就越少。这一前提就是对象具有足够的高内聚与细粒度。这样的对象一方面有利于对象的重用,另一方面也保证了对象的稳定性。
对象的职责可以是自己承担,也可以委派给其他对象。因此,有对象就必然有依赖,正如有人就有江湖。那么,我们该如何降低对象之间的依赖?第一要则是依赖于抽象,如依赖倒置原则(DIP)[2]所云。如果无法依赖于抽象,则至少应该保证你所依赖的对象是足够稳定的。事实上,最稳定的对象就是抽象对象,所以万法归一,稳定才是降低依赖的基础。
依赖之殇的源头是“变化”。变化与稳定显然是矛盾的,软件设计的最大问题就是如何协调这两者之间的矛盾。我们需要像高明的杂技师,要学会掌握平衡,能够在钢丝绳上无碍的行走。那么,如何解决变化带来的影响呢?答案是利用封装来隔离变化。
封装的一种方式是抽象,因为相对于实现而言,接口总能保持一定的稳定性。例如税收策略。对于调用方而言,只是希望能够得到准确的税值,至于如何计算,则不是他关心的内容。抽象出计算税值的接口,就能够隔离调用方与可能变化的税收策略之间的依赖关系,如下图所示:
利用抽象还可以解除对特定实现环境例如外部资源、硬件或数据库的依赖。此时抽象隔离的变化可能是外部环境提供的API。例如,在考勤系统中,利用抽象隔离不同型号考勤机的变化。
利用抽象解除对象之间的依赖,还可以保证系统具有良好的可测试性。因为调用者依赖于抽象接口,就为我们引入Mock对象(当然也可以是Fake对象)执行单元测试提供了方便。尤其是当我们对领域对象进行测试时,如果领域对象需要对数据库操作,可以通过依赖抽象的持久对象(或仓储对象)实现职责的委派。此时,我们可以引入持久对象的Mock对象,模拟领域对象持久化的职责,既分离了领域对象与数据库资源的依赖关系,又能够提高单元测试的效率。
public interface IOrderRepository
{
void Add(OrderInfo order);
void Remove(OrderInfo order);
}
public class Order
{
private IOrderRepository m_repository;
public Order(IOrderRepository repository)
{
m_repository = repository;
}
public void Place(OrderInfo order)
{
if (order.Validate())
{
m_repository.Add(order);
}
}
}
利用封装隔离变化,并非必须依赖于抽象,根据不同的场景,降低要求,依赖于较为稳定的具体类对象也是可行的。这是一种降低复杂度的设计方式。例如,我们可以引入一个Helper类来封装第三方API的调用,从而实现调用方与第三方API的隔离。例如为SQL Server数据库操作定义一个Helper类:
public static class SQLHelper
{
public int ExecuteNonQuery() {}
public DataSet ExecuteQuery() {}
}
这样的设计类似于Gateway模式[3],利用一个Gateway对象来封装外部系统或资源访问。具体类对象显然不如抽象接口稳定,因此在设计时,我们需要遵循单一职责原则。这样的设计体现了DRY[4]原则,利用封装避免代码的重复,避免解决方案蔓延的坏味道[5]。合理的封装可以将变化点集中或限制到一处,以应对变化。一个常见的例子是利用简单工厂模式,将所有对象的创建集中在一个类中(当然也可以按模块创建不同的静态工厂)。即使创建的产品对象发生了变化,我们也可以只修改静态工厂类一处的实现。简单工厂模式常常可以应用在领域层中,通过工厂对象创建持久层对象(或所谓的数据访问对象)。
依赖源于对象的协作。传递依赖的方式可以通过属性,构造函数或方法的参数。若要保证对象间的松散耦合,构造函数或方法的参数以及属性的类型就应定义为抽象类型,如前面例子中的Order类。这是依赖解耦的关键方式,完全符合“面向接口设计”的编程思想,同时,它也有利于我们在后期实现“依赖注入”。
然而,产生依赖的方式绝不仅限于上述三种情形。例如,方法的返回值以及方法体中局部对象的创建,同样可能产生依赖。比较而言,这种依赖关系更难解除,因为它与具体的实现紧密相关。换句话说,因为这两种情形的依赖都涉及到具体对象的创建,且由实现者完成,而不能转交给调用方。例如,在如下的设计中,消息头会决定消息编码的方式。

MessageHeader的GetEncoder()方法需要返回一个IEncoder对象,这就要求在方法体中创建一个具体的IEncoder对象。要解除这样的依赖关系非常困难,如需彻底解除,一种可能是利用反射技术,通过具体类的类型来创建。还有一种可能是利用“惯例优于配置”实现解耦[6]。如果不需要彻底解除依赖,也可以利用“表驱动法”,或者直接将条件分支语句封装到方法中。
如果在方法实现中需要创建一个局部对象,我们可以考虑简单工厂模式或Registry模式[3]。例如,在Role对象的IsAuthorized()方法中,需要创建一个PriviledgeFinder对象,通过调用它的FindPriviledges()方法获得角色对应的权限集。此时,我们可以在Registry对象中提供PriviledgeFinder对象:
interface IPriviledgeFinder
{
IList<Priviledge> GetPriviledges(int roleID);
}
public class PriviledgeFinder:IPriviledgeFinder
{}
public class Registry
{
private Registry()
{ }
private static Registry Instance = new Registry();
protected virtual IPriviledgeFinder m_priviledge = new PriviledgeFinder();
public static IPriviledgeFinder PriviledgeFinder()
{
return Instance.m_priviledge;
}
}
public class Role
{
public bool IsAuthorized()
{
IList<Priviledge> priviledges =
Registry.PriviledgeFinder().GetPriviledges(this.ID);
}
}
上述实现实际上仍然利用了“将变化集中在一处”的设计原则。注意Registry类中的m_priviledge属性是virtual的受保护属性,它提供了一种变化的可能,可以交由子类去实现。
如何知道一个类是否过多的依赖其他类?一个办法就是创建这个类,并保证创建的对象能够正常使用。如果创建的过程非常复杂,就说明该类的依赖过多。此时,可以考虑分解该类的职责。如果这些依赖是必须的,则可以考虑利用封装,例如将对外部对象的调用修改为在内部创建(应用builder模式);也可以考虑使用Factory Method模式或者利用简单工厂。
依赖关系不仅仅只限于类与类之间,包(组件、模块、层)与包(组件、模块、层)之间同样存在依赖关系。良好的设计需要包之间保持松散耦合。大体上讲,包之间的依赖解除与类之间的依赖解除方式是一致的。即:要求一个包尽量依赖于一个稳定的包。注意,一个包依赖于另一个包,就代表着它依赖于这个包的每一个类。Robert C. Martin说:“我放入一个包中的所有类是不可分开的,仅仅依赖于其中一部分的情况是不可能的。”[7]因此,我们可以将一个包看做是一个类,它仍然要求职责的高内聚。在包中对类的分配,就相当于是对类进行一次分类。共同封闭原则[7]要求:“包中的所有类对于同一类性质的变化应该是共同封闭的。一个变化若对一个包产生影响,则将对该包中的所有类产生影响,而对于其他的包不造成任何影响。”简言之,我们在对包进行设计时,需要避免将不同的职责耦合在一个包中,它会造成变化点的扩散。
解除包之间依赖关系的一个重要方法仍然是抽象。使用Seperated Interface模式[3],在一个包中定义接口,而在另一个与这个包分离的包中实现这个接口。例如在分层架构模式中,我们常常对数据访问层进行抽象,使得业务逻辑层依赖于该抽象层,而不是它的低层模块。
上图的设计实际上是依赖倒置原则的体现。在项目开发中,这种将抽象与实现分别放在不同的包中,是系统设计中常见的方式。这样的设计也能够更好地应用在分布式开发场景中。
[1]单一职责原则(Single Responsibility Principle):就一个类而言,应该只专注于做一件事和仅有一个引起变化的原因;
[2]依赖倒置原则(Dependency Inversion Principle):高层模块不应该依赖于低层模块,二者都应该依赖于抽象;抽象不应该依赖于细节,细节应该依赖于抽象;
[3]Martin Fowler, Patterns of Enterprise Application Architecture;
[4]DRY原则,即“不要重复你自己(Don't Repeat Yourself)”它要求“系统中的每项知识只应该在一个地方描述。”
[5]Joshua Kerievsky, Refactoring to Patterns;
[6]文章《解除具体依赖的技术》
[7]Robert C. Martin Agile Software Development:Principles,Patterns and Practices
- 添加新评论
- 阅读次数:
《编程絮语》之一

C# 的语法脱胎于C++,因而保留了virtual关键字,可以定义一个虚方法(或虚属性)。一个类的成员被定义为virtual,就意味着它在告诉自己的子 类:我准备了一笔遗产,你可以全盘接受,也可以完全拒绝或者修改我的遗嘱。显然,虚方法授予子类的权利甚至大于抽象方法。子类面对抽象方法只有重写 (override)的权利,而对于虚方法,它还可以选择完全继承。
毫无疑问,虚方法破坏了对象的封装性。如果不加约束的使用,会对调用方 造成破坏,至少它有可能破坏子类与父类之间在外在行为上的一致性。因此,当我们在重写虚方法时,务必要遵循Liskov替换原则。我们要保证对于调用方而 言,子类对于父类是完全可以替换的。这里所谓的“替换”,是指子类不能破坏调用方对父类行为的期待。准确地说,子类在重写父类的虚方法时,必须遵循调用该 方法的前置条件与后置条件。这也是“契约式设计”的思想。最理想的状态是让使用对象甚至无法知道是否存在派生类[1]。即类的继承体系对于调用者而言,必 须体现外部接口的一致性,这样才能做到调用者对派生类无知。
如果确实需要重写父类的方法,最好的方式是扩展而不是修改。这实际上也是开放- 封闭原则的体现。例如在Decorator模式中,我们重写父类方法的目的,是为了实现对该方法的装饰。Proxy模式的实现同样如此。Michael C. Feathers对此给出的忠告是[2]:
1)尽可能避免重写具体方法。
2)倘若真的重写了某个具体方法,那么看看能否在重写方法中调用被重写的那个方法。
Feathers 的忠告是针对Java语言,因为在C#中我们无法重写具体方法,只能利用new关键字在子类中新建一个相同方法签名的具体方法,而这样的方法并不具备多态 性。这里涉及到一个有趣的话题,是关于Java和C#的比较。在Java语言中,如果没有添加任何关键字,则方法默认就是虚方法,任何子类都可以重写它。 C#则相反,它对虚方法给予了显式的定义。Java语言的缔造者显然是“性本善”论者,他认为所有子类的实现者均抱着善意的态度来对待父类的方法,因而他 赋予了子类相当程度的自由,但却可能被别有用心者偷偷打开封装的**。如果确有非常重要的隐私防止被篡改,则可以利用final关键字来强制保护。C#语 言的发明者则持有“性本恶”的论调,他恶意地揣测子类总是会不怀好意,所以提供了一套默认的强权,来保护父类的隐私。如果需要对子类开放,则明确地声明为 virtual,这就牢牢地把控制权攥紧在父类的手中。
C#保守的做法使得语言的特质更加安全(当然,Java会更加自由),我们可以使用 virtual的自由性,搭配方法的访问限制,搭建一个安全合理的白盒框架。virtual关键字的含意本身就是面向子类的,所以,我们应该尽可能地将其 放在protected方法中使用。如果该方法代表的行为确实需要公开给调用者,我们可以定义一个公开的具体方法,在其中调用一个受保护的虚方法。
在Template Method模式中,体现了C#这种划分具体方法和虚方法的好处。Template Method模式要求子类只能部分地替换父类的实现,整个骨架则必须保持固定不变。在父类中,我们将模板方法定义为具体方法,将基本方法定义为抽象方法。 模板方法规定了基本方法的调用顺序,如果我们可以在子类中重写模板方法,就可能破坏基本方法的调用顺序,从而对整个策略造成影响。Strategy模式就 不存在这个问题,因为它的策略是整体的。Template Method模式在模板方法中规定的骨架,实际上就是为调用者制订的前置条件和后置条件。
有 一种说法是不要在虚方法中访问私有字段[3]。这存在一定的合理性。因为一旦我们在父类的虚方法中访问了私有字段,那么在子类重写该虚方法时,由于无法获 得父类的私有字段值,就可能会导致该字段值的缺失。但这种说法并不完全准确。一方面,我们认为Liskov替换原则主要是为了约束Is-A关系在行为上的 一致性[4],如果该字段对行为不会造成影响,则无大碍。另一方面,这也说明我们在重写虚方法时,最佳实践还是需要在重写的同时,调用父类的虚方法,如 Decorator模式的实现方式。
[1] Alan Shalloway, James R. Trott Design Patterns Explained
[2] Michael C. Feathers Working Effectively with Legacy Code
[3] Dino Esposito, Andrea Saltarello Microsoft.NET Architecting Applications for the Enterprise
[4] Robert C. Martin Agile Software Development:Principles,Patterns and Practices
- 添加新评论
- 阅读次数:
当我看到什么速成或者多少天学会某种技术时,我泰半会采取怀疑的态度。这属于典型的标题党。那么,重构能够在31天速成吗?能,前提作为读者的你必须具备非常扎实的设计技能,以及丰富的项目经验。如果真是这样的读者,恐怕一周就能速成了吧。
开个玩笑。实际上我是想推荐一本书,它的名字叫31 Days of Refactoring。这本书其实讲的并不是什么速成技巧,而是重构技术的经验荟萃。它利用大量的代码实例(C#代码)演示了各种重构模式的应用。这些示例很简单,简单到初学者都可以看会。至于重构模式的讲解,就更直观了,没有冗长的理论描述,更没有让人乏味的重构步骤,甚至没有让人小心翼翼的单元测试,只有初始与结果。重构之前是什么样,重构之后又是什么样,全部用代码来说话。干脆,直接。
最关键的,本书完全免费!!你可以通过这个链接在线阅读每一章节,也可以通过这个链接下载本书的PDF电子版。
还犹豫什么,快行动吧!Enjoye it:)
- 添加新评论
- 阅读次数:
老美的国庆节自然不是10月1日,因此在我们举国同庆祖国60华诞的日子里,他们不得不呆在Office里继续上班,所以微软在那天发给我的邮件,我到今天才看到。邮件的主题是“恭喜您成为Microsoft MVP”。想起来,这已经是我的连续第四任了。从2006年10月第一次被评上MVP开始,到现在还没有一次落选,这让我感到很欣慰。实际上,在这三年的时间(第四任MVP的任期是从今年10月到明年。不过,微软准备将MVP任期调整到与自然年相同,所以不知这一届的任期是到什么时候结束),我在社区发表了无数篇与微软技术相关的文章,同时,还撰写以及翻译(与徐宁合译)了一本书(著作目前正在写第二版,而译著的第二版也由我承担,目前译稿已经交付出版社,就等着出版了)。尤其在InfoQ担任编辑之后,我的翻译量骤增,加上我自己写的一些原创文章,以及在技术方面的日积月累,使我在申请连任MVP时,显得底气十足。
但我深知,自己现在还存在诸多不足。技术的道路是没有止境的,我丝毫不敢懈怠。现在的我,除了平时的工作,主要是为我的第二本书做大量的准备,翻阅了大量的文献与著作,更需要从项目中总结经验。目前,我关注的技术重心还是在软件架构方面。经历得越多,越觉得自己无知。我还有很长的一段路要走。或许,过一段时间,我希望能写一些系统解决方案中关于架构的文章,尤其是架构模式在实际开发的运用。我正在学习Ruby,现在的我已经为动态语言之美而深深折服。未来,我希望能够用Ruby On Rails来开发自己的个人网站。当然,WCF和WF自然不能完全丢弃,这两个重量级技术在企业开发中是如此的重要,我怎么能够就此放弃呢?
恩,在微软创新日重庆站,我还将担任讲师,负责讲解“开启Web创新新篇章——ASP.NET MVC技术与微软Web平台简介”。我期待10月15日,在重庆市图书馆,与你们见面。
- 添加新评论
- 阅读次数:
在我看来,我从第一版出版之后得到的读者反馈实在是有限。除了有少数几位细心的读者给我指出书中的错误之外,大体上就都是泛泛而谈了。这对本书第二版的写作带来一些障碍。因为我无法知道读者对每一章的评价,不知道哪些章节对大家有益,哪些章节还有不足之处。我只能根据自己的经验来揣摩读者的想法,对第一版的内容进行改善。同时,在新版中增加第一版出书之后所获得的新知识与新认识。第二版在风格上仍然沿袭了第一版的特色,但内容无疑更加丰富。
在第一篇《设计之要》中,我会增加两个新的章节,分别介绍面向对象思想与设计原则,以及领域驱动设计。同时,删去原书的第5章《设计,由你掌握》。增加的这两章,前者是讲解设计基础,而后者则会以一个完整的案例为读者展现领域驱动设计的要点、宗旨、原则和相关思想。第五章的部分内容会合并到原书第1章《设计之道》与第2章《封装变化》之中。此外,我会极大地丰富第1章的内容,企图通过这一章为读者全面介绍软件设计的相关思想与技术。对于《封装变化》一章,我修订了一些小小错误,同时增加了“封装对象结构变化”一节。关于解除具体耦合,原书只是简单介绍了依赖注入。第二版不仅会深入介绍依赖注入,还将增加注入表驱动法、惯例优于配置、服务***等模式与方法。对于原书第3章和第4章对于重构和测试驱动开发的介绍,我准备更换一下演示的案例。尤其是重构一章,关于数学容器的设计实在太过于简单了。
第二篇《.NET Framework与设计模式》在第一版是针对.NET 2.0进行分析的。在第二版会针对最新的.NET框架进行分析。这一篇的变动不会太大,但可能会增加一些在.NET框架中的设计模式分析。目前,我已经完成了第6章《Factory Method模式》和第7章《Composite模式》的修改。我修改了第6章的Factory Method模式的例子。而在第7章,我则改善了原有的设计,使之更加完美和优雅。
第三篇《媒体播放器的设计之旅》的变化或许会比较大。因为我会开发一个真实的媒体播放器,用以演示各种模式的运用。因此,可能会增加几种模式的运用,不过关于第一版中讲解Adapter模式的章节,则可能会删去。
第四篇《设计模式应用实践》仍然沿用旧有的风格。我会对第17章的Builder模式案例进行调整,因为本章的案例对于Builder模式的应用还不够典型。第18章《Command模式应用实践》的案例不会改变,但我会进一步完善它,尤其是充分利用Command模式的特性。第19章《Chain of Responsibility模式应用实践》写得过于矫情,我可能会考虑删去它,也可能会用另外的案例代替。经读者提醒,第21章《Proxy模式应用实践》存在一个小小的错误,我会在第二版中对其进行修正。第22章《复合的设计模式实践》思想是好的,但明显有过度设计的嫌疑,且设计思路并不够好。我会考虑对其进行大的手术。此外,我可能还会增加一些章节,不过具体有哪些,我现在还说不清楚。
第五篇《.NET体系架构设计》叙述的内容以现在看来,过于陈旧了。我会在其中适度地增加体系架构设计的相关知识。最关键的是,第二版不再以PetShop作为讲解的模板,拟考虑对DinnerNow(或者StockTrader)进行分析。在这一篇中,会增加对LINQ、WCF、WF等知识的介绍。当然,介绍的思路与结构不会发生太大的变化,仍然以分层式架构作为主体框架。
- 添加新评论
- 阅读次数:
距离《软件设计精要与模式》的出版已有两年多的时间,从出版之初的热销到后来归于平淡,我也经历了从兴奋期到蛰伏期的过程。这本书的反应不算好,也不算坏。在浩瀚如大海一般的书市里,就好似一滴水珠融入大海,冒了一个小小的泡儿,然后就被波涛给淹没了。不过,这滴水珠对于我而言还是非比寻常,我不能完全漠视。这两年多以来,也陆续参与了一些项目,并负责了项目的架构设计。这期间,我又广泛的阅读了大量书籍,其中主要关注的还是软件设计。这段时间的积累,方才发现当初的想法还是过于稚嫩。这本书囿于我当初的水平,不免存在许多疏漏,甚至错误。我一直在想,如果我能够重头再来,我应该会写得更好。
出版社对于本书还是抱着正面的态度(坦白说,读者的反馈大体是还是正面的),但我不能就此满足,我希望能精益求精。去年年底,我到北京参见WinHEC大会,有机会和本书的责任编辑胡辛征先生相聚。我们就此谈了本书出版第二版的相关事宜。回来之后,我忽然开始贬低我的这本处女作了。“要么,推倒重来!?”我心中产生了一种大胆的想法。
于是,我开始了未雨绸缪,心里为自己制订的计划,也是抱着创作新书的目标。我希望自己能够阐述软件设计的本质,而不是仅仅对设计模式的展示与阐述。这对于我而言,是一项巨大的工程。唯一可以凭借的是我曾经拥有的设计经验、设计模式的培训经验以及技术书籍的创作经验。几个月的准备时间,让我积累了大约4万余字的读书笔记与心得体会。但我却迟迟不敢动笔。我对软件设计越了解得多,感觉到自己的不足就更加的深刻。我需要厚积薄发。
实际上,创作新书的想法还在于自己被刺激了。《软件设计精要与模式》一书虽然没有沦落到蒙尘的地步,但销售并没有达到我的期望。这就意味本书没有得到更多的认同。今年4月,我参加了QCon大会。在大会期间,我有幸认识了很多技术界的大师级人物,深入了解了他们的经历与思想。我觉得自己的眼界豁然开朗了。我觉得自己不能过于拘泥一时之得失。
不久之前,宁波大学的一位老师给我发来Email,说他准备选用我的书作为他们的教材。可惜现在购买不到,所以写信询问购书事宜。我于是查询了网上书店,果然发现我的书在诸如当当书店、China-Pub等处已经缺货了。询问了出版社,结果出版社的库存也没有了。基本上可以说,《软件设计精要与模式》一书已经售罄。这对于我来说,无疑是一个安慰,同时也为我打了一针强心针。
站在市场的角度,现在是创作本书的第二版的好时机。但最关键的还是我有了这样的信心和愿望。我想,我可以尽自己最大的努力来完善本书。现在,我又该踏上《软件设计精要与模式》第二版的征途了。至于我计划的新书,看来又得往后推移了。
- 添加新评论
- 阅读次数:

极限编程中有一条著名的懒汉原则,称之为KISS原则,KISS是Keep it simple and stupid的缩写。简略地说,就是设计尽量保证简单。极限编程坚持只为今天的需求设计以及编码,而不用考虑明天。这颇有一些“做一天和尚撞一天钟”的意味。
这个原则带来一个问题,那就是我们还需要设计吗?
我们强调设计,其目的就在于设计出合理、优雅的结构,以提供具有良好复用性与可扩展性的系统,这是一种未雨绸缪,为未来考虑。而现在,我们若要遵循KISS原则,就是不再考虑明天的需求。显然,这两者的观点是相悖的。于是,矛盾出现:一方面我们需要保持设计简单,不做无谓的功能预测;另一方面,我们又需要拥抱变化,在尽可能少的改变结构与代码的情况之下,满足未来的需求。
如何解决这个矛盾。让我们先看看提出简单原则的初衷。在《敏捷开发思想之拥抱变化》一篇中,我提到需求的变化是不可避免的。即使是最优秀的需求分析师和架构设计师都不可能在项目之初穷尽所有客户要求的功能,作出最完美的分析与设计,即做到“增之一分则太多,减之一分则太少”。我们需要把握分析和设计的“度”。但事实却是,我们总喜欢越俎代庖,利用自己的经验帮助客户提出需求,而事后证明这些需求往往是多余的。我们总是在重复做着“吃力不讨好”的事情,与其如此,还不如在事先偷懒取巧。因为需求的变化总是不可控的,根据“利益趋避原则”,自然应选择对自己更有利的一个趋向。
但这种简单并不是“简陋”,即使我们不需要考虑明天的需求,一些好的重用原则与可扩展原则仍然需要遵循。例如,我们应尽量保证对象是高内聚、低耦合的;我们应遵循“面向接口编程”原则。一言以蔽之,我们需要做到:
1、减少依赖;
2、合理抽象;
3、功能最简。
简单设计还需要重构来保证设计的质量。我们之所以敢于奢谈“简单”,正是因为重构的保障。即使设计过于粗陋,合理利用重构也能够亡羊补牢。在重构过程中,我们仍然需要遵循简单原则,仅为当前的需求对系统结构进行重构。例如,我们在最初的需求分析中,只有一个功能要求发送电子邮件。那么,我们可以编写一个方法来封装发送电子邮件的实现,这个方法甚至可以放在业务对象的私有方法中。随着需求的逐步演进,新增的几个功能同样需要发送电子邮件,此时就有必要利用重构技术,将原来发送电子邮件的方法独立到单独的类中。但是,基于简单原则,我们没有必要完善所有功能,例如增加发送Meet Request的功能。因为目前的需求并不需要。
“简单”并不只限于设计。在敏捷开发过程中,我们还需要保证项目计划的简单,以及文档的简单,乃至于过程的简单。项目计划的简单可以由小步行进的迭代周期来保证,通过对项目阶段的分解,简化项目计划。至于文档的简单,我们完全可以抛弃复杂标准的文档模板,转而书写仅仅是自己需要关注的内容。至少,项目内部的文档完全可以言之有物,而不需要注重形式。我们还可以通过对项目过程进行裁剪,来保障过程的简单性。事实上,在极限编程中,很多原则和实践都是为了实现简单而提出的。例如计划游戏、小版本、简单设计,包括持续集成和代码所有权,都是为了提高开发过程的效率,这实际上也是简单的一种体现。
“简单最好”是一种心态,更是一条原则。
- 添加新评论
- 阅读次数:






张逸(Bruce Zhang)


