可以说,软件工程,至少现在人们通常认为的那样,确实是随着第一个正式确定的软件开发方法而产生的。这种方法(最终在 1976 年被称为瀑布法)让人们开始思考的不仅仅是软件如何工作,或者如何编写代码,而是编写代码的过程需要什么样才能更有效。从那时起,大约有十几种其他方法已经形成,至少在一种情况下,各种敏捷方法的集合中,有近十几种不同的子变体,尽管 Scrum 几乎肯定是最广为人知的,看板可能是紧随其后的。
虽然这些方法正在成长和成熟,但计算能力的提高也最终导致了更新、更有用或更高效的开发模式。面向对象编程(OOP)和函数编程(FP)可能是几十年来主导该领域的原始过程编程范式上最著名的进步。代码集成自动化和推广实践(分别是持续集成和交付)近年来也变得很流行。
在本章中,我们将介绍以下主题:
-
过程方法
-
瀑布
-
敏捷的:
- 并列争球
- 看板
-
发展模式:
- 面向对象编程(OOP)
- 函数式编程(FP)
-
发展做法:
- 连续积分
- 连续交付
在某种程度上,所有开发过程方法都是在一些共同现实的范围内管理开发这一主题的变体:
- 每个人每天只有那么多有用的工作时间可以用于一个项目
- 项目可用的资源是有限的,无论是人员、设备还是资金
- 当项目完成时,有一个最低可接受的质量标准
这有时表示为项目管理的I****罗恩三角:
关于速度点,最主要的关注点是时间。最常见的关注点可能是需要在特定截止日期前完成的项目,或者存在其他一些时间限制,只有通过向团队中添加开发人员(增加成本)或通过抄近路才能克服(降低质量。 预算变化是成本点的一个共同主题。任何花费金钱的东西,无论是以增加开发人员、更新/更快/更好的工具等形式。 减少可用资源/人员会降低速度项目完工和/或最终质量
质量点显然与质量度量有关,质量度量可能包括特定的内部或外部标准,但可能容易包括不太明显的项目,如长期可维护性和对新特性和功能的支持。优先考虑质量至少需要更多的开发时间,降低速度,增加成本。
通常,有效优先级(无论有效值是多少)最多只能给予三角形三个点中的两个,从而产生三种优先级可能性:
- 以质量为代价的快速、廉价的开发
- 快速、高质量的开发,但成本更高
- 高质量、低成本的开发,需要更长的时间才能完成
The Lean Startup Method (or just Lean) is sometimes cited as an alternative process methodology that can overcome the constraints of the Iron Triangle, but is beyond the scope of this book. A reasonable introduction to its concepts can be found at https://www.castsoftware.com/glossary/lean-development.
在本书中,有三种具体的开发过程方法值得深入研究。首先,我们将研究瀑布,以便为两种敏捷方法提供一个参考框架,Scrum 和 Kanban,并且还将研究其他一些方法,至少简要地介绍一下。对其中任何一个的全面讨论都远远超出了本书的范围,但其目的是提供足够的细节来说明它们的重点和优先事项,以及它们的优缺点。至少,这应该提供在任何一种方法中工作时预期的基线,将每种方法的阶段与第 3 章、*系统建模、*中的模型 SDLC 阶段联系起来,以显示发生了什么、何时以及如何发生。
瀑布的起源可能可以追溯到制造业和/或建筑规划。在许多方面,它是规划和实施开发工作的一种非常简单的方法,基本上可以分解为定义和设计要构建的内容、构建、测试和部署。
更正式地说,它是六个独立的阶段,按以下顺序执行:
- 要求
- 分析
- 设计
- 实施
- 测试
- 安装和操作:
这些相位与 SDLC 中的相位顺序相对应。它们非常相似,无论是出于偶然还是设计,目的都是为了实现许多相同的目标。它们的重点可能最好概括为:在将设计交付给开发进行实施之前,设计、记录和定义成功开发所需的一切。在理想的执行中,设计和需求信息将为开发人员提供他们所需的一切,一旦实现开始,项目经理可能会完全放手。
从概念上讲,这种方法有一些优点,如果所有内容都被彻底准确地记录下来,那么开发人员将拥有他们所需要的一切,他们可以完全专注于编写代码来完成需求。作为初始项目规范的一部分,文档已经创建,因此一旦部署了软件,管理最终系统的任何人都将可以访问该文档,其中一些文档甚至可能面向用户并可供他们使用。
如果做得好,它几乎肯定会在实现过程中捕获并允许依赖关系,并且它提供了一个易于跟踪的事件序列。总的来说,该方法很容易理解。这几乎是一种自反式的方法来构建一些东西:决定做什么,计划如何做,做什么,检查所做的是想要的,然后它就完成了。
然而,在实践中,一个好的瀑布计划和执行并非易事,除非执行需求、分析和设计阶段的人员真的很好,或者需要足够的时间(可能需要很多时间)来达成和审查这些细节。这假设需求从一开始都是可识别的,但通常情况并非如此,并且它们不会中途更改,这一情况的发生频率可能比显而易见的要高。由于它首先关注文档,因此在长期应用于大型或复杂系统时,它的速度往往会减慢。毕竟,不断增长的文档集的持续更新需要时间,而且是额外的(和不断增长的)几乎总是需要花费时间来防止无法管理的膨胀蔓延到系统周围的其他支持结构中。
瀑布过程的前三个阶段(需求、分析和设计包括 SDLC 模型的前五个阶段:
- 初步概念/构想
- 概念发展
- 项目管理规划
- 需求分析和定义
- 系统架构与设计
理想情况下,这些将包括这些阶段的任何文档/工件,以及任何系统建模项(第 3 章、系统建模),所有这些都打包好供开发人员使用和参考。通常,这些过程将涉及一名专门的项目规划师,负责与各种利益相关者、架构师等进行沟通和协调,以组装整个过程。
在一个定义良好且管理良好的瀑布式流程中,从这三个阶段产生并交给开发和质量保证部门的工件是组成项目计划的文档或文档集合。这样一个计划可能很长,因为它应该理想地捕获所有在开发中和开发后使用的开发前工作的所有输出:
-
目标和目的(可能是高水平的)
-
已完成的工作的内容和预期:
- 完整的需求细分
- 需要缓解或至少关注的任何风险、问题或依赖性
-
架构、设计和系统模型考虑事项(新结构或对现有结构的更改):
- 逻辑和/或物理体系结构项
- 用例
- 数据结构和流程
- 进程间通信
-
发展计划
-
质量保证/测试计划
-
变更管理计划
-
安装/分配计划
-
退役计划
瀑布过程的实施和测试阶段,除了将项目计划作为起点参考外,可能还将遵循一个简单且非常典型的过程:
- 开发人员编写代码
- 开发人员测试代码(编写和执行单元测试),修复任何功能问题并重新测试,直到完成为止
- 开发人员将完成的代码交给质量保证部门进行进一步测试
- 质量保证测试代码,如果发现问题,将其交还给开发人员
- 已测试/批准的代码将升级到实时系统
这个过程在所有的开发工作和方法中都很常见,除非有明显的偏离,否则稍后将不再提及。
瀑布的安装和运行阶段包含 SDLC 模型的安装/分配和运行/使用和维护阶段。它还可能包括停运阶段,因为这可能被视为一种特殊的运行情况。像实施和测试阶段一样,这些阶段很可能会以一种容易预期的方式再次进展,除了项目计划文档中可能存在的任何相关信息外,实际上没有任何东西可以指示与简单,对这些问题采取常识性方法,因为常识的任何价值都适用于系统环境。
虽然瀑布法通常被认为是一种过时的方法,一种倾向于以过于僵化的方式实施的方法,并且或多或少需要摇滚明星人员长期良好地工作,但只要存在一个或多个条件,它仍然可以工作:
- 准确分析需求和范围,并对其进行全面说明
- 要求和范围在执行期间不会发生重大变化
- 对于方法论而言,系统并不太大或太复杂
- 对于方法论来说,对系统的更改不会太大或太复杂
其中,如果没有政策和程序支持,第一个通常是不可依赖的,而政策和程序支持通常不在开发团队的控制范围之内。考虑到足够长的一段时间,后两者几乎不可避免地是无法克服的,这仅仅是因为系统很少随着时间的推移变得更小或更不复杂,而对更大、更复杂的系统的更改本身往往会变得更大、更复杂。
到 1990 年代初,人们对发展进程的看法发生了巨大变化。瀑布式流程尽管被广泛采用,甚至在美国的政府承包商政策中也得到了广泛采用,但它开始显示出越来越多应用于大型复杂系统的固有缺陷。其他正在使用的非瀑布式方法也开始显示出磨损的迹象,因为它们太重,太容易产生适得其反的微观管理,以及各种其他抱怨和担忧。
因此,围绕开发过程的许多想法开始集中在轻量级、迭代式和管理密集度较低的方法上,这些方法最终结合在《敏捷宣言》和作为其基础的十二项原则上:
- 我们正在发现更好的开发软件的方法,通过这样做和帮助他人这样做。通过这项工作,我们认识到:
- 过程和工具上的个人和交互
- 在综合文档上运行软件
- 合同谈判中的客户协作
- 响应变化胜过遵循计划
也就是说,虽然右边的项目有价值,但我们更看重左边的项目。我们遵循以下原则:
- 我们的首要任务是通过尽早、持续地交付有价值的软件来满足客户。
- 欢迎不断变化的需求,即使是在开发后期。敏捷流程利用变化实现客户的竞争优势。
- 频繁交付工作软件,从几周到几个月不等,优先选择较短的时间范围。
- 在整个项目中,业务人员和开发人员必须每天一起工作。
- 围绕有动力的个人建立项目。为他们提供所需的环境和支持,并信任他们完成工作。
- 在开发团队中传递信息的最有效的方法是面对面交谈。
- 工作软件是进度的主要衡量标准。
- 敏捷过程促进可持续发展。赞助商、开发人员和用户应该能够无限期地保持恒定的速度。
- 持续关注卓越的技术和良好的设计可以增强敏捷性。
- 简单——最大化未完成工作量的艺术至关重要。
- 最好的架构、需求和设计来自自组织团队。
- 团队定期反思如何变得更有效,然后相应地调整和调整其行为。
You may refer to The Agile Manifesto at http://Agilemanifesto.org/ for more details.
在应用程序中,这些原则导致了不同方法中的一些共同特征。在其他仍然被认为是敏捷的方法中可能会有例外,但出于我们的目的,对于这里讨论的特定方法,这些共同特征如下:
- 开发是在一系列迭代中进行的,每个迭代都有一对多的目标
- 每个目标都是最终系统的子集
- 在每次迭代结束时,系统是可部署和可操作的(可能仅针对给定的可操作值)
- 需求是以小块的形式详细定义的,在迭代之前可能根本不会定义需求
Scrum 被认为是最受欢迎的,或者至少是使用最广泛的敏捷开发方法(在敏捷报告的12第年度状态中,有 56%的敏捷方法在使用中),因此可能值得更详细的关注。看板是另一种敏捷方法论,它需要进行一些检查,因为它更接近于本书中主要系统项目的呈现方式。
还有一些其他的敏捷方法,它们至少可以快速查看一下它们可以为开发工作带来的一些特定关注点,或者是它们自己,或者是作为一种混合或与其他方法的混合。
Businesses are also exploring additions and modifications to textbook Agile processes to improve them and address needs that weren't encompassed by the original concept. One such process is the Scaled Agile Framework, which is used to improve the use of Agile processes at larger scales.
Scrum 大体上有以下几个活动部分:
- Scrum 方法以称为 Sprint 的有时间限制的迭代为中心:
- Sprint 被定义为开发团队(有时是涉众)可以商定的固定时间长度
- 每次冲刺的持续时间通常是相同的,但如果有理由,可以临时或永久地(直到下一次更改)更改该持续时间
- 每个 Sprint 都有一组与之相关的特性/功能,开发团队承诺在 Sprint 结束时完成这些特性/功能。
- 每个特性/功能项由用户故事描述。
- 考虑到 Sprint 的持续时间,团队确定他们可以承诺完成哪些用户故事。
- 用户故事的优先级由涉众(通常是产品所有者)决定,但可以协商。
- 团队定期聚会,整理积压工作,包括:
- 估计没有故事的故事的大小
- 向用户情景中添加任务级详细信息
- 如果存在功能依赖性或与大小相关的执行问题,则将故事细分为更小、更易于管理的区块,并获得相关干系人的批准
- 团队在最后回顾 Sprint,寻找进展顺利的事情,或者寻找改进进展不顺利的事情的方法。
- 团队定期开会计划下一次冲刺。
- 团队有一个简短的每日会议(站立会议),目的是揭示自上次更新以来的状态变化。最著名的形式,虽然不是这些会议的唯一形式,但是每位与会者就以下事项发表的简短声明:
- 他们自上次站起来以来所做的工作,完成或其他。
- 他们计划在下次站出来之前做什么。
- 他们正在处理哪些障碍,团队中的其他人可能会提供帮助。
Story sizing should not be based around any sort of time estimate. Doing so tends to discount any assessments of complexity and risk that might be critically important, and implies an expectation that all developers will be able to complete the same story in the same length of time, which is probably not going to be the case. Use story points or t-shirt sizes (extra small, small, medium, large, extra large and extra-extra large) instead!
- 假设一切顺利,从开始到结束,一个典型的冲刺会像这样展开:
- 第一天冲刺启动活动:
- 故事和任务设置在任务板上,无论是真实的还是虚拟的,都处于未启动状态,并按优先顺序排列。
- 团队成员要求编写一个故事,从优先级最高的项目开始。如果一个故事有多人参与,则每个人都会声明与之相关的一项任务。声称的故事在任务板上被移动到进行中状态。
- 第 1 天–冲刺结束前一天:开发和质量保证。
- 每日站立会议(可能在第一天跳过)。
- 开发:
- 任务完成后,任务板上会更新其状态,以显示尽可能多的任务。
- 故事完成后,它们将在开发后移至任务板上的下一个状态。本栏可能是开发完成、QA 就绪,或者考虑到团队结构,任何其他状态描述都是有意义的。
- 如果遇到障碍,会通知Scrum Master,后者负责协助解决障碍问题。如果无法立即解决,则应在任务板上更新被阻止的故事或任务的状态,开发人员将继续处理下一个任务或故事。
- 质量保证活动:
- 如果 QA 人员嵌入到开发团队中,他们的流程通常与开发活动类似,只是他们会从表明开发人员完成项目的任何一列中声明一个故事开始测试
- 测试故事至少应包括该故事的验收标准
- 测试可能(也可能应该)包括不属于验收标准的功能测试
- 故事验收:如果有任何已完成的故事未被验收,则相关利益相关者可以证明并接受或拒绝这些故事。被拒绝的项目可能会返回到正在开发或未启动状态,具体取决于被拒绝的原因,以及如何解决被拒绝的原因。
- 冲刺结束日:
- 演示和接受任何剩余故事。
- 如果之前没有时间这样做,应该为下一次冲刺做准备:
- 冲刺计划,为下一次冲刺准备用户故事
- 待办事项整理,为任何需要这些细节的用户故事准备并定义细节和任务
- 接受剩余的故事。
- 回顾会议-团队聚在一起确定以下内容:
- 为了尝试和利用使其工作良好的因素,什么在 Sprint 中工作良好
- 为了避免将来出现类似情况,哪些措施效果不佳,或者根本不起作用
所有日常活动都围绕一个任务板展开,该任务板提供了一个快速机制,可以方便地查看正在进行的工作以及每个项目的状态:
An example task board, showing stories and tasks in different stages of development The task board shown has more detailed status columns than are technically required—the bare-minimum column set would be Stories, where the top-level stories' details live until they are done, Not Started, and In Progress for tasks that are part of the Sprint, and Done, where tasks (and possibly stories) land when they are complete, tested, and ready for acceptance.
Scrum 的首要任务是关注透明度、检查和自我纠正,以及对不断变化的需求和要求的适应性。任务委员会是该方法透明度方面的一个重要组成部分,使任何有兴趣的人都能一目了然地了解发展努力的现状。但这并没有结束,还有一个角色被称为产品负责人,他是开发团队和系统所有利益相关者之间的中心沟通点。他们参加每日的站立会议,以便近实时地了解进度、障碍等,并代表整个利益相关者群体发言和做出决策。他们还负责在出现产品所有者自己无法解决的问题或担忧时,将团队成员与外部利益相关者联系起来。他们的作用对于确保在向利益相关者提供持续开发工作的透明度和不让开发团队承担持续状态报告的负担之间取得良好平衡至关重要。
Scrum 期望在过程本身中进行大量的自我检查,并鼓励通过优先考虑团队开放性和成员交流,对过程的结果、所创建的软件以及创建过程中使用的实践和规程进行类似的检查,提供一种机制来提高对风险和阻塞条件的可见性,甚至在某种程度上,通过鼓励需要最小努力来实现给定功能目标的用户故事。当关注点或问题出现时,强调即时沟通和能够提供指导和做出决策的人员的随时可用性,可以快速解决这些问题,并且对正在进行的开发过程的干扰程度最小。
从适应变化的角度来看,Scrum 可能是一种更好的方法。想象一下,在两周(或更长)的 Sprint 的第一周,开发团队一直在处理项目的部分内容。这时,涉众级别的某个人突然决定需要对其中一个故事进行更改。有好的、坏的或无关紧要的几个可能的原因使这种改变成为必要。
也许故事背后的功能被认为是过时的,如果故事还没有完成,就不再需要它,那么可以简单地从 Sprint 中删除它,并从 backlog 中提取另一个故事来处理,如果一个可用的故事不比被删除的故事大的话。如果已经有针对这个故事编写的代码,那么可能需要删除它,但就对代码库的影响而言就是这样。如果故事已经完成,那么相关的代码也会被删除,但是没有新的工作(额外的故事)被引入。
如果故事被更改,则其背后的功能将被更改,以更好地满足用户需求或期望,例如,故事将以与被删除时相同的方式从当前 Sprint 中退出,至少是这样。如果有时间重新确定故事的范围并将其插入到 Sprint 中,那么这是可以进行的,否则它将被添加到 backlog 中,从优先级的角度来看,可能位于或接近列表的顶部。
有时,Sprint 可能会脱轨,但该方法也对如何处理这一问题抱有期望。如果一个冲刺由于任何原因不能成功完成,它应该停止,并计划从该冲刺结束的地方开始新的冲刺。
Scrum 的一些有利方面包括:
- Scrum 非常适合于可以分解为小型、快速工作的工作。即使在大型系统中,若对大型代码库的添加或更改可以用简短、省力的故事来描述,那个么 Scrum 也是一个很好的应用过程。
- Scrum 适用于在其领域内拥有合理一致技能的团队。也就是说,例如,如果团队中的所有开发人员都可以在没有重大帮助的情况下使用项目的主语言编写代码,那么与六分之一的团队成员都可以编写代码相比,这是一个更好的团队动态。
同时,由于 Scrum 过程涉及的结构,有一些警告:
-
由于 Sprint 代表了完成一系列故事和功能的承诺,因此即使有很好的理由,更改过程中 Sprint 也是麻烦、耗时和破坏性的。因此,这意味着,无论是谁,在做出可能需要过程中 Sprint 变更的决策时,都需要意识到这些决策的潜在影响。理想情况下,这些决策可能会避免 Sprint 中断性变更,而没有真正、真正好的理由。
-
Scrum 可能无法很好地满足项目或系统级的最后期限,除非团队在整个系统领域及其代码库中拥有相当多的专业知识。迭代截止日期的风险较小,尽管它们可能需要更改或缩小范围,以便在一个迭代一个迭代的基础上交付工作软件。
-
如果团队成员更换每个新的团队成员,特别是如果他们在不同的时间加入团队,开发工作和产出将变得不可预测,这将对团队的可预测能力产生一定影响,直到新的团队名册有时间适应。Scrum 对这些变化特别敏感,因为新的团队成员可能暂时不具备满足迭代承诺所需的所有部落知识。
-
如果一个团队的成员不在同一个物理区域,Scrum 可能无法很好地工作。在现代电话会议中,像其他各种各样的会议一样,举行每日站立会议仍然是可能的,但是 Scrum 旨在实现协作,因此当出现问题或问题时,更容易直接与其他团队成员联系往往变得非常重要。
-
除非非常小心地管理,否则 Scrum 往往会强化团队中的技能库,如果只有一个开发人员知道,例如,如何用系统需要的第二语言编写代码,为了满足迭代的承诺,该人员将被更频繁地或默认地用于任何需要该知识的任务或故事。有意识地努力将强化筒仓的故事或任务转变为团队或成对的开发工作,可以大大减少这些影响,但如果没有努力,或者没有支持减少这些筒仓,它们将持续存在。
-
如果系统有大量的外部依赖性(例如,来自其他团队的工作),或者开发人员必须应对大量的质量控制工作,那么 Scrum 可能会具有挑战性。如果这些质量控制要求具有与之相关的法律或法规要求,则最后一项可能特别有问题。确保外部依赖本身更具可预测性可以大大减轻此类挑战,但这可能超出团队的控制范围。
在 Scrum 过程的特定部分,SDLC 模型的各个阶段对开发工作非常重要,如下所示:
- 在开发开始之前:
- 需求分析和定义发生在故事创建和梳理过程中,通常在冲刺计划期间进行一些后续工作。目标是在将每个故事包含在 Sprint 中之前,了解并提供每个故事的需求。
- 系统架构和设计项目遵循大致相同的模式,尽管迭代中的故事也可能有架构和/或设计任务。
- 发展过程本身:
- 发展显然发生在冲刺阶段。
- 质量保证活动通常也会作为 Sprint 的一部分发生,当开发者认为每个故事都已完成时,就应用到每个故事中。如果测试活动发现问题,故事将回到任务板上的正在开发状态,或者可能是更早的状态,并将尽快被收集和纠正。
- 系统集成和测试也可能在 Sprint 期间进行,假设有一个环境可以使用新代码执行这些活动。
- 验收可以在每个故事通过所有 QA、系统集成和测试活动时逐个故事进行,也可以在 Sprint 演示和验收会议结束时一次性进行。
从开发人员的角度不难看出为什么 Scrum 很受欢迎,它有着严格的规划,并致力于确保开发人员的时间得到尊重和切实分配,他们的日常关注减少到他们目前正在做的任何事情。如果有一个成熟的团队,他们拥有相当一致的技能集,并且对系统及其代码库有良好的工作知识,那么从业务角度来看,Scrum 是可以合理预测的。最后,Scrum 如果谨慎和有纪律地管理,在出现问题或担忧时会自我纠正,在某种程度上,或流程与系统和代码库一起,流程将提供解决和纠正这些项目的机制。
看板作为一个过程,与 Scrum 有很多相似之处:
- 主要的工作单元是用户故事。
- 故事具有相同类型的故事级流程状态,以至于使用相同类型的任务板(真实或虚拟)来跟踪并提供对正在进行的工作的可见性。
- 故事应该准备好所有的需求和其他相关信息,并在开始工作之前等待。这意味着有某种类型的故事梳理过程,尽管它可能没有 Scrum 中同等的正式结构。
看板,与 Scrum 不同:
- 没有时间限制,就没有冲刺。
- 不期望或不需要每日状态/站立会议,尽管它是一个足够有用的工具,因此被普遍采用。其他变体和方法,可能首先关注被阻止的项目,然后关注正在进行的项目,然后是其他任何东西,也是可行的。
- 不期望或要求故事的大小,尽管它同样是一个足够有用的工具,并且并不少见,特别是如果它是一个有用的标准,用于优先考虑故事的开发。
看板的主要关注点可以被描述为尽量减少上下文变化的努力,这表现为在完成单个故事之后再继续下一个故事。这通常会导致按需求对功能进行优先级排序,这非常适合于故事之间存在功能依赖关系的情况。
That working-until-complete focus is probably going to occur in a Scrum process as well, but it's not actually expected, since the goal in Scrum is to complete all stories in a Sprint, and assistance from others on the team to complete a story may well be necessary at any point to accomplish that goal.
看板的整个过程非常简单:
-
故事(及其任务)已准备就绪,并按工作优先级排列
-
一个或多个开发人员选择一个故事,并对其进行处理,直到它完成,然后用另一个故事重复该过程,再重复另一个故事,依此类推
-
在针对当前故事进行开发和工作的过程中,随着细节变得可用,新故事已经准备好并添加到可用工作的堆栈中,并相应地进行优先级排序
看板与 Scrum 具有不同的策略和程序,具有不同的优势:
- 看板非常适合于存在大量知识或专业知识的工作,因为它专注于功能的完成,不管需要多长时间
- 看板处理的故事和功能都很大,而且不容易划分为较小的逻辑或功能块,而不必经历将它们细分为 Sprint 大小的块的过程(但有关这方面的缺点,请参阅下一节)
- 看板直接限制了正在进行的工作,从而降低了开发人员过度工作的可能性,前提是工作流程得到了正确和良好的规划
- 看板允许利益相关者在任何时间点以任何优先级添加新工作,但最好还是避免中断正在进行的工作
- 如果每个故事都是独立的和可交付的,那么一旦被接受,每个已完成的故事就可以进行安装或实施
它也有自己的一套警告:
-
看板在开发过程中更容易出现瓶颈,特别是如果后续故事存在大规模或长时间的依赖关系,例如可能需要三周时间才能完成的数据存储系统,也就是说,存在许多需要它的小型类结构的依赖关系,如果数据存储系统完成,这项技术可以在几天内实施。
-
由于看板不能在比单个故事更高的层次上提供任何具体的里程碑,因此,如果出于外部业务原因需要这些里程碑,看板需要更直接和有意识的努力来建立这些里程碑。
-
对于看板流程中分阶段开发的功能,通常需要更多的有意识的思考和努力,以使其高效。例如,任何具有必须具备、应该具备和很高兴具备所有将要实现的功能,需要从一开始就提供对未来阶段目标的一些认识和指导,以保持效率。
-
看板不要求团队作为一个整体了解工作的基础设计,这可能会导致误解,甚至导致跨目的的开发工作。可能需要有意识地努力取消筒仓设计,并提高对更大规模需求的整体认识,而一开始可能并不明显。
许多敏捷过程,特别是那些将故事作为基本工作单元的过程,有很多相似之处。由于大多数与故事相关的项目都在讨论 Scrum 时进行了详细描述,因此任何使用故事的后续方法都只会注意到主题的变化:
- **在开发开始之前:**需求分析和定义、系统架构和设计的工作方式与 Scrum 中的工作方式大致相同,原因也很多。主要区别在于看板中有一个不太正式的结构,用于将需求和架构细节附加到故事中。它通常发生在有时间和/或感知到的需求时,例如开发团队即将用完可用的故事。
- **开发过程本身:**开发和质量保证过程是给定故事流程的一部分,因为它一直在工作到完成。系统集成和测试也是如此,验收几乎必须发生在故事的生命周期中,因为 Sprint 会议并没有结束来展示开发结果和获得验收。
看板的结构形式不太正式,流程仪式较少,流程采用易于理解的准时制方法,因此易于理解和管理。在关键点上进行一些额外的关注,以及识别这些关键点的能力,在很大程度上有助于保持事情顺利进行,但只要识别和解决这些关键点的能力随着时间的推移而提高,流程也会随之提高。
Scrum 和看板不是唯一的两种敏捷方法,甚至不是唯一值得考虑的两种。值得注意的其他一些方法包括极限编程(extremeprogramming),作为一种独立的方法,以及特性和测试驱动的开发(Feature and Test-Driven Development),或者作为独立的方法,或者作为其他方法的混合。
极限编程(XP最值得注意的方面可能是成对编程方法,这可能是其实现的一个组成部分。它背后的意图/期望是两个开发人员使用一台计算机处理代码,这在理想情况下可以提高他们的专注度、协作能力、更快地解决任何挑战,并允许更快、更好、更可靠地检测正在生成的代码固有的潜在风险。在一个成对的场景中,两个开发人员在编写代码和在编写代码时对代码进行审阅之间以某种频率交替。并非所有的 XP 实现都使用配对方法,但当它不起作用时,其他过程(如广泛且频繁的代码审查和单元测试)是必要的,以保持至少一些因不使用该选项而失去的好处。
XP 作为一种方法论,可能无法在不牺牲一些开发速度的情况下处理高度复杂的代码库或对代码库的高度复杂的更改。与 Scrum 和看板等更及时的方法相比,它还需要更密集的规划和需求,因为理想情况下,成对的开发人员应该能够以他们能够管理的方式自主地处理代码。结对团队预先掌握的信息越多,他们花在追踪所需信息上的时间就越少,对他们的工作造成的干扰也就越少。XP 实际上没有任何方法来跟踪进度,或者使工作和障碍保持可见,但是采用或栓接其他方法肯定是可能的。
特性驱动开发(FDD过程中的主要工作单元是特性。这些特性是详细的系统建模工作的最终结果,重点是创建一对多领域模型的重要细节,绘制出特性在系统领域中的位置,以及它们如何(或是否)相互交互用例、中应产生的信息种类数据结构、流模型和进程间通信模型。一旦建立了整体模型,就会构建一个功能列表并对其进行优先级排序,从特定的角度来看,至少尝试将列表中每个功能的实现时间框架保持在合理的最长两周似乎是典型的限制。如果单个功能预计需要的时间超过可接受的最长时间,则将对其进行细分,直到可以在该时间段内完成并交付。
一旦完整的特性列表准备好实施,围绕完成这些特性的迭代将在固定的时间段内进行规划。在每次迭代中,特性或特性集被单独或成组地分配给开发人员。这些开发人员制定出最终的实现设计,并在需要时对其进行审查和改进。一旦设计被认为是可靠的,就会进行代码的开发和测试,以实现设计,并将生成的新代码提升到构建或分发就绪的代码库中进行部署。
FDD 与几个开发最佳实践(自动化测试、配置管理和常规构建)齐头并进,因此,如果它们不是一个完整的、正式的持续集成过程,那么它们很接近成为一个。功能团队通常规模较小,动态组建,至少有两名成员,旨在促进协作和早期反馈,特别是在功能的设计和实现质量方面。
通过将工作分解为小的、可管理的功能,FDD 可能是大型和复杂系统的一个很好的选择,即使是在非常大型、非常复杂的系统环境中进行的开发也将以很高的成功率进行维护。启动和运行任何单个功能的过程都很简单,也很容易理解。除非偶尔签入以确保开发不会因为某种原因而停滞,否则 FDD 是非常轻量级和非侵入性的。功能团队通常会有一个与之相关的首席开发人员,负责协调开发工作,并在需要时细化实现细节。然而,这确实意味着首席开发人员不太可能对实际代码做出贡献,特别是当他们花费大量时间执行协调或设计优化工作,或指导团队的其他成员时。
测试驱动设计(TDD)正如其名称所预期的那样,首先关注的是使用代码库的自动测试来指导开发工作。整个过程分为以下步骤:
- 对于正在实现的每个功能目标(新功能或增强功能):
- 编写一个新的测试或一组测试,这些测试将失败,直到被测试的代码满足被测试的合同和期望。
- 确保新测试因预期原因按预期失败,并且不会引发任何其他失败。
- 编写通过新测试的代码。一开始,它可能非常笨拙,不雅观,但只要它满足测试中嵌入的要求,这并不重要。
- 根据需要优化和/或重新考虑新代码,重新测试以确保测试仍然通过,如有必要,将其移动到代码库中的适当位置,并通常确保其满足整个代码库的任何其他标准和期望。
- 运行所有测试以证明新代码仍然通过新测试,并且没有其他测试因新代码而失败。
TDD 作为一个过程提供了一些明显的好处:
- 系统中的所有代码都将被测试,并且至少有一整套回归测试
- 由于编写代码的主要目标只是通过为其创建的测试,因此代码通常足以实现这一目标,这通常会导致更小、更易于管理的代码库
- 类似地,TDD 代码趋向于更模块化,这几乎总是一件好事,反过来,这通常有助于更好的体系结构,这也有助于更易于管理的代码
很明显,主要的权衡是必须创建和维护测试套件。它们将随着系统的增长而增长,并且需要越来越长的时间来执行,尽管显著的增长(希望)需要一段时间才能显现出来。测试套件的创建和维护需要时间,这本身就是一门学科。一些人甚至认为编写好的测试是一门艺术,这是有一定道理的。最重要的是,有一种倾向是寻找错误的度量来显示测试的执行情况:比如代码覆盖率,甚至是单个测试用例的数量,这些都不表示测试的质量。
编程最初出现时,常常受到硬件功能和当时可用于简单过程代码的高级语言的限制。在这个范例中,程序是一系列步骤,从头到尾执行。有些语言支持子例程,甚至可能支持简单的函数定义功能。例如,有一些方法可以循环代码的各个部分,这样程序就可以继续执行,直到达到某种终止条件为止。但总的来说,它是一组非常强大的从开始到结束的过程。
随着底层硬件的功能随着时间的推移而改进,更复杂的功能开始变得更容易获得,正如人们现在普遍认为的那样,正式功能更加强大,或者至少具有灵活的循环和其他流控制选项,等等。然而,除了一些通常只能在学术殿堂和学术墙内使用的语言之外,直到 20 世纪 90 年代,面向对象编程才开始成为一种重要的、甚至占主导地位的范式,主流工作中对过程方法没有太多重大的改变。
以下是一个相当简单的程序示例,该程序要求提供网站 URL,从中读取数据,然后将数据写入文件:
#!/usr/bin/env python
"""
An example of a simple procedural program. Asks the user for a URL,
retrieves the content of that URL (http:// or https:// required),
writes it to a temp-file, and repeats until the user tells it to
stop.
"""
import os
import urllib.request
if os.name == 'posix':
tmp_dir = '/tmp/'
else:
tmp_dir = 'C:\\Temp\\'
print('Simple procedural code example')
the_url = ''
while the_url.lower() != 'x':
the_url = input(
'Please enter a URL to read, or "X" to cancel: '
)
if the_url and the_url.lower() != 'x':
page = urllib.request.urlopen(the_url)
page_data = page.read()
page.close()
local_file = ('%s%s.data' % (tmp_dir, ''.join(
[c for c in the_url if c not in ':/']
)
)).replace('https', '').replace('http', '')
with open(local_file, 'w') as out_file:
out_file.write(str(page_data))
print('Page-data written to %s' % (local_file))
print('Exiting. Thanks!')面向对象编程的显著特点是(毫不奇怪),它表示数据并通过对象实例提供功能。对象是数据的结构,或属性或属性的集合,它们也具有相关的功能(方法)。对象根据需要从类中构造,通过属性和方法的定义,在属性和方法之间定义对象是什么、具有什么以及对象可以做什么。OO 方法允许以一种与过程方法中的等价方法截然不同的、通常更有用的方式处理编程挑战,因为这些对象实例跟踪自己的数据。
以下功能与前面所示的简单过程示例相同,但使用面向对象的方法编写:
#!/usr/bin/env python
"""
An example of a simple OOP-based program. Asks the user for a URL,
retrieves the content of that URL, writes it to a temp-file, and
repeats until the user tells it to stop.
"""
# Importing stuff we'll use
import os
import urllib.request
if os.name == 'posix':
tmp_dir = '/tmp/'
else:
tmp_dir = 'C:\\Temp\\'
if not os.path.exists(tmp_dir):
os.mkdirs(tmp_dir)
# Defining the class
class PageReader:
# Object-initialization method
def __init__(self, url):
self.url = url
self.local_file = ('%s%s.data' % (tmp_dir,
''.join(
[c for c in the_url if c not in ':/']
)
)).replace('https', '').replace('http', '')
self.page_data = self.get_page_data()
# Method to read the data from the URL
def get_page_data(self):
page = urllib.request.urlopen(self.url)
page_data = page.read()
page.close()
return page_data
# Method to save the page-data
def save_page_data(self):
with open(self.local_file, 'w') as out_file:
out_file.write(str(self.page_data))
print('Page-data written to %s' % (self.local_file))
if __name__ == '__main__':
# Almost the same loop...
the_url = ''
while the_url.lower() != 'x':
the_url = input(
'Please enter a URL to read, or "X" to cancel: '
)
if the_url and the_url.lower() != 'x':
page_reader = PageReader(the_url)
page_reader.save_page_data()
print('Exiting. Thanks!')尽管它执行完全相同的任务,并且对于用户来说,以完全相同的方式执行,但是在它下面是一个PageReader类的实例,它完成了所有实际工作。在此过程中,它存储各种数据,这些数据可以作为该实例的一个成员进行访问。也就是说,page_reader.url、page_reader.local_file和page_reader.page_data属性都存在,如果需要检索该数据,可以检索并使用这些属性,并且可以再次调用page_reader.get_page_data方法来获取页面上数据的新副本。需要注意的是,属性是附加到实例的,因此可能有多个PageReader实例,每个实例都有自己的数据,它们都可以用自己的数据做同样的事情。也就是说,如果执行了以下代码:
python_org = PageReader('http://python.org')
print('URL ................ %s' % python_org.url)
print('Page data length ... %d' % len(python_org.page_data))
google_com = PageReader('http://www.google.com')
print('URL ................ %s' % google_com.url)
print('Page data length ... %d' % len(google_com.page_data))它将产生以下输出:
面向对象的设计和实现使复杂系统的开发在相当长的一段时间内变得相当容易,并伴随着复杂的交互,尽管它可能不是解决所有开发挑战和工作的灵丹妙药。但是,如果遵循好的 OO 设计的基本原则,它们通常会使代码更容易编写、更容易维护、更不容易被破坏。对 OO 设计原则的全面讨论远远超出了本书的范围,但一些更基本的原则如果不遵守,可能会造成很多困难,如下所示:
- 对象应该有一个单一责任-每个都应该做(或代表)一件事,并且做得很好
- 对象应该打开进行扩展,而关闭进行实例实际功能的修改,除非它是一个全新的功能,完全不存在,不需要修改实际代码
- 对象应该封装什么变化它不需要使用对象来了解它是如何做的,它做什么的,只要它能做到
- 对象的使用应该是对接口编程的练习,而不是对实现编程。这是一个复杂的主题,值得详细讨论,有一些实质内容和上下文,因此将在第 9 章、测试业务对象中详细介绍,在制定
hms_sys项目的架构时
函数式编程(FP)是一种围绕通过一系列纯函数传递控制的概念,避免共享状态和可变数据结构的开发方法。也就是说,FP 中的大多数实际功能都封装在函数中,这些函数对于任何给定的输入都将始终返回相同的输出,并且不修改任何外部变量。从技术上讲,纯函数不应该将数据写入任何地方,既不应该将日志记录到控制台或文件,也不应该将数据写入文件。如何满足这种输出的需要,这一讨论远远超出了本书的范围。
下面的功能与前两个示例中的功能相同,但使用函数式编程方法编写(如果勉强如此,因为它执行的任务并不那么复杂):
#!/usr/bin/env python
"""
An example of a simple FP-based program. Asks the user for a URL,
retrieves the content of that URL, writes it to a temp-file, and
repeats until the user tells it to stop.
"""
# Importing stuff we'll use
import os
import urllib.request
if os.name == 'posix':
tmp_dir = '/tmp/'
else:
tmp_dir = 'C:\\Temp\\'
if not os.path.exists(tmp_dir):
os.mkdirs(tmp_dir)
# Defining our functions
def get_page_data(url):
page = urllib.request.urlopen(url)
page_data = page.read()
page.close()
return page_data
def save_page_data(local_file, page_data):
with open(local_file, 'w') as out_file:
out_file.write(str(page_data))
return('Page-data written to %s' % (local_file))
def get_local_file(url):
return ('%s%s.data' % (tmp_dir, ''.join(
[c for c in the_url if c not in ':/']
)
)).replace('https', '').replace('http', '')
def process_page(url):
return save_page_data(
get_local_file(url), get_page_data(url)
)
def get_page_to_process():
the_url = input(
'Please enter a URL to read, or "X" to cancel: '
)
if the_url:
return the_url.lower()
return None
if __name__ == '__main__':
# Again, almost the same loop...
the_url = get_page_to_process()
while the_url not in ('x', None):
print(process_page(the_url))
the_url = get_page_to_process()
print('Exiting. Thanks!')同样,此代码执行完全相同的功能,并且使用与前两个示例相同的离散步骤/过程来执行。然而,它可以这样做,而不必实际存储它使用的任何各种数据,在流程本身中没有可变的数据元素,只有在process_page函数的初始输入中,即使如此,它也不能在很长时间内有效地可变。主函数process_page也不使用任何可变值,只使用其他函数调用的结果。所有的组件函数都返回一些东西,即使它只是一个None值。
函数式编程不是一种新的范式,但直到最近才被广泛接受。它有可能像面向对象编程一样具有根本性的破坏性。它在许多方面也是不同的,因此向它过渡可能很困难,毕竟,它依赖于本质上不同的方法,以及在其他现代发展范式中非常非典型的无状态基础。然而,这种无状态的性质,以及它在执行过程中强制执行严格的事件序列的事实,有可能使基于 FP 的代码和流程比它们的 OO 或过程对应物更加稳定。
至少出现了两种开发后流程自动化实践,要么是由于一些增量开发方法,要么只是同时出现:持续集成和持续交付(或部署)。
持续集成(CI)在其最简单的描述中,是一个可重复、自动化的过程,用于将新的或修改过的代码合并到公共共享环境中,无论是在某种时间基础上,还是在向源代码管理系统提交更改等事件的结果下。它的主要目标是尽可能早地在代码升级或部署过程中尝试并检测潜在的集成问题,以便在将出现的任何问题部署到实际的生产分支之前解决这些问题。为了实现 CI 流程,无论使用何种工具来控制或管理它,都有一些先决条件:
- 代码需要在某种类型的版本控制系统中进行维护,理想情况下,CI 流程将针对一个且只有一个分支执行。
- 构建过程应该是自动化的,无论它是按照预定的时间表启动的,还是作为提交到版本控制系统的结果启动的。
- 作为构建过程的一部分,所有自动化测试(特别是单元测试,但可以有效执行的任何集成或系统测试至少应考虑包含在内)都应执行。当这些测试启动时可能值得讨论,因为可能有两个或更多的机会窗口,并且它们都有各自的优势:
- 在提交和生成完成之前执行的测试,如果工具和流程可以阻止提交或生成,或者在测试失败时将提交回滚到其最后一个良好状态,将阻止提交其测试失败的代码。这种情况下的折衷是,来自两个或多个代码更改源的冲突更改可能会严重纠结,需要相应地注意补救。此外,如果无法提交有问题的代码,则可能很难将有问题的代码交给能够快速解决问题的其他开发人员。
- 构建后执行的测试将允许将一个或多个测试失败的代码提交到集合代码库,但至少存在已知问题。根据这些问题的形状和范围,它可能会破坏构建,这可能会破坏整个团队的生产力。
- 需要有某种通知流程来提醒开发人员存在问题,特别是当问题导致构建中断时。
- 该过程需要确保每个提交都经过测试并可成功构建。
- 成功构建的结果需要以某种方式提供,无论是通过某种脚本或自动部署到特定测试环境,使新构建的安装程序可供下载,还是最适合产品、团队或利益相关者需求的任何其他机制。
有了这些,流程的其余部分只是制定一些流程规则和期望,并在需要时实施、监控和调整它们:
- 什么时候发生?每日的在故事、特写或任何可能适用的工作单元的开发结束时?
- 提交测试构建过程需要以多快的速度运行?可以采取哪些步骤(如果有的话)使其足够快而有用?
持续交付或部署(CD)是 CI 流程的自然延伸或分支,每次成功构建,收集所有相关组件,或者直接部署(通常用于 web 和云端驻留的应用程序和系统),或者采取任何必要的步骤使新构建可用于部署,例如创建最终的、最终用户或生产就绪的安装包,但不实际部署它。
完整的 CD 流程将允许仅基于源代码管理系统中的信息创建、更新或重新创建生产系统。它还可能涉及系统管理端的一些配置管理和发布管理工具,这些工具很可能会对系统的设计和实现施加特定的功能或架构要求,或两者兼而有之。
最后几章希望能让您至少对开发工作中的所有活动部分(实际编写代码之外)有一个大致的了解,这在软件工程中是非常有用的。任何给定的团队或公司都有可能选择哪种方法,以及开发前和开发后的流程。即便如此,在不同的组合环境中工作时,了解对他们的期望,或者可能引起关注的原因,也是有用的信息,通常也是区分程序员和软件工程师的期望之一。
说了这么多,现在是时候开始更深入、更详细地研究这些开发过程本身的组合了。要做到这一点,我们需要一个系统——一个项目。



