编程方法论
# 编程方法论
# 编程宣言
# 敏捷软件开发宣言
我们一直在实践中探寻更好的软件开发方法,身体力行的同时也帮助他人。由此我们建立了如下价值观:
个体和互动 高于 流程和工具 工作的软件 高于 详尽的文档 客户合作 高于 合同谈判 响应变化 高于 遵循计划
也就是说,尽管右项有其价值,我们更重视左项的价值。
# 敏捷软件的十二条原则
我们遵循以下原则:
- 我们最重要的目标,是通过持续不断地及早交付有价值的软件使客户满意。
- 欣然面对需求变化,即使在开发后期也一样。为了客户的竞争优势,敏捷过程掌控变化。
- 经常地交付可工作的软件,相隔几星期或一两个月,倾向于采取较短的周期。
- 业务人员和开发人员必须相互合作,项目中的每一天都不例外。
- 激发个体的斗志,以他们为核心搭建项目。提供所需的环境和支援,辅以信任,从而达成目标。
- 不论团队内外,传递信息效果最好效率也最高的方式是面对面的交谈。
- 可工作的软件是进度的首要度量标准。
- 敏捷过程倡导可持续开发。责任人、开发人员和用户要能够共同维持其步调稳定延续。
- 坚持不懈地追求技术卓越和良好设计,敏捷能力由此增强。
- 以简洁为本,它是极力减少不必要工作量的艺术。
- 最好的架构、需求和设计出自组织团队。
- 团队定期地反思如何能提高成效,并依此调整自身的举止表现。
# 三分钟了解敏捷开发
小灰经过千辛万苦,终于拿到了心仪的 offer,今天小灰上班的第一天 ……






下班后,小灰找到同学大黄来请教 ……



# 场景一:小灰在餐厅














# 场景二:无奈的项目经理






# 敏捷开发
# 什么是敏捷开发?
敏捷开发(Agile)是一种以人为核心、迭代、循序渐进的开发方法。
在敏捷开发中,软件项目的构建被切分成多个子项目,各个子项目的成果都经过测试,具备集成和可运行的特征。
简单地来说,敏捷开发并不追求前期完美的设计、完美编码,而是力求在很短的周期内开发出产品的核心功能,尽早发布出可用的版本。然后在后续的生产周期内,按照新需求不断迭代升级,完善产品。
这一切正如某人的名言:

是谁这么厉害,提出了敏捷开发思想?是一位名叫 Martin Fowler 的美国大叔。

大叔不但是敏捷开发的创始人之一,还在面向对象开发、设计模式、UML 建模领域做出了重要贡献。目前担任 ThoughtWorks 公司的首席科学家。


# 敏捷开发模式的分类
敏捷开发的实现主要包括 SCRUM、XP(极限编程)、Crystal Methods、FDD(特性驱动开发)等等。其中 SCRUM 与 XP 最为流行。
同样是敏捷开发,XP 极限编程 更侧重于实践,并力求把实践做到极限。这一实践可以是测试先行,也可以是结对编程等,关键要看具体的应用场景。
SCRUM 则是一种开发流程框架,也可以说是一种套路。SCRUM 框架中包含三个角色,三个工件,四个会议,听起来很复杂,其目的是为了有效地完成每一次迭代周期的工作。在这里我们重点讨论的是 SCRUM。
# SCRUM 的工作流程
学习 Scrum 之前,我们先要了解几个基本术语:
- Sprint:冲刺周期,通俗的讲就是实现一个“小目标”的周期。一般需要 2-6 周时间。
- User Story:用户的外在业务需求。拿银行系统来举例的话,一个 Story 可以是用户的存款行为,或者是查询余额等等。也就是所谓的小目标本身。
- Task:由 User Story 拆分成的具体开发任务。
- Backlog:需求列表,可以看成是小目标的清单。分为 Sprint Backlog 和 Product Backlog。
- Daily meeting:每天的站会,用于监控项目进度。有些公司直接称其为 Scrum。
- Sprint Review meeting: 冲刺评审会议,让团队成员们演示成果。
- Sprint burn down:冲刺燃尽图,说白了就是记录当前周期的需求完成情况。
- Release:开发周期完成,项目发布新的可用版本。

如上图所示,在项目启动之前,会由团队的产品负责人(Product owner)按照需求优先级来明确出一份 Product Backlog,为项目做出整体排期。
随后在每一个小的迭代周期里,团队会根据计划(Sprint Plan Meeting)确定本周期的 Sprint Backlog,再细化成一个个 Task,分配给团队成员,进行具体开发工作。每一天,团队成员都会进行 Daily meeting,根据情况更新自己的 Task 状态,整个团队更新 Sprint burn down chart。
当这一周期的 Sprint backlog 全部完成,团队会进行 Spring review meeting,也就是评审会议。一切顺利的话,会发布出这一版本的 Release,并且进行 Sprint 回顾会议(Sprint Retrospective Meeting)。
那么,现实中的 Scrum 是什么样的情景呢?看看下面的照片就知道了:





# 敏捷开发与 DevOps
DevOps 是 Development 和 Operations 的合成词,其目标是要加强开发人员、测试人员、运维人员之间的沟通协调。如何实现这一目标呢?需要我们的项目做到持续集成、持续交付、持续部署。
时下流行的 Jenkins、Bamboo,就是两款优秀的持续集成工具。而 Docker 容器则为 DevOps 提供了强大而有效的统一环境。


# 极限编程
# 概述
敏捷方法论有一个共同的特点,那就是都将矛头指向了“文档”,它们认为传统的软件工程方法文档量太“重”了,称为“重量级”方法,而相应的敏捷方法则是“轻量级”方法。正是因为“轻量级”感觉没有什么力量,不但不能够有效体现灵活性,反而显得是不解决问题的方法论似的。因此,就有了一次划时代的会议,创建了敏捷联盟。
在敏捷方法论领域中,比较知名的、有影响力的,是拥有与 Microsoft 的操作系统相同缩写语——XP的极限编程(eXtreme Programming)。极限编程方法论可以说是敏捷联盟中最鲜艳的一面旗帜,也是被研究、尝试、应用、赞扬、批判最多的一种方法论,也是相对来说最成熟的一种。
这一被誉为“黑客文化”的方法论的雏形最初形成于1996—1999年间,Kent Beck、Ward Cunninggham、Ron Jeffrey 在开发 C3 项目(Chrysler Comprehensive Compensation)的实践中总结出了 XP 的基本元素。在此之后,Kent Beck 和他的一些好朋友们一起在实践中完善提高,终于形成了极限编程方法论。
# 解析极限编程
那么什么是 XP 呢?XP 是一种轻量(敏捷)、高效、低风险、柔性、可预测、科学而且充满乐趣的软件开发方式。与其他方法论相比,其最大的不同在于:
- 在更短的周期内,更早地提供具体、持续的反馈信息。
- 在迭代的进行计划编制,首先在最开始迅速生成一个总体计划,然后在整个项目开发过程中不断的发展它。
- 依赖于自动测试程序来监控开发进度,并及早地捕获缺陷。
- 依赖于口头交流、测试和源程序进行沟通。
- 倡导持续的演化式设计。
- 依赖于开发团队内部的紧密协作。
- 尽可能达到程序员短期利益和项目长期利益的平衡。
Kent Beck 曾经说过“开车”就是一个 XP 的范例,即使看上去进行得很顺利,也不要把视线从公路上移开,因为路况的变化,将使得你必须随时做出一些这样那样的调整。而在软件项目中,客户就是司机,他们也没有办法确切地知道软件应该做什么,因此程序员就需要向客户提供方向盘,并且告知我们现在的位置。
XP 包括写什么呢?如图,XP 由价值观、原则、实践和行为四个部分组成,它们彼此相互依赖、关联, 并通过行为贯穿于整个生命期。

# 四大价值观
XP 的核心是其总结的沟通(Communication)、简单(Simplicity)、反馈(Feedback)、勇气(Courage)四大价值观,它们是XP的基础,也是XP的灵魂。
此外还扩展了第五个价值观:谦逊(Modesty)。 XP用“沟通、简单、反馈、勇气和谦逊”来减轻开发压力和包袱;XP 精神可以启发我们如何学习和对待快速变化、多样的开发技术。成功学习 XP 的关键,是用“沟通、简单、反馈、勇气和谦逊”的态度来对待 XP;轻松愉快地来感受 XP 的实践思想;自己认真实践后,通过对真实反馈的分析,来决定 XP 对自己的价值;有勇气接受它,或改进它。
# 沟通
通常程序员给人留下的印象就是“内向、不善言谈”,然后项目中的许多问题就出在这些缺乏沟通的开发人员身上。经常由于某个程序员做出了一个设计决定,但是却不能及时地通知大家,结果使得大家在协作与配合上出现了很多的麻烦,而在传统的方法论中,并不在意这种口头沟通不畅的问题,而是希望借助于完善的流程和面面俱到的文档、报表、计划来替代,但是这同时又引入了效率不高的新问题。
XP 方法论认为,如果小组成员之间无法做到持续的、无间断的交流,那么协作就无从谈起,从这个角度能够发现,通过文档、报表等人工制品进行交流面临巨大的局限性。因此,XP 组合了诸如对编程这样的最佳实践,鼓励大家进行口头交流,通过交流解决问题,提高效率。
# 简单
XP 方法论提倡在工作中秉承“够用就好”的思路,也就是尽量地简单化,只要今天够用就行,不考虑明天会发现的新问题。这一点看上去十分容易,但是要真正做到保持简单的工作其实很难的。因为在传统的开发方法中,都要求大家对未来做一些预先规划,以便对今后可能发生的变化预留一些扩展的空间。
正如对传统开发方法的认识一样,许多开发人员也会质疑 XP,保持系统的扩展性很重要,如果都保持简单,那么如何使得系统能够有良好的扩展性呢?其实不然,保持简单的理由有两个:
- 开发小组在开发时所做的规划,并无法保证其符合客户需要的,因此做的大部分工作都将落空,使得开发过程中重复的、没有必要的工作增加,导致整体效率降低。
- 另外,在 XP 中提倡时刻对代码进行重构,一直保持其良好的结构与可扩展性。也就是说,可扩展性和为明天设计并不是同一个概念,XP 是反对为明天考虑而工作,并不是说代码要失去扩展性
而且简单和沟通之间还有一种相对微妙的相互支持关系。当一个团队之间,沟通的越多,那么就越容易明白哪些工作需要做,哪些工作不需要做。另一方面,系统越简单,需要沟通的内容也就越少,沟通也将更加全面。
# 反馈
是什么原因使得我们的客户、管理层这么不理解开发团队?为什么客户、管理层总是喜欢给我们一个死亡之旅?究其症结,就是开发的过程中缺乏必要的反馈。在许许多多项目中,当开发团队经历过了需求分析阶段之后,在相当长的一段时间内,是没有任何反馈信息的。整个开发过程对于客户和管理层而言就像一个黑盒子,进度完全是不可见的。
而且在项目的过程中,这样的现象不仅出现在开发团队与客户、管理层之间,还包括在开发团队内部。这一切问题都需要我们更加注重反馈。,反馈对于任何软件项目的成功都是至关重要的,而在 XP 方法论中则更进一步,通过持续、明确的反馈来暴露软件状态的问题。具体而言就是:
- 在开发团队内部,通过提前编写单元测试代码,时时反馈代码的问题与进展。
- 在开发过程中,还应该加强集成工作,做到持续集成,使得每一次增量都是一个可执行的工作版本,也就是逐渐是软件长大,整个过程中,应该通过向客户和管理层演示这些可运行的版本,以便及早地反馈,及早地发现问题。
同时,我们也会发现反馈与沟通也有着良好的配合,及时和良好的反馈有助于沟通。而简单的系统更有利于测试和反馈。
# 勇气
在应用 XP 方法论时,我们每时每刻都在应对变化:由于沟通良好,因此会有更多需求变更的机会;由于时刻保持系统的简单,因此新的变化会带来一些重新开发的需要;由于反馈及时,因此会有更多中间打断你的思路的新需求。
总之这一切,使得你立刻处于变化之中,因此这时就需要你有勇气来面对快速开发,面对可能的重新开发。也许你会觉得,为什么要让我们的开发变得如此零乱,但是其实这些变化若你不让它早暴露,那么它就会迟一些出现,并不会因此消亡,因此,XP 方法论让它们早出现、早解决,是实现“小步快走”开发节奏的好办法。
也就是 XP 方法论要求开发人员穿上强大、自动测试的盔甲,勇往直前,在重构、编码规范的支持下,有目的地快速开发。
勇气可以来源于沟通,因为它使得高风险、高回报的试验成为可能;勇气可以来源于简单,因为面对简单的系统,更容易鼓起勇气;勇气可以来源于反馈,因为你可以及时获得每一步前进的状态(自动测试),会使得你更勇于重构代码。
# 四大价值观之外
在这四大价值观之下,隐藏着一个更深刻的东西,那就是尊重。因为这一切都建立在团队成员之间的相互关心、相互理解的基础之上。
# 5 个原则
# 快速反馈
及时地、快速地获取反馈,并将所学到的知识尽快地投入到系统中去。也就是指开发人员应该通过较短的反馈循环迅速地了解现在的产品是否满足了客户的需求。这也是对反馈这一价值观的进一步补充。
# 简单性假设
类似地,简单性假设原则是对简单这一价值观的进一步补充。这一原则要求开发人员将每个问题都看得十分容易解决,也就是说只为本次迭代考虑,不去想未来可能需要什么,相信具有将来必要时增加系统复杂性的能力,也就是号召大家出色地完成今天的任务。
# 逐步修改
就像开车打方向盘一样,不要一次做出很大的改变,那样将会使得可控性变差,更适合的方法是进行微调。而在软件开发中,这样的道理同样适用,任何问题都应该通过一系列能够带来差异的微小改动来解决。
# 提倡更改
在软件开发过程中,最好的办法是在解决最重要问题时,保留最多选项的那个。也就是说,尽量为下一次修改做好准备。
# 优质工作
在实践中,经常看到许多开发人员喜欢将一些细小的问题留待后面解决。例如,界面的按钮有一些不平整,由于不影响使用就先不管;某一两个成员函数暂时没用就不先写等。这就是一种工作拖泥带水的现象,这样的坏习惯一旦养成,必然使得代码质量大打折扣。
而在 XP 方法论中,贯彻的是“小步快走”的开发原则,因此工作质量决不可打折扣,通常采用测试先行的编码方式来提供支持。
# 13 个最佳实践
在 XP 中,集成了 13 个最佳实践,有趣的是,它们没有一个是创新的概念,大多数概念和编程一样老。其主要创新点在于提供一种良好的思路,将这些最佳实践结合在一起,并且确保尽可能彻底地执行它们,使得它们能够在最大程度上相互支持,紧接下来,我们就对每一种最佳实践进行一番了解。
# 计划游戏
计划游戏的主要思想就是先快速地制定一份概要的计划,然后随着项目细节的不断清晰,再逐步完善这份计划。计划游戏产生的结果是一套用户故事及后续的一两次迭代的概要计划。
“客户负责业务决策,开发团队负责技术决策”是计划游戏获得成功的前提条件。也就是说,系统的范围、下一次迭代的发布时间、用户故事的优先级应该由客户决定;而每个用户故事所需的开发时间、不同技术的成本、如何组建团队、每个用户故事的风险,以及具体的开发顺序应该由开发团队决定。
好了,明白这些就可以进行计划游戏了。首先客户和开发人员坐在同一间屋子里,每个人都准备一支笔、一些用于记录用户故事的纸片,最好再准备一个白板,就可以开始了。
- 客户编写故事:由客户谈论系统应该完成什么功能,然后用通俗的自然语言,使用自己的语汇,将其写在卡片上,这也就是用户故事。
- 开发人员进行估算:首先客户按优先级将用户故事分成必须要有、希望有、如果有更好三类,然后开发人员对每个用户故事进行估算,先从高优先级开始估算。如果在估算的时候,感到有一些故事太大,不容易进行估算,或者是估算的结果超过 2人/周,那么就应该对其进行分解,拆成 2 个或者多个小故事。
- 确定迭代的周期:接下来就是确定本次迭代的时间周期,这可以根据实际的情况进行确定,不过最佳的迭代周期是 2~3 周。有了迭代的时间之后,再结合参与的开发人数,算出可以完成的工作量总数。然后根据估算的结果,与客户协商,挑出时间上够、优先级合适的用户故事组合,形成计划。
# 小型发布
XP 方法论秉承的是“持续集成,小步快走”的哲学,也就是说每一次发布的版本应该尽可能的小,当然前提条件是每个版本有足够的商业价值,值得发布。
由于小型发布可以使得集成更频繁,客户获得的中间结果也越频繁,反馈也就越频繁,客户就能够实时地了解项目的进展情况,从而提出更多的意见,以便在下一次迭代中计划进去。以实现更高的客户满意度。
# 隐喻
相对而言,隐喻这一个最佳实践是最令人费解的。什么是隐喻呢?根据词典中的解释是:“一种语言的表达手段,它用来暗示字面意义不相似的事物之间的相似之处”。那么这在软件开发中又有什么用呢?总结而言,常常用于四个方面。
- 寻求共识:也就是鼓励开发人员在寻求问题共识时,可以借用一些沟通双方都比较熟悉的事物来做类比,从而帮助大家更好地理解解决方案的关键结构,也就是更好地理解系统是什么、能做什么。
- 发明共享词汇:通过隐喻,有助于提出一个用来表示对象、对象间的关系通用名称。例如,策略模式(用来表示可以实现多种不同策略的设计模式)、工厂模式(用来表示可以按需“生产”出所需类得设计模式)等。
- 创新的武器:有的时候,可以借助其他东西来找到解决问题的新途径。例如:“我们可以将工作流看做一个生产线”。
- 描述体系结构:体系结构是比较抽象的,引入隐喻能够大大减轻理解的复杂度。例如管道体系结构就是指两个构件之间通过一条传递消息的“管道”进行通信。
当然,如果能够找到合适的隐喻是十分快乐的,但并不是每种情况都可以找到恰当的隐喻,你也没有必要强求
# 简单设计
强调简单设计的价值观,引出了简单性假设原则,落到实处就是“简单设计”实践。这个实践看上去似乎很容易理解,但却又经常被误解,许多批评者就指责 XP 忽略设计是不正确的。其实,XP 的简单设计实践并不是要忽略设计,而且认为设计不应该在编码之前一次性完成,因为那样只能建立在“情况不会发生变化”或者“我们可以预见所有的变化”之类的谎言的基础上的。
Kent Beck 概念中简单设计是这样的:
- 能够通过所有的测试程序。
- 没有包括任何重复的代码。
- 清楚地表现了程序员赋予的所有意图。
- 包括尽可能少的类和方法
- 他认为要想保持设计简单的系统,需要具备简单思考的能力,拥有理解代码和修改的勇气,以及为了消除代码的“坏味道”而定期重构的习惯。
- 那么如何开始进行简单的设计呢?XP 实践者们也总结也一些具体的、可操作的思考方法。
- 首先写测试代码:具体将在后面详细描述。
- 保持每个类只负责一件事:SRP(单一职责原则)是面向对象设计的基础原则之一。
- 使用 Demeter(迪米特)法则:迪米特法则,也称为 LoD 法则、最少知识原则。也就是指一个对象应当对其他对象尽可能少地了解。用隐喻的方法来解释的话就是“只与你直接的朋友通信”、“不要和陌生人说话”。
- 使用 CRC 卡片进行探索。
# 测试先行/测试驱动开发
当我第一次看到“测试先行”这个概念的时候,我的第一感觉就是不解,陷入了“程序都还没有写出来,测试什么呀?”的迷思。我开始天马行空地寻求相关的隐喻,终于找到了能够启发我的工匠,首先,我们来看看两个不同的工匠是如何工作的吧。
- 工匠一:先拉上一根水平线,砌每一块砖时,都与这跟水平线进行比较,使得每一块砖都保持水平。
- 工匠二:先将一排砖都砌完,然后再拉上一根水平线,看看哪些砖有问题,对有问题的砖进行适当的调整。
你会选择哪种工作方法呢?你一定会骂工匠二笨吧!这样多浪费时间呀!然而你自己想想,你平时在编写程序的时候又是怎么做的呢?我们就是按工匠二的方法在工作呀!甚至有时候比工匠二还笨,是整面墙都砌完了,直接进行“集成测试”,经常让整面的墙倒塌。看到这里,你还会觉得自己的方法高明吗?这个连工匠都明白的道理,自己却画地为牢呀。
不仅我们没有采用工匠一的工作方法,甚至有的时候程序员会以“开发工作太紧张”为理由,而忽略测试工作。但这样却导致了一个恶性循环,越是没有空编写测试程序,代码的效率与质量越差,花在找 Bug、解决 Bug 的时间也越来越多,实际产能大打降低。由于产能降低了,因此时间更紧张,压力更大。你想想,为什么不拉上一根水平线呢?难道,我们不能够将后面浪费的时间花在单元测试上,使得我们的程序一开始就更健壮,更加易于修改吗?不过,编写测试程序当然要比拉一条水平线难道多,所以我们需要引入“自动化测试工具”,免费的 xUnit 测试框架就是你最佳的选择。
为了鼓励程序员原意甚至喜欢在编写程序之前编写测试代码,XP 方法论还提供了许多有说服力的理由。
- 如果你已经保持了简单的设计,那么编写测试代码根本不难。
- 如果你在结对编程,那么如果你想出一个好的测试代码,那么你的伙伴一定行。
- 当所有的测试都通过的时候,你再也不会担心所写的代码今后会“暗箭伤人”,那种感觉是相当棒的。
- 当你的客户看到所有的测试都通过的时候,会对程序充满前所未有的信心。
- 当你需要进行重构时,测试代码会给你带来更大的勇气,因为你要测试是否重构成功只需要一个按钮。
测试先行是 XP 方法论中一个十分重要的最佳实践,并且其中所蕴含的知识与方法也十分丰富。
# 重构
重构时一种对代码进行改进而不影响功能实现的技术,XP 需要开发人员在闻到代码的坏味道时,有重构代码的勇气。重构的目的是降低变化引发的风险,使得代码优化更加容易。通常重构发生在两种情况之下。
- 实现某个特性之前:尝试改变现有的代码结构,以使得实现新的特性更加容易。
- 实现某个特性之后:检查刚刚写完的代码后,认真检查一下,看是否能够进行简化。
在《重构》一书中,作者 Martin Fowler 提示我们:在考虑重构时,应该要养成编写并经常运行测试代码的习惯;要先编写代码,再进行重构;把每一次增加功能都当做一次重构的好时机;将每一个纠正错误当做一次重构的重要时机。同时,该书中也列出大量需要重构的情况和重构方法。
最后类似地,给还没有足够勇气进行重构的程序员打几剂强心针:
- XP 提倡集体代码所有制,因此你可以大胆地在任何需要修改的地方做改动。
- 由于在 XP 项目组中有完整的编码标准,因此在重构前无须重新定义格式。
- 在重构中遇到困难,和你结对编程的伙伴能够为你提供有效的帮助。
- 简单的设计,会给重构带来很大的帮助。
- 测试先行让你拥有了一个有效的检验器,随时运行一下就知道你重构的工作是否带来了影响。
- 由于 XP 在持续集成,因此你重构所带来的破坏很快就能够暴露,并且得以解决。
重构技术是对简单性设计的一个良好的补充,也是 XP 中重视“优质工作”的体现,这也是优秀的程序员必备的一项技能。
# 结对编程
“什么!两个人坐在一起写程序?那岂不是对人力的巨大浪费吗?而且我在工作时可不喜欢有一个人坐在边上当检察官。”是的,正如这里列举出来的问题一样,结对编程技术还是被很多人质疑的。
不过,自从 20 世纪 60 年代,就有类似的实践在进行,长期以来的研究结果却给出了另外一番景象,那就是结对编程的效率反而比单独编程更高。一开始虽然会牺牲一些速度,但慢慢的,开发速度会逐渐加快,究其原因,主要是结对编程大打降低了沟通的成本,提供了工作的质量,具体表现在:
- 所有的设计决策确保不是由一个人做出的。
- 系统的任何一个部分都肯定至少有 2 个人以上熟悉。
- 几乎不可能有 2 个人都忽略的测试项或者其他任务
- 结对组合的动态性,是一个企业知识管理的好途径。
- 代码总是能够保证被评审过。
- 而且 XP 方法论集成的其他最佳实践也能够使得结对编程更加容易进行:
- 编码标准可以消除一些无谓的分歧。
- 隐喻可以帮助结对伙伴更好地沟通。
- 简单设计可以使得结对伙伴更了解他们所从事的工作。
结对编程技术被誉为 XP 保持工作质量、强调人文主义的一个典型的实践,应用得当还能够使得开发团队之前的协作更加流畅、知识交流与共享更加频繁,团队的稳定性也会更加稳固。
# 集体代码所有制
由于 XP 方法论鼓励团队进行结对编程,而且认为结对编程的组合应该动态地搭配,根据任务的不同、专业技能的不同进行最优组合。由于每个人都肯定会遇到不同的代码,所以代码的所有制就不再适合于私有,因为那样会给修改工作带来巨大的不便。
也就是说,团队中的每个成员都拥有对代码进行改进的权利,每个人都拥有全部代码,也都需要对全部代码负责。同时,XP 强调代码是谁破坏的(也就是修改后发生问题),就应该由谁来修复。
由于在 XP 中,有一些与之匹配的最佳实践,因此你并无须担心采用集体代码所有制会让你的代码变得越来越乱:
- 由于在 XP 项目中,集成工作是一件经常性得工作,因此当有人修改代码而带来了集成的问题,会在很快的时间内被发现。
- 由于每一个类都会有一个测试代码,因此不论谁修改了代码,都需要运行这个测试代码,这样偶然性的破坏发生的概率将很小。
- 由于每一个代码的修改就是通过了结对的两个程序员共同思考,因此通常做出的修改都是对系统有益的。
- 由于大家都坚持了相同的编码标准,因此代码的可读性、可修改性都会比较好,而且还能够避免由于命名法、缩进等小问题引发经常性得代码修改。
集成代码所有制是 XP 与其他敏捷方法的一个较大不同,也是从另一个侧面体现了 XP 中蕴含的很深厚的编码情节。
# 持续集成
在前面谈到小型发布、重构、结对编程、集体代码所有制等最佳实践的时候,我们多次看到“持续集成”的身影,可以说持续集成是对这些最佳实践的基本支撑条件。
可能大家会对持续集成与小型发布代表的意思混淆不清,其实小型发布是指在开发周期经常发布中间版本,而持续集成的含义则是要求 XP 团队每天尽可能多次地做代码集成,每次都在确保系统运行的单元测试通过之后进行。
这样,就可以及早地暴露、消除由于重构、集体代码所有制所引入的错误,从而减少解决问题的痛苦
要在开发过程中做到持续集成并不容易,首先需要养成这个习惯。而且集成工作往往是十分枯燥、烦琐的,因此适当地引入每日集成工具是十分必要的。XP 建议大家首先使用配置管理服务器将代码管理起来,然后使用 Ant 或 Nant 等 XP 工具,编写集成脚本,调用 xUint 等测试框架,这样就可以实现每当程序员将代码 Check in 到配置服务器上时,Ant 就会自动完成编译和集成,并调用测试代码完成相应的测试工作。
# 每周工作 40 小时/可持续的速度
这是最让开发人员开心的、管理者反对的一个最佳实践了,加班、再加班早已成为开发人员的家常便饭,也是管理者最常使用的一种策略,而 XP 方法论认为,加班最终会扼杀团队的积极性,最终导致项目失败,这也充分体现了 XP 方法关注人的因素比关注过程的因素更多一些。
Kent Beck 认为开发人员即使能够工作更长的时间,他们也不该这样做,因为这样做会使他们更容易厌倦编程工作,从而产生一些影响他们效能的其他问题。因此,每周工作 40 小时是一种顺势行为,是一种规律。其实对于开发人员和管理者来说,违反这种规律是不值得的。
- 开发人员:如果不懂得休息,那么就无法将自己的节奏调整到最佳状态,那么就会带来很大的负面影响。而且在精神不集中的状态下,开发质量也得不到保证。
- 管理者:也许这可以称得上“第二种人月神话”,那就是你不得不通过延长每天的工作时间来获得更多的人月。这是因为,每个开发人员的工作精力是有限的,不可能无限增长,在精力不足的时候,不仅写出来的代码质量没有保障,而且还可能为项目带来退步的效果。因此采用加班的方式并不是一个理性的方式,是得不偿失的。
不过有一点是需要解释的,“每周工作 40 小时”中的 40 不是一个绝对数,它所代表的意思是团队应该保证按照“正常的时间”进行工作。那么如何做到这一点呢?
- 首先,定义符合你团队情况的“正常工作时间”。
- 其次,逐步将工作时间调整到“正常工作时间”。
- 再次,除非你的时间计划一团糟,否则不应该在时间妥协。
- 最后,鼓起勇气,制定一个合情合理的时间表。
正如米卢说过的“享受足球”一样,同样地,每一个开发人员应该做到“享受编程”,那么“每周工作 40 小时”就是你的起点。
团队只有持久才有获胜的希望。他们以能够长期维持的速度努力工作,他们保存精力,他们把项目看作是马拉松长跑,而不是全速短跑。
# 现场客户
为了保证开发出来的结果与客户的预想接近,XP 方法论认为最重要的需要将客户请到开发现场。就像计划游戏中提到过的,在 XP 项目中,应该时刻保证客户负责业务决策,开发团队负责技术决策。因此,在项目中有客户在现场明确用户故事,并做出相应的业务决策,对于 XP 项目而言有着十分重要的意义。
也许有人会问,客户提交了用户故事之后不就完成工作了吗?其实很多尝试过用户故事的团队都会发现其太过简单,包含的信息量极少,XP方法论不会不了解,因此,不会把用户故事当做开发人员交付代码的唯一指示。用户故事只是一个起点,后面的细节还需要开发人员与客户之间建立起来的良好沟通来补充。
作为一名有经验的开发人员,绝对不会对现场客户的价值产生任何怀疑,但是都会觉得想要实现现场客户十分困难。要实现这一点,需要对客户进行沟通,让其明白,想对于开发团队,项目成功对于客户而言更为重要。而现场客户则是保障项目成功的一个重要措施,想想在你装修房子的时候,你是不是常常在充当现场客户的角色呢?其实这隐喻就是让客户理解现场客户重要性最好的突破口。
其实现场客户在具体实施时,也不是一定需要客户一直和开发团队在一起,而是在开发团队应该和客户能够随时沟通,可以是面谈,可以是在线聊天,可以是电话,当然面谈是必不可少的。其中的关键是当开发人员需要客户做出业务决策是,需要进一步了解业务细节时能够随时找到相应的客户。
不过,也有一些项目是可以不要现场客户参与的:
- 当开发组织中已经有相关的领域专家时。
- 当做一些探索性工作,而且客户也不知道他想要什么时(例如新产品、新解决方案的研究与开发)。
去尝试吧,现场客户不仅可以争取得到,而且还能使得团队焕然一新,与客户建立起良好的合作与信任。
# 编码标准
编码标准是一个“雅俗共享”的最佳实践,不管是代表重型方法论的 RUP,PSP,还是代表敏捷方法论的 XP,都认为开发团队应该拥有一个编码标准。XP 方法论认为拥有编码标准可以避免团队在一些与开发进度无关的细节问题上发生争论,而且会给重构、结对编程带来很大麻烦。试想如果有人将你上次写的代码的变量命名法做了修改,下次你需要再改这部分代码时,会是一种什么感觉呢?
不过,XP 方法论的编码标准的目的不是创建一个事无巨细的规则表,而是只要能够提供一个确保代码清晰,便于交流的指导方针。
如果你的团队已经拥有编码标准,就可以直接使用它,并在过程中进行完善。如果还没有,那么大家可以先进行编码,然后在过程中逐步总结出编码规则,边做边形成。当然除了这种文字规范以外,还可以采用一些如自动格式化代码工具之类的方法进行代码规范。,事实上,你只需要很好地贯彻执行其他的实践并且进行沟通,编码标准会很容易地浮现出来。
# 配合是关键
有句经典名言“1+1 > 2 ”最适合表达 XP 的观点,Kent Beck 认为 XP 方法论的最大价值在于在项目中融会贯通地运用12个最佳实践,而非单独地使用。你当然可以使用其中的一些实践,但这并不意味着你就运用了 XP 方法论。XP 方法论真正能够发挥其效能,就必须完整地运用12个实践。

# 微服务十二要素宣言
# 概述
如今,软件通常会作为一种服务来交付,它们被称为网络应用程序,或软件即服务(SaaS)。12-Factor 为构建如下的 SaaS 应用提供了方法论:
- 使用标准化流程自动配置,从而使新的开发者花费最少的学习成本加入这个项目。
- 和操作系统之间尽可能的划清界限,在各个系统中提供最大的可移植性。
- 适合部署在现代的云计算平台,从而在服务器和系统管理方面节省资源。
- 将开发环境和生产环境的差异降至最低,并使用持续交付实施敏捷开发。
- 可以在工具、架构和开发流程不发生明显变化的前提下实现扩展。
- 这套理论适用于任意语言和后端服务(数据库、消息队列、缓存等)开发的应用程序。
# 背景
本文的贡献者者参与过数以百计的应用程序的开发和部署,并通过 Heroku 平台间接见证了数十万应用程序的开发,运作以及扩展的过程。
本文综合了我们关于 SaaS 应用几乎所有的经验和智慧,是开发此类应用的理想实践标准,并特别关注于应用程序如何保持良性成长,开发者之间如何进行有效的代码协作,以及如何 避免软件污染 。
我们的初衷是分享在现代软件开发过程中发现的一些系统性问题,并加深对这些问题的认识。我们提供了讨论这些问题时所需的共享词汇,同时使用相关术语给出一套针对这些问题的广义解决方案。本文格式的灵感来自于 Martin Fowler 的书籍: 《企业应用架构模式》(Patterns of Enterprise Application Architecture), 《重构:改善既有代码的设计》(Refactoring) 。
# 云原生应用的 12 要素
# 基准代码
一份基准代码(Codebase),多份部署(deploy)
12-Factor 应用通常会使用版本控制系统加以管理,如 Git, Mercurial, Subversion。一份用来跟踪代码所有修订版本的数据库被称作 代码库(code repository, code repo, repo)。
在类似 SVN 这样的集中式版本控制系统中,基准代码 就是指控制系统中的这一份代码库;而在 Git 那样的分布式版本控制系统中,基准代码 则是指最上游的那份代码库。

基准代码和应用之间总是保持一一对应的关系:
- 一旦有多个基准代码,就不能称为一个应用,而是一个分布式系统。分布式系统中的每一个组件都是一个应用,每一个应用可以分别使用 12-Factor 进行开发。
- 多个应用共享一份基准代码是有悖于 12-Factor 原则的。解决方案是将共享的代码拆分为独立的类库,然后使用 依赖管理 策略去加载它们。
尽管每个应用只对应一份基准代码,但可以同时存在多份部署。每份 部署 相当于运行了一个应用的实例。通常会有一个生产环境,一个或多个预发布环境。此外,每个开发人员都会在自己本地环境运行一个应用实例,这些都相当于一份部署。
所有部署的基准代码相同,但每份部署可以使用其不同的版本。比如,开发人员可能有一些提交还没有同步至预发布环境;预发布环境也有一些提交没有同步至生产环境。但它们都共享一份基准代码,我们就认为它们只是相同应用的不同部署而已。
# 依赖
显式声明依赖关系( dependency )
大多数编程语言都会提供一个打包系统,用来为各个类库提供打包服务,就像 Perl 的 CPAN 或是 Ruby 的 Rubygems 。通过打包系统安装的类库可以是系统级的(称之为 “site packages”),或仅供某个应用程序使用,部署在相应的目录中(称之为 “vendoring” 或 “bunding”)。
12-Factor 规则下的应用程序不会隐式依赖系统级的类库。 它一定通过 依赖清单 ,确切地声明所有依赖项。此外,在运行过程中通过 依赖隔离 工具来确保程序不会调用系统中存在但清单中未声明的依赖项。这一做法会统一应用到生产和开发环境。
例如, Ruby 的 Bundler 使用 Gemfile 作为依赖项声明清单,使用 bundle exec 来进行依赖隔离。Python 中则可分别使用两种工具 – Pip 用作依赖声明, Virtualenv 用作依赖隔离。甚至 C 语言也有类似工具, Autoconf 用作依赖声明,静态链接库用作依赖隔离。无论用什么工具,依赖声明和依赖隔离必须一起使用,否则无法满足 12-Factor 规范。
显式声明依赖的优点之一是为新进开发者简化了环境配置流程。新进开发者可以检出应用程序的基准代码,安装编程语言环境和它对应的依赖管理工具,只需通过一个 构建命令 来安装所有的依赖项,即可开始工作。例如,Ruby/Bundler 下使用 bundle install,而 Clojure/Leiningen 则是 lein deps。
12-Factor 应用同样不会隐式依赖某些系统工具,如 ImageMagick 或是 curl。即使这些工具存在于几乎所有系统,但终究无法保证所有未来的系统都能支持应用顺利运行,或是能够和应用兼容。如果应用必须使用到某些系统工具,那么这些工具应该被包含在应用之中。
# 配置
在环境中存储配置
通常,应用的 配置 在不同 部署 (预发布、生产环境、开发环境等等) 间会有很大差异。这其中包括:
- 数据库,Memcached,以及其他 后端服务 的配置
- 第三方服务的证书,如 Amazon S3、Twitter 等
- 每份部署特有的配置,如域名等
有些应用在代码中使用常量保存配置,这与 12-Factor 所要求的代码和配置严格分离显然大相径庭。配置文件在各部署间存在大幅差异,代码却完全一致。
判断一个应用是否正确地将配置排除在代码之外,一个简单的方法是看该应用的基准代码是否可以立刻开源,而不用担心会暴露任何敏感的信息。
要指出的是,这里定义的 “配置” 并不包括应用的内部配置,比如 Rails 的 config/routes.rb,或是使用 Spring 时 代码模块间的依赖注入关系 。这类配置在不同部署间不存在差异,所以应该写入代码。
另外一个解决方法是使用配置文件,但不把它们纳入版本控制系统,就像 Rails 的 config/database.yml 。这相对于在代码中使用常量已经是长足进步,但仍然有缺点:总是会不小心将配置文件签入了代码库;配置文件的可能会分散在不同的目录,并有着不同的格式,这让找出一个地方来统一管理所有配置变的不太现实。更糟的是,这些格式通常是语言或框架特定的。
12-Factor 推荐将应用的配置存储于 环境变量 中( env vars, env )。环境变量可以非常方便地在不同的部署间做修改,却不动一行代码;与配置文件不同,不小心把它们签入代码库的概率微乎其微;与一些传统的解决配置问题的机制(比如 Java 的属性配置文件)相比,环境变量与语言和系统无关。
配置管理的另一个方面是分组。有时应用会将配置按照特定部署进行分组(或叫做 “环境”),例如 Rails 中的 development,test, 和 production 环境。这种方法无法轻易扩展:更多部署意味着更多新的环境,例如 staging 或 qa。 随着项目的不断深入,开发人员可能还会添加他们自己的环境,比如 joes-staging ,这将导致各种配置组合的激增,从而给管理部署增加了很多不确定因素。
12-Factor 应用中,环境变量的粒度要足够小,且相对独立。它们永远也不会组合成一个所谓的 “环境”,而是独立存在于每个部署之中。当应用程序不断扩展,需要更多种类的部署时,这种配置管理方式能够做到平滑过渡。
# 后端服务
把后端服务 (backing services) 当作附加资源
后端服务 是指程序运行所需要的通过网络调用的各种服务,如数据库(MySQL,CouchDB),消息 / 队列系统(RabbitMQ,Beanstalkd),SMTP 邮件发送服务(Postfix),以及缓存系统(Memcached)。
类似数据库的后端服务,通常由部署应用程序的系统管理员一起管理。除了本地服务之外,应用程序有可能使用了第三方发布和管理的服务。示例包括 SMTP(例如 Postmark),数据收集服务(例如 New Relic 或 Loggly),数据存储服务(如 Amazon S3),以及使用 API 访问的服务(例如 Twitter, Google Maps, Last.fm)。
12-Factor 应用不会区别对待本地或第三方服务。 对应用程序而言,两种都是附加资源,通过一个 url 或是其他存储在 配置中的服务定位 / 服务证书来获取数据。12-Factor 应用的任意 部署 ,都应该可以在不进行任何代码改动的情况下,将本地 MySQL 数据库换成第三方服务(例如 Amazon RDS)。类似的,本地 SMTP 服务应该也可以和第三方 SMTP 服务(例如 Postmark )互换。上述 2 个例子中,仅需修改配置中的资源地址。
每个不同的后端服务是一份 资源 。例如,一个 MySQL 数据库是一个资源,两个 MySQL 数据库(用来数据分区)就被当作是 2 个不同的资源。12-Factor 应用将这些数据库都视作 附加资源 ,这些资源和它们附属的部署保持松耦合。

部署可以按需加载或卸载资源。例如,如果应用的数据库服务由于硬件问题出现异常,管理员可以从最近的备份中恢复一个数据库,卸载当前的数据库,然后加载新的数据库 – 整个过程都不需要修改代码。
# 构建,发布,运行
严格分离构建和运行
基准代码 转化为一份部署 (非开发环境) 需要以下三个阶段:
- 构建阶段 是指将代码仓库转化为可执行包的过程。构建时会使用指定版本的代码,获取和打包 依赖项,编译成二进制文件和资源文件。
- 发布阶段 会将构建的结果和当前部署所需 配置 相结合,并能够立刻在运行环境中投入使用。
- 运行阶段 (或者说 “运行时”)是指针对选定的发布版本,在执行环境中启动一系列应用程序 进程。

12-facfor 应用严格区分 构建,发布,运行 这三个步骤。 举例来说,直接修改处于运行状态的代码是非常不可取的做法,因为这些修改很难再同步回构建步骤。
部署工具通常都提供了发布管理工具,最引人注目的功能是退回至较旧的发布版本。比如, Capistrano 将所有发布版本都存储在一个叫 releases 的子目录中,当前的在线版本只需映射至对应的目录即可。该工具的 rollback 命令可以很容易地实现回退版本的功能。
每一个发布版本必须对应一个唯一的发布 ID,例如可以使用发布时的时间戳(2019-07-22-03:19:17),亦或是一个增长的数字(v100)。发布的版本就像一本只能追加的账本,一旦发布就不可修改,任何的变动都应该产生一个新的发布版本。
新的代码在部署之前,需要开发人员触发构建操作。但是,运行阶段不一定需要人为触发,而是可以自动进行。如服务器重启,或是进程管理器重启了一个崩溃的进程。因此,运行阶段应该保持尽可能少的模块,这样假设半夜发生系统故障而开发人员又捉襟见肘也不会引起太大问题。构建阶段是可以相对复杂一些的,因为错误信息能够立刻展示在开发人员面前,从而得到妥善处理。
# 进程
以一个或多个无状态进程运行应用
运行环境中,应用程序通常是以一个和多个 进程 运行的。
最简单的场景中,代码是一个独立的脚本,运行环境是开发人员自己的笔记本电脑,进程由一条命令行(例如 python my_script.py)。另外一个极端情况是,复杂的应用可能会使用很多 进程类型 ,也就是零个或多个进程实例。
12-Factor 应用的进程必须无状态且 无共享 。 任何需要持久化的数据都要存储在 后端服务 内,比如数据库。
内存区域或磁盘空间可以作为进程在做某种事务型操作时的缓存,例如下载一个很大的文件,对其操作并将结果写入数据库的过程。12-Factor 应用根本不用考虑这些缓存的内容是不是可以保留给之后的请求来使用,这是因为应用启动了多种类型的进程,将来的请求多半会由其他进程来服务。即使在只有一个进程的情形下,先前保存的数据(内存或文件系统中)也会因为重启(如代码部署、配置更改、或运行环境将进程调度至另一个物理区域执行)而丢失。
源文件打包工具(Jammit, django-compressor) 使用文件系统来缓存编译过的源文件。12-Factor 应用更倾向于在 构建步骤做此动作 —— 正如 Rails 资源管道 ,而不是在运行阶段。
一些互联网系统依赖于 “粘性 session”, 这是指将用户 session 中的数据缓存至某进程的内存中,并将同一用户的后续请求路由到同一个进程。粘性 session 是 12-Factor 极力反对的。Session 中的数据应该保存在诸如 Memcached 或 Redis 这样的带有过期时间的缓存中。
# 端口绑定
通过端口绑定 (Port binding) 来提供服务
互联网应用有时会运行于服务器的容器之中。例如 PHP 经常作为 Apache HTTP 的一个模块来运行,正如 Java 运行于 Tomcat 。
12-Factor 应用完全自我加载 而不依赖于任何网络服务器就可以创建一个面向网络的服务。互联网应用 通过端口绑定来提供服务 ,并监听发送至该端口的请求。
本地环境中,开发人员通过类似 http://localhost:5000/ 的地址来访问服务。在线上环境中,请求统一发送至公共域名而后路由至绑定了端口的网络进程。
通常的实现思路是,将网络服务器类库通过 依赖声明 载入应用。例如,Python 的 Tornado, Ruby 的 Thin , Java 以及其他基于 JVM 语言的 Jetty。完全由 用户端,确切的说应该是应用的代码,发起请求。和运行环境约定好绑定的端口即可处理这些请求。
HTTP 并不是唯一一个可以由端口绑定提供的服务。其实几乎所有服务器软件都可以通过进程绑定端口来等待请求。例如,使用 XMPP 的 ejabberd , 以及使用 Redis 协议 的 Redis 。
还要指出的是,端口绑定这种方式也意味着一个应用可以成为另外一个应用的 后端服务 ,调用方将服务方提供的相应 URL 当作资源存入 配置 以备将来调用。
# 并发
通过进程模型进行扩展
任何计算机程序,一旦启动,就会生成一个或多个进程。互联网应用采用多种进程运行方式。例如,PHP 进程作为 Apache 的子进程存在,随请求按需启动。Java 进程则采取了相反的方式,在程序启动之初 JVM 就提供了一个超级进程储备了大量的系统资源 (CPU 和内存),并通过多线程实现内部的并发管理。上述 2 个例子中,进程是开发人员可以操作的最小单位。

在 12-factor 应用中,进程是一等公民。12-Factor 应用的进程主要借鉴于 unix 守护进程模型 。开发人员可以运用这个模型去设计应用架构,将不同的工作分配给不同的 进程类型 。例如,HTTP 请求可以交给 web 进程来处理,而常驻的后台工作则交由 worker 进程负责。
这并不包括个别较为特殊的进程,例如通过虚拟机的线程处理并发的内部运算,或是使用诸如 EventMachine, Twisted, Node.js 的异步 / 事件触发模型。但一台独立的虚拟机的扩展有瓶颈(垂直扩展),所以应用程序必须可以在多台物理机器间跨进程工作。
上述进程模型会在系统急需扩展时大放异彩。 12-Factor 应用的进程所具备的无共享,水平分区的特性 意味着添加并发会变得简单而稳妥。这些进程的类型以及每个类型中进程的数量就被称作 进程构成 。
12-Factor 应用的进程 不需要守护进程 或是写入 PID 文件。相反的,应该借助操作系统的进程管理器 (例如 Upstart ,分布式的进程管理云平台,或是类似 Foreman 的工具),来管理 输出流 ,响应崩溃的进程,以及处理用户触发的重启和关闭超级进程的请求
# 易处理
快速启动和优雅终止可最大化健壮性
12-Factor 应用的 进程 是 易处理(disposable)的,意思是说它们可以瞬间开启或停止。 这有利于快速、弹性的伸缩应用,迅速部署变化的 代码 或 配置 ,稳健的部署应用。
进程应当追求 最小启动时间 。 理想状态下,进程从敲下命令到真正启动并等待请求的时间应该只需很短的时间。更少的启动时间提供了更敏捷的 发布 以及扩展过程,此外还增加了健壮性,因为进程管理器可以在授权情形下容易的将进程搬到新的物理机器上。
进程 一旦接收 终止信号(SIGTERM) 就会优雅的终止 。就网络进程而言,优雅终止是指停止监听服务的端口,即拒绝所有新的请求,并继续执行当前已接收的请求,然后退出。此类型的进程所隐含的要求是 HTTP 请求大多都很短 (不会超过几秒钟),而在长时间轮询中,客户端在丢失连接后应该马上尝试重连。
对于 worker 进程来说,优雅终止是指将当前任务退回队列。例如,RabbitMQ 中,worker 可以发送一个 NACK 信号。 Beanstalkd 中,任务终止并退回队列会在 worker 断开时自动触发。有锁机制的系统诸如 Delayed Job 则需要确定释放了系统资源。此类型的进程所隐含的要求是,任务都应该 可重复执行 , 这主要由将结果包装进事务或是使重复操作 幂等 来实现。
进程还应当在面对突然死亡时保持健壮,例如底层硬件故障。虽然这种情况比起优雅终止来说少之又少,但终究有可能发生。一种推荐的方式是使用一个健壮的后端队列,例如 Beanstalkd ,它可以在客户端断开或超时后自动退回任务。无论如何,12-Factor 应用都应该可以设计能够应对意外的、不优雅的终结。Crash-only design 将这种概念转化为 合乎逻辑的理论。
# 开发环境与线上环境等价
尽可能的保持开发,预发布,线上环境相同
从以往经验来看,开发环境(即开发人员的本地 部署)和线上环境(外部用户访问的真实部署)之间存在着很多差异。这些差异表现在以下三个方面:
- 时间差异: 开发人员正在编写的代码可能需要几天,几周,甚至几个月才会上线。
- 人员差异: 开发人员编写代码,运维人员部署代码。
- 工具差异: 开发人员或许使用 Nginx,SQLite,OS X,而线上环境使用 Apache,MySQL 以及 Linux。
12-Factor 应用想要做到 持续部署 就必须缩小本地与线上差异。 再回头看上面所描述的三个差异:
- 缩小时间差异: 开发人员可以几小时,甚至几分钟就部署代码。
- 缩小人员差异: 开发人员不只要编写代码,更应该密切参与部署过程以及代码在线上的表现。
- 缩小工具差异: 尽量保证开发环境以及线上环境的一致性。
将上述总结变为一个表格如下:
| 传统应用 | 12-Factor 应用 | |
|---|---|---|
| 每次部署间隔 | 数周 | 几小时 |
| 开发人员 vs 运维人员 | 不同的人 | 相同的人 |
| 开发环境 vs 线上环境 | 不同 | 尽量接近 |
后端服务 是保持开发与线上等价的重要部分,例如数据库,队列系统,以及缓存。许多语言都提供了简化获取后端服务的类库,例如不同类型服务的 适配器 。下列表格提供了一些例子。
| 类型 | 语言 | 类库 | 适配器 |
|---|---|---|---|
| 数据库 | Ruby/Rails | ActiveRecord | MySQL, PostgreSQL, SQLite |
| 队列 | Python/Django | Celery | RabbitMQ, Beanstalkd, Redis |
| 缓存 | Ruby/Rails | ActiveSupport::Cache | Memory, filesystem, Memcached |
开发人员有时会觉得在本地环境中使用轻量的后端服务具有很强的吸引力,而那些更重量级的健壮的后端服务应该使用在生产环境。例如,本地使用 SQLite 线上使用 PostgreSQL;又如本地缓存在进程内存中而线上存入 Memcached。
12-Factor 应用的开发人员应该反对在不同环境间使用不同的后端服务 ,即使适配器已经可以几乎消除使用上的差异。这是因为,不同的后端服务意味着会突然出现的不兼容,从而导致测试、预发布都正常的代码在线上出现问题。这些错误会给持续部署带来阻力。从应用程序的生命周期来看,消除这种阻力需要花费很大的代价。
与此同时,轻量的本地服务也不像以前那样引人注目。借助于 Homebrew,apt-get 等现代的打包系统,诸如 Memcached、PostgreSQL、RabbitMQ 等后端服务的安装与运行也并不复杂。此外,使用类似 Chef 和 Puppet 的声明式配置工具,结合像 Vagrant 这样轻量的虚拟环境就可以使得开发人员的本地环境与线上环境无限接近。与同步环境和持续部署所带来的益处相比,安装这些系统显然是值得的。
不同后端服务的适配器仍然是有用的,因为它们可以使移植后端服务变得简单。但应用的所有部署,这其中包括开发、预发布以及线上环境,都应该使用同一个后端服务的相同版本。
# 日志
把日志当作事件流
日志 使得应用程序运行的动作变得透明。在基于服务器的环境中,日志通常被写在硬盘的一个文件里,但这只是一种输出格式。
日志应该是 事件流 的汇总,将所有运行中进程和后端服务的输出流按照时间顺序收集起来。尽管在回溯问题时可能需要看很多行,日志最原始的格式确实是一个事件一行。日志没有确定开始和结束,但随着应用在运行会持续的增加。
12-factor 应用本身从不考虑存储自己的输出流。 不应该试图去写或者管理日志文件。相反,每一个运行的进程都会直接的标准输出(stdout)事件流。开发环境中,开发人员可以通过这些数据流,实时在终端看到应用的活动。
在预发布或线上部署中,每个进程的输出流由运行环境截获,并将其他输出流整理在一起,然后一并发送给一个或多个最终的处理程序,用于查看或是长期存档。这些存档路径对于应用来说不可见也不可配置,而是完全交给程序的运行环境管理。类似 Logplex 和 Fluent 的开源工具可以达到这个目的。
这些事件流可以输出至文件,或者在终端实时观察。最重要的,输出流可以发送到 Splunk 这样的日志索引及分析系统,或 Hadoop/Hive 这样的通用数据存储系统。这些系统为查看应用的历史活动提供了强大而灵活的功能,包括:
- 找出过去一段时间特殊的事件。
- 图形化一个大规模的趋势,比如每分钟的请求量。
- 根据用户定义的条件实时触发警报,比如每分钟的报错超过某个警戒线。
# 管理进程
后台管理任务当作一次性进程运行
进程构成(process formation)是指用来处理应用的常规业务(比如处理 web 请求)的一组进程。与此不同,开发人员经常希望执行一些管理或维护应用的一次性任务,例如:
- 运行数据移植(Django 中的 manage.py migrate, Rails 中的 rake db:migrate)。
- 运行一个控制台(也被称为 REPL shell),来执行一些代码或是针对线上数据库做一些检查。大多数语言都通过解释器提供了一个 REPL 工具(python 或 perl) ,或是其他命令(Ruby 使用 irb, Rails 使用 rails console)。
- 运行一些提交到代码仓库的一次性脚本。
一次性管理进程应该和正常的 常驻进程 使用同样的环境。这些管理进程和任何其他的进程一样使用相同的 代码 和 配置 ,基于某个 发布版本 运行。后台管理代码应该随其他应用程序代码一起发布,从而避免同步问题。
所有进程类型应该使用同样的 依赖隔离 技术。例如,如果 Ruby 的 web 进程使用了命令 bundle exec thin start ,那么数据库移植应使用 bundle exec rake db:migrate 。同样的,如果一个 Python 程序使用了 Virtualenv,则需要在运行 Tornado Web 服务器和任何 manage.py 管理进程时引入 bin/python 。
12-factor 尤其青睐那些提供了 REPL shell 的语言,因为那会让运行一次性脚本变得简单。在本地部署中,开发人员直接在命令行使用 shell 命令调用一次性管理进程。在线上部署中,开发人员依旧可以使用 ssh 或是运行环境提供的其他机制来运行这样的进程。
# 无状态应用
# 概述
- 无状态应用: Stateless Application 是指并不会在会话中保存下次会话中去要的客户端数据。 每一个会话都像首次执行一样,不会依赖之前的数据进行响应。
- 有状态的应用: Stateful Application 是指会在会话中保存客户端的数据,并在客户端下一次的请求中来使用那些数据。
# 什么是无状态应用
在无状态应用中,会话数据将会被存储在客户端或者透传给需要的这些数据的服务。在开发离线应用时,这是一个非常重要的的因素。通过这种方式来开发,会话数据将会被存储在终端用户的设备上,例如:当网络不可用时,用户将数据存储在自己的设备上,当网络重新连接时,会话数据将被上传并复制到云中。
在分布式系统中,无状态应用使实现了分布式水平扩展成为可能。当分布式系统中的一个组建是无状态时,能够在出现故障时轻松的重新部署,也能够自由的水平扩展来适应负载。组建之间也能够方便的使用 API 来进行通信。
函数式编程(Functional Programming) 是一种使用非常小的代码段进行软件开发的方法。每一个函数执行的时候都仿佛是首次执行,不依赖于之前的内容。因为所有函数都是无状态的,所以开发人员可以用多种方式来组装函数,而不必担心破坏了依赖关系。
# 为什么需无状态

我们在开发微服务时,一种实现方式是将所有的需要的数据进行透传。比如,当业务上需要从 User 类中获取 country 国家这个属性时,如果不是基于 stateless app,那么初期就会将获取用户身上的 country 散落在各个服务中。一旦 user 类上属性发生变化,例如 country 更加语义的被修改为 countryCode,那么带来的修改量时巨大的,而且很有可能部署之后才发现对其他服务的影响。使用 stateless app,则事情变的简单了,还是上面的例子,每个服务都是通过 API 的参数来获取 country 的 value,即时 user 类中的属性发生的变化,也不会突然波及很多服务。
# 轻应用
# 概述
LAPP (Light App) 即轻应用是一种无需下载、即搜即用(即用即走)的全功能 App,既有媲美甚至超越 native app 的用户体验,又具备 webapp 的可被检索与智能分发的特性,将有效解决优质应用和服务与移动用户需求对接的问题。
# 特点
- 无需下载,即搜即用
以往,开发者付出高昂成本拉动用户下载应用,每隔十天半月还要推送更新版本,一不小心就遭用户卸载。
- 破壳检索,智能分发
开发者开发的应用不再是信息孤岛,里面的内容都可以被索引,这跟原生应用形成明显的差别。在应用商店里,只有用户输入明确的 App 名称,例如 “滴滴打车”,这个应用才能够被分发。而现在,移动搜索中自然表达的所有与打车有关的需求,比如 “我要打车”、“从国贸到雍和宫” 等,都将导向开发者开发的打车类应用,大大增加应用的曝光量和使用率,从源头解决分发难题。
- 功能强大,全能体验
轻应用能够帮应用调起语音、摄像头、定位、存储等手机本地或云端的多种能力,让应用的功能更强大。以好大夫在线轻应用为例,开发者不仅可以设置语音交流模块,还可以调起本地摄像头帮助用户拍摄化验单或患处,从而提供和 Native App 相同甚至更好的体验。
- 订阅推送,沉淀用户
轻应用不仅支持用户搜索时实现调用,还支持用户主动订阅。如果用户有订阅需求并添加应用,相关开发者就能够将用户沉淀下来,并对用户进行持续、精准的信息和服务推送。例如,很多视频类应用的用户有追剧的需求,百度支持用户订阅的功能,只要用户订阅了应用,每当有新剧更新,开发者都可以第一时间通知用户,增强粘性,从而与用户建立起更加稳固牢靠的关系。
# 三大领域
- 媒体: 包括资讯媒体、自媒体
- 工具: 包括办公学习、金融理财、实用查询
- 生活服务: 包括交通、住房、医疗、旅游、娱乐、教育、汽车、美食等
# 禅道简介
# 概述
禅道由青岛易软天创网络科技有限公司开发,国产开源项目管理软件。它集产品管理、项目管理、质量管理、文档管理、组织管理和事务管理于一体,是一款专业的研发项目管理软件,完整覆盖了研发项目管理的核心流程。禅道管理思想注重实效,功能完备丰富,操作简洁高效,界面美观大方,搜索功能强大,统计报表丰富多样,软件架构合理,扩展灵活,有完善的 API 可以调用。禅道,专注研发项目管理!
# 为什么用禅道这个名字
禅和道这两个字含义极其丰富,有宗 教方面的含义,也有文化层面的含义。禅道项目管理软件取其文化含义,期望通过这两个字来传达我们对管理的理解和思考。这个名字是受《编程之道》和《编程之禅》这两本书的启发。英文里面的禅为 Zen,道为 Tao,所以我们软件的英文名字为 zentao。
# 设计理念
禅道项目管理软件的主要管理思想基于国际流行的敏捷项目管理方法 —Scrum。Scrum 方法注重实效,操作性强,非常适合软件研发项目的快速迭代开发。但它只规定了核心的管理框架,还有很多细节流程需要团队自行扩充。禅道在遵循其管理方式基础上,结合国内研发现状,整合了 bug 管理,测试用例管理,发布管理,文档管理等功能,完整的覆盖了软件研发项目的整个生命周期。在禅道软件中,明确的将产品、项目、测试三者概念区分开,产品人员、开发团队、测试人员,三者分立,互相配合,又互相制约,通过需求、任务、bug 来进行交相互动,终通过项目拿到合格的产品。
# 为什么选择禅道
- 禅道是专业的研发项目管理软件,非简单任务管理软件可比。
- 管理思想简洁实效,可以帮助企业实现快速敏捷开发。
- 功能完备,您无需再费心整合若干系统在一起使用。
- 源代码开源开放,有灵活的扩展机制,方便企业使用并二次开发。
- 国产软件,本地支持,操作习惯更符合国人。
- 自主开发的底层框架和前端 UI 框架,健壮稳定,界面美观,交互友好。
- 完善的社区机制,可以获得及时的技术支持和帮助。
- 零投入,相比动辄十几万的商业软件,您选择禅道没有任何风险。
- 禅道支持多种部署方式,可以私有部署,也可以选择云端服务。
- 我们团队一直专注企业管理,持续迭代更新,不断完善软件。
# 主要功能列表
- 产品管理:包括产品、需求、计划、发布、路线图等功能。
- 项目管理:包括项目、任务、团队、版本、燃尽图等功能。
- 质量管理:包括 bug、测试用例、测试任务、测试结果等功能。
- 文档管理:包括产品文档库、项目文档库、自定义文档库等功能。
- 事务管理:包括 todo 管理,我的任务、我的 Bug、我的需求、我的项目等个人事务管理功能。
- 组织管理:包括部门、用户、分组、权限等功能。
- 统计功能:丰富的统计表。
- 搜索功能:强大的搜索,帮助您找到相应的数据。
- 扩展机制:几乎可以对禅道的任何地方进行扩展。
- API 机制:所见皆 API,方便与其他系统集成。
# 安装禅道
# 原版下载
禅道开源版:http://dl.cnezsoft.com/zentao/docker/docker_zentao.zip
数据库配置(可以设置
MYSQL_ROOT_PASSWORD
变量来更改密码):
- 账号: root
- 密码: 123456
可挂载目录
- /app/zentaopms: 该目录为禅道目录,里面包含禅道代码及附件上传目录
- /var/lib/mysql: 该目录为数据库的数据目录
# Compose
解压 docker_zentao.zip 后修改目录名为 build,增加 docker-compose.yml 配置如下
version: '3.1'
services:
zendao:
build: build
restart: always
container_name: zendao
environment:
MYSQL_ROOT_PASSWORD: 123456
ports:
- 80:80
volumes:
- ./app:/app/zentaopms
- ./data:/var/lib/mysql
2
3
4
5
6
7
8
9
10
11
12
13
# 构建镜像
docker-compose build
# 启动容器
docker-compose up -d
2
3
4
# 安装





