渐进式设计:敏捷开发的长期成功秘诀
1. 进化式设计的重要性
传统设计起步迅速,因为一切都是全新的。而进化式设计则相反,一开始你需要摸索前行,随着团队产生新的想法不断改进设计。但随后,传统设计会变慢,而进化式设计会加速。根据经验,大约4 - 6周后,情况就会发生转变:采用进化式设计构建的代码比相同时间内传统设计的代码更容易处理,并且会不断改进。
进化式设计对于敏捷开发的长期成功至关重要,它具有革命性,但却鲜为人知。进化式设计主要有以下三种实践方法:
-
增量设计
:让团队成员在交付的同时进行设计。
-
简单设计
:创建易于修改和维护的设计。
-
反思式设计
:持续改进现有设计。
2. 增量设计的概念
敏捷团队对程序员提出了具有挑战性的要求:每1 - 2周,团队要完成4 - 10个以客户为中心的故事。而且客户可能随时修改当前计划并引入全新的故事,没有提前通知。这意味着程序员必须能够在一周内从头开始实现故事。由于计划随时可能改变,不能预留几周时间来建立设计架构,而应专注于交付有客户价值的故事。
增量设计允许你在交付故事时,以小的增量逐步构建设计。代码设计良好的标志是变更成本低。成功的交付团队的秘诀在于他们从不停止设计。通过结对编程或团队编程,至少一半的程序员会专注于思考设计,测试驱动开发也鼓励在每一步都改进设计。
交付团队会不断讨论设计,讨论内容既有详细琐碎的,如“这个方法应该叫什么?”,也有高层次的,如“这两个模块有一些共同的职责,我们应该把它们分开,创建第三个模块”。设计讨论不应局限于当前一起工作的人,可以根据需要进行更大范围的小组讨论,并使用有用的建模技术,保持讨论的非正式和协作性,简单的白板草图就很有效。
3. 增量设计的工作原理
增量设计与简单设计和反思式设计协同工作,具体步骤如下:
1.
简单设计
:从最简单的可行设计开始。
2.
增量设计
:当设计不能满足所有需求时,逐步进行添加。
3.
反思式设计
:每次进行更改时,通过反思设计的优缺点来改进设计。
例如,在实现网络鼠标指针时,最初创建了一个网络类,其中有一个方法用于将指针位置发送到服务器:
sendPointerLocation(x, y) {
this._socket.emit("mouse", { x, y });
}
这种具体的设计对于有经验的程序员来说可能很困难,因为他们习惯于抽象思考。但避免过早引入抽象可以创建更简单、更强大的设计。
当需要接收服务器的指针事件时,引入了
ClientPointerEvent
和
ServerPointerEvent
类,代码变为:
sendPointerLocation(x, y) {
this._socket.emit(
ClientPointerEvent.EVENT_NAME,
new ClientPointerEvent(x, y).toSerializableObject()
);
}
代码变得更复杂,但也更灵活。
当需要处理网络绘图事件时,将
sendPointerLocation(x, y)
和
sendDrawEvent(event)
方法泛化为
sendEvent(event)
方法:
sendEvent(event) {
this._socket.emit(event.name(), event.toSerializableObject());
}
按照这个模式,经过几次修改后,设计的抽象程度通常会达到理想状态,并且由于结合了实际需求和持续改进,设计会变得优雅而强大。
4. 不同层次的增量设计
4.1 类或模块内部
如果你实践过测试驱动开发,那么至少在单个模块或类的层面上实践过增量设计。从无到有,逐层构建完整的解决方案,并在过程中进行改进。在类或模块内部,重构在测试驱动开发周期的“重构”步骤中每隔几分钟就会发生,突破可能每小时发生几次,通常只需几分钟就能完成。
例如,在一个示例中,正则表达式让
transformLetter()
函数变得简单,在此之前,重构带来的是小而稳定的改进,而突破之后,
transformLetter()
函数有了显著简化。
4.2 跨类和模块
使用测试驱动开发时,容易创建出设计良好的模块和类,但还需要关注模块和类之间的交互。在工作时要考虑更广泛的范围,问自己一些问题,如“这段代码与系统的其他部分有相似之处吗?”“职责是否明确,概念是否清晰表示?”“当前正在处理的模块或类与其他模块和类的交互效果如何?”
当发现问题时,记录下来。在测试驱动开发的重构步骤中(通常是在一个合适的停顿点),仔细研究解决方案并进行重构。如果设计变更会对团队其他成员产生重大影响,可以在白板前快速讨论。
要避免设计讨论变成冗长的分歧,遵循十分钟规则:如果对设计方向有分歧达10分钟,尝试一种方案并看实际效果。如果分歧特别大,可以分成两组分别尝试作为探索性解决方案。
跨模块和跨类的重构每天会发生几次,突破可能每周发生几次,可能需要几个小时才能完成。要利用空闲时间完成突破式重构,只要设计在一周结束时比开始时更好,就足够了。
例如,在开发一个小型内容管理引擎时,最初实现了一个单一的
Server
类来提供静态文件。添加对将Jade模板转换为HTML的支持时,最初将代码放在
Server
类中。添加对动态端点的支持后,情况变得复杂,于是将模板职责提取到
JadeProcessor
模块中。这带来了一个突破,即静态文件和动态端点可以分别提取到
StaticProcessor
和
JavaScriptProcessor
模块中,并且它们都可以依赖于同一个
SiteFile
类,从而清晰地分离了网络、HTML生成和文件处理代码。
4.3 应用程序架构
这里的“架构”指的是团队代码中的重复模式,不是《设计模式》意义上的正式模式,而是代码库中反复出现的约定。例如,Web应用程序通常每个端点都有路由定义和控制器类,控制器通常采用事务脚本实现。
这些重复模式体现了应用程序架构,虽然它们使代码保持一致,但也是一种重复,会使架构变更更加困难。例如,将Web应用程序从事务脚本方法改为领域模型方法,需要更新每个端点的控制器。
引入新的架构模式时要谨慎,只引入当前代码量和功能所需的内容。在引入新约定之前,问问自己是否真的需要重复。可以尝试将重复隔离到单个文件中,或者允许系统的不同部分使用不同的方法。
例如,在之前的内容管理引擎中,没有一开始就制定支持不同模板和标记语言的宏大策略,而是从实现一个单一的
Server
类开始,让代码随着时间发展成架构。即使引入了不同标记类型的类,也没有让它们遵循一致的模式,而是让它们采用最简单的独特方法。随着时间的推移,逐渐标准化方法,最终将其转换为插件架构,现在只需将文件放入目录就能支持新的标记语言或模板。
架构决策很难更改,因此要延迟做出这些承诺。架构突破通常每几个月发生一次,支持突破的重构可能需要几周或更长时间,因为涉及大量的重复代码。只有当改进足够显著,值得付出成本时,才值得进行。
进行架构变更时,先在代码的一部分尝试新的模式,让其运行一段时间(1 - 2周),确保变更在实践中有效。确认后,让系统的其他部分遵循新方法。在日常工作中对接触到的每个类或模块进行重构,并利用空闲时间更新其他类和模块。在重构时要继续交付故事,平衡技术卓越性和交付价值,避免让现场客户失望。逐步引入架构模式有助于减少架构重构的需求,扩展架构比简化过于雄心勃勃的架构更容易。
5. 架构决策记录
一些团队使用架构决策记录(ADRs)来记录架构决策,包括正在进行的架构重构。这些是轻量级文档,不超过一两页,与代码一起存储在仓库中。
例如,一个Node.js代码库有以下引入
async
关键字的ADR:
Jan 30, 2018: async/await
ES6 is now supported, so we’re migrating from callbacks to async/await. When you edit a function that
takes a callback, refactor it to return a promise instead, and rename it to end in Async().
To perform this refactoring incrementally, it’s usually best to add a new myFunctionAsync() that exists
side-by-side with the old myFunction(). Change one caller at a time, then delete the old function when
done. (Avoid stopping halfway, so we don’t have two different functions for the same thing.)
Because making callers use await forces the caller to be async, and this has disruptive knock-
on effects, it is probably easiest for callers to migrate from myFunction(callback) to myFunction
Async().then(...).catch(...). However, migrating to await myFunctionAsync() is the long-term goal
and should be preferred when convenient.
When there are no more callbacks in use, delete this note.
6. 风险驱动的架构
架构似乎太重要了,不能不提前设计,但实际上应该尽可能晚地设计,因为这时有最多的信息,可以做出最好的决策。虽然有些问题似乎难以增量式更改,如编程语言的选择,但很多“架构”决策如果消除重复并拥抱简单性,实际上很容易更改。分布式处理、持久化、国际化、安全性和事务结构通常被认为很复杂,必须从一开始就进行设计,但可以增量式处理。
当预见到一个难题时,例如利益相关者坚持不花时间在国际化上,但知道这是迟早要解决的问题,且越晚处理成本越高。架构添加的难度取决于设计的质量,例如,当货币格式化代码在应用程序中重复时,国际化会很困难,但如果格式化代码集中化,国际化就会容易,至少不会比从一开始就处理更难。
这就是风险驱动架构的用武之地。每周都有一定的空闲时间用于重构,在决定如何使用空闲时间时,优先考虑架构风险。例如,如果代码在货币格式化方面有很多重复,国际化就存在风险,应优先进行消除重复的重构。
要将精力限制在改进设计上,不要添加新功能。例如,可以重构
Currency
类,使其在未来更容易国际化,但在处理国际化故事之前,不要实际进行国际化。重构后,以后进行国际化和现在进行一样容易。
以下是增量设计在不同层次的总结表格:
| 设计层次 | 特点 | 重构频率 | 突破频率及时间 | 示例 |
| ---- | ---- | ---- | ---- | ---- |
| 类或模块内部 | 从无到有,逐层构建,代码从具体到通用 | 每隔几分钟 | 每小时几次,几分钟完成 |
transformLetter()
函数的简化 |
| 跨类和模块 | 关注模块和类交互,发现问题记录并重构 | 每天几次 | 每周几次,几小时完成 | 内容管理引擎中模块的拆分 |
| 应用程序架构 | 关注代码重复模式,谨慎引入新架构模式 | 较少 | 每几个月一次,几周或更长时间 | 内容管理引擎架构的演变 |
下面是增量设计工作原理的mermaid流程图:
graph LR
A[简单设计:从最简单可行设计开始] --> B[增量设计:需求不满足时逐步添加]
B --> C[反思式设计:每次更改后反思改进]
C --> B
增量设计在敏捷开发中具有重要意义,通过逐步构建和持续改进设计,可以更好地应对需求变化,提高代码质量和开发效率。在不同层次的设计中,要根据其特点合理安排重构和突破的时机,同时利用架构决策记录和风险驱动架构来辅助设计过程。
7. 增量设计的优势总结
增量设计在敏捷开发中展现出多方面的显著优势,以下为详细总结:
-
适应需求变化
:敏捷开发中需求频繁变动,增量设计允许在交付故事的过程中逐步构建设计,避免了前期投入大量时间进行设计架构,而因计划改变导致工作浪费。例如,客户随时可能修改计划并引入新故事,采用增量设计,程序员能在一周内从头开始实现故事,灵活应对变化。
-
持续改进设计
:从简单设计起步,随着需求增加逐步添加和改进,每次更改都进行反思式设计,使设计不断优化。如在实现网络鼠标指针功能时,从最初简单的发送指针位置方法,到引入事件类,再到方法的泛化,设计逐渐变得更灵活、更强大。
-
降低变更成本
:代码设计良好的标志是变更成本低,增量设计通过持续的小幅度重构,使代码结构始终保持清晰,降低了后续变更的难度和成本。
-
促进团队协作
:在增量设计过程中,团队成员不断进行设计讨论,无论是结对编程、团队编程,还是大规模的小组讨论,都能让不同成员的想法相互交流,提高团队整体的设计能力和协作效率。
8. 实施增量设计的注意事项
8.1 避免过早抽象
有经验的程序员习惯抽象思考,但在增量设计中,应避免过早引入抽象。从具体的解决方案开始,根据实际需求逐步泛化,这样能创建更简单、更贴合实际的设计。例如,在实现功能时,先专注于解决当前具体问题,而不是一开始就追求通用的解决方案。
8.2 控制设计讨论
设计讨论是增量设计的重要环节,但要避免讨论变成冗长的分歧。遵循十分钟规则,如果对设计方向有分歧达10分钟,尝试一种方案并看实际效果。若分歧特别大,可以分成两组分别尝试作为探索性解决方案,以实际代码来验证设计决策。
8.3 合理利用空闲时间
在不同层次的设计中,突破式重构需要一定的时间和精力,要合理利用团队的空闲时间来完成。对于应用程序架构的突破式重构,由于涉及大量代码和可能的重复,可能需要几周或更长时间,要确保在不影响交付故事的前提下进行。
8.4 平衡技术与价值
在进行架构变更时,要继续交付故事,平衡技术卓越性和交付价值。虽然重构可能会导致代码在短期内出现不一致,但这主要是美观问题,只要能保证为客户持续提供价值,就可以接受。
9. 增量设计与其他开发实践的结合
9.1 与测试驱动开发结合
测试驱动开发(TDD)与增量设计相辅相成。在类或模块内部,TDD的循环过程(编写测试、实现功能、重构)本身就是增量设计的体现。从无到有编写测试,逐步实现功能并进行重构,使代码从具体到通用,不断优化。同时,TDD鼓励在每一步都改进设计,与增量设计中持续设计的理念一致。
9.2 与结对编程和团队编程结合
结对编程和团队编程能让至少一半的程序员专注于思考设计。在编程过程中,成员之间不断交流设计想法,无论是细节问题还是高层次的架构问题,都能及时讨论和解决。这种协作方式有助于在增量设计过程中发现问题、改进设计。
以下是增量设计与其他开发实践结合的表格总结:
| 结合的开发实践 | 结合方式 | 优势 |
| ---- | ---- | ---- |
| 测试驱动开发 | TDD循环过程体现增量设计,每一步改进设计 | 代码从具体到通用,持续优化 |
| 结对编程和团队编程 | 成员交流设计想法,及时讨论解决问题 | 提高设计能力,促进团队协作 |
下面是增量设计与其他开发实践结合的mermaid流程图:
graph LR
A[增量设计] --> B[测试驱动开发]
A --> C[结对编程和团队编程]
B --> A
C --> A
10. 总结
增量设计是敏捷开发中实现长期成功的关键方法。它通过与简单设计、反思式设计协同工作,在不同层次的设计中逐步构建和优化代码。从类或模块内部的细致重构,到跨类和模块的交互优化,再到应用程序架构的稳健演变,增量设计都发挥着重要作用。
同时,合理利用架构决策记录和风险驱动架构,能更好地辅助设计过程,应对各种挑战。在实施增量设计时,要注意避免过早抽象、控制设计讨论、合理利用空闲时间以及平衡技术与价值。与测试驱动开发、结对编程和团队编程等开发实践的结合,进一步增强了增量设计的效果。
通过采用增量设计,开发团队能够更好地适应需求变化,降低变更成本,提高代码质量和开发效率,为敏捷开发的长期成功奠定坚实基础。
超级会员免费看
646

被折叠的 条评论
为什么被折叠?



