65.9K
CodeProject 正在变化。 阅读更多。
Home

推出您的功能

starIconstarIconstarIconstarIconstarIcon

5.00/5 (13投票s)

2014 年 2 月 14 日

MIT

13分钟阅读

viewsIcon

35295

downloadIcon

324

如何摒弃“稳定系统不应更改”的观念,并尽可能频繁地发布——通过抽象分支、特性分支和 FeatureBee 的功能开关

引言

在我刚开始开发时,变更处理很简单。“稳定系统不应更改”作为一种指导方针,确保我们开发者只在特定时间(例如每年一次)进行发布,以使发布过程尽可能顺利和简单。嗯,可惜的是,它几乎从未真正顺利过。

一年只发布一次,这意味着必须在那之前完成你的功能。如果它没有 100% 完成,好吧,你只能希望没人会注意到。你还是会想办法把它发布出去。

结果是,这些“大爆炸式”的发布是灾难性的。有一些发布,在“大爆炸”之后几天,平台仍然处于离线状态,我们试图集成那些只在生产环境中出现且在开发机上无法重现的 bug 的修复。

甚至在那时,我们注意到的一件事是,bug 修复发布比第一次“大爆炸”式发布要混乱得少,速度更快,而且效果更好。在一个“大爆炸”式的流程结束时,通常经过两周的披萨、可乐和无眠的夜晚,我们可能会发布 5-6 次,而且每次都比之前更顺利。

起初我们忽略了这种改进。我们需要休息,需要一些时间和家人在一起,并认为发布很愚蠢,因为它让我们工作辛苦。

幸运的是,比我这个卑微的作者更聪明的人,比如 Jez Humble 和 David Farley,决定这种方式很愚蠢。他们有了一个新方法:持续交付。

持续交付(引用 Jez 和 David 的话说)“是将每一个好的构建都发布给用户(Humble & Farley, 2010),“这意味着,如果你一年只发布一次,你要么必须违反你的编码规范(通常会说‘交付高质量的代码’),要么永远不要提交到主干。”

紧随其后的是另一个带有“持续”一词的朋友——持续集成。这要求开发人员(引用互联网的巨匠,维基百科的话)“每天多次将所有开发者的副本合并到主线”。

这样做,并拥有一个持续集成流水线,让你很容易看到一个构建是否是好的,因为它会变成绿色。从一年发布一次,似乎你已经达到了每天发布十几次的程度。至少理论上是这样。

实际上,你正在开发功能,而功能很少能在 10 分钟内完成,通常需要数周。如果你经常将未完成功能的所有更改提交到主干,你的同事开发者、QA 和自动化测试套件很可能会因为你一直破坏构建而把你吊起来。

图 1:有些功能就是不喜欢在几分钟内完成

最终,你不会更快。当与多个团队合作时,它甚至可能阻止任何发布,因为当多个团队在开发多个功能时,至少有一个功能可能处于不稳定的状态。

那么,让我们来看看这个困境的解决方案。

小步快跑发布

这是最佳实践解决方案。你尝试将用户故事分解为任务,这些任务最多需要一天完成,并立即展示给客户。这是一个完美的解决方案,但在我看来,在日常业务的许多场景中,它效果并不理想。

图 2:我告诉她分小步发布,但她就是不听 | (来源:Office Royality Free Images)

当处理一个产品时,你会面临约束——与其他团队的依赖关系、你必须等待的上游/下游依赖关系、需要你在特定日期展示新功能以进行营销宣传,或者要求在展示给所有人之前先在一组选定的用户上测试新功能。

抽象分支

抽象分支是名称最愚蠢的应用模式,因为它可能让你联想到版本控制中的分支,而实际上并非如此。

其思想很简单——识别你的关注点,在你的应用程序和处理该关注点的旧代码之间添加一个抽象层,并实现你的新方式来处理这个抽象。一旦你的新方式实现完成,只需轻轻拨动开关,新功能即可正常工作。

抽象分支模式与策略设计模式非常相似,所以你可能已经有如何实现的想法了。如果你必须替换应用程序的某些部分,例如切换到新数据库,迁移到云端(这可能需要你重新思考所有的本地存储小玩意儿)等等,这是一个绝佳的选择。

好的一点是——当你开发新实现时,构建一直都是好的,即使你一直在集成到主干。

此外,这种方法让你在某些场景下可以分小步发布,从而将所有已经由新方法实现的所有请求路由到新方法。

这个抽象层可以通过分离关注点来帮助你改进代码,从而引入较低的耦合和更强的内聚。

然而,在大多数情况下,这个额外的层会引入一些不必要的复杂性。如果你没有理由保留这个层(因为这部分未来可能再次改变),那么在实现完成后将其删除,并直接使用新方式通常是好的做法。你的代码很可能仍然比以前更漂亮,因为你不得不识别并解耦了一个关注点。

特性分支

现在我们到了实际进行分支的地方。特性分支围绕着每个功能实现都在自己的分支中进行的思想。分支被推送到中央存储库,因此每个特性分支对每个开发人员都可用。

一旦实现完成,开发人员会发起一个拉取请求(pull-request),团队会讨论这些更改,然后合并到主分支。

图 3:特性分支——只要有不到 200 个人并行操作一个类,它们看起来都很简单

这有很多优点——你正在处理的代码不会影响主分支,所以你可以独立工作。此外,如果你正在处理一个客户端应用程序,其中二进制文件会交付给客户,这是一种非常经济有效的方式来确保客户端无法激活代码。

但你必须确保特性分支的生命周期不要太长。如果它们的生命周期太长,并且有很多开发人员/团队拥有很多分支,你可能会再次陷入可怕的合并冲突。

我个人倾向于结对编程而不是代码评审,我认为拉取请求所需的沟通是一种额外的开销。

此外,这是我的个人观点,它并没有自动强制你解耦你的功能。而强制低耦合——高内聚是我非常看重的。

功能开关

功能开关基本上是特定功能状态的布尔值。你的做法是将新逻辑包装在一个 `if` 语句中,而将旧代码块包装在 `else` 中。它看起来可能像这样

if (Feature.IsEnabled(„KillerFeature“)) {
    // awesome!
} else {
    // boring…
}

老实说,这看起来并不好。它似乎在尖叫:“技术债务”、“测试复杂性”和“损坏的代码”。

现在我们先来谈谈 **技术债务**。确实,通过这个简单的 `if`/`else`,你创建了一个整个区域,而你的应用程序没有使用它。基本上是未使用的代码,但仍然被编译和打包。

但是它有帮助——一旦功能完成,你就可以简单地删除 `else` 中的所有内容(以及仅在 `else` 中使用的所有依赖项)。因此,在功能开关的环境中,代码僵尸实际上可能会减少——至少根据我的经验。因此,技术债务更像是在开始功能之前你承担的“技术债务信用”,并在功能结束时将其偿还,并赚取“易于删除所有未使用的代码”的利息。

此外,这项技术债务允许我们在出现问题时回滚。如果生产环境出现问题,我们可以简单地将功能设置为 `false`。

**损坏的代码**,即半完成的功能会破坏你的生产系统的恐惧,是真实存在的。你必须有单元测试、验收测试到回归测试,以确保任何更改都不会导致系统崩溃。
然而,这对于你在代码中做的任何其他更改都是适用的。

**测试复杂性** 在我们最初的功能开关方法中得到了很好的解决:在我第一次使用功能开关的 AutoScout24,功能的状态是在配置中设置的。这导致了以下场景:功能开启或关闭,但不能同时开启。这使得我们可以限制开关的影响——只需要测试一种状态。

这种设置的工作流程是这样的

图 4:让别人只短暂等待而不是永远等待,这是一种很好的感觉

你推送代码时,开关值设置为 `true`,尝试让它通过你的流水线,如果失败,你就切换开关,不会阻塞其他人。

在我看来,功能开关最大的优点可能是你可以强制执行的一点:允许一个功能只在一个位置进行切换。

为什么这是优点?它要求你审视你的设计,并将关注点分离到非常明确的层面。如果你不能在一个地方切换一个功能,那么该功能要么不是一个很好的用户故事,要么你的代码存在某种“气味”。

在第一种情况下,可能为时已晚(因为尝试切换你的代码意味着计划已经完成,你已经开始做了),但在第二种情况下,你可以开始重构。策略模式通常是一个很好的起点。

一个缺点可能是,在客户端应用程序中,你将所有代码部署到客户端,如果客户端查看配置,它就能切换该功能。对于非常不成熟的功能,这可能不是你想要的。

功能开关的另一个缺点是嵌套的危险——你可能会意外地将一个功能嵌套在另一个功能中,这会带来很多问题,甚至更多的复杂性;例如,你需要切换一个开关才能看到另一个与它无关的功能,而测试这种场景非常棘手,我可以告诉你。

然而,在我看来,这一切的麻烦都是值得的,因为你可以真正实现持续集成(通过将所有更改提交到主分支),持续交付(通过不总是破坏一切),并保持你的代码整洁。

Feature Bee

虽然将我们的功能状态放在配置中是一种非常安全和方便的功能发布方式,但我们发现这种配置有两个“气味”

  1. 关注点分离不足 (SoC) – 我们这样做是错误的。将代码和功能一起发布使我们变慢。每次切换开关时,我们的生产发布都会花费更长的时间,因为有大量手动工作(验收)涉及其中。此外,这种紧密的耦合阻止了自动部署到我们的生产环境。
    另外,设置功能开关和转换配置也包含在我们的构建流水线以及回归测试中。
  2. 产品负责人无法履行职责 – 配置与源代码在一起,虽然它只是一个 yaml 文件,但并非公司里的所有产品负责人都能编辑源代码仓库中的文件。
  3. 条件/声明式发布很棘手 – AutoScout24 是一家国际公司,在多个国家设有市场,但只有一个共享的代码库(这本身就有点“气味”,但这属于另一个话题)。
    一些功能首先在较小的国家/地区向一部分用户发布,以测试其影响。这是通过 Optimizely 完成的,Optimizely 只适用于前端。后端更改,例如更改搜索索引器,总是需要手动编码。

图 5:单一职责原则——我们做错了 | (c) http://www.flickr.com/photos/redjar/

为了克服这些问题,我们不得不改变我们的方法,采用另一种设置——与 AutoScout24 的另一位开发者 Philipp 一起,我开始了一个名为 FeatureBee 的新项目。该项目旨在解决这三个问题。

总而言之,FeatureBee 是一个发布功能的单一按钮。

图 6:一个按钮——FeatureBee 的核心思想

更具体地说:它是一个带有 API 和 UI 的服务器,以及许多询问服务器状态的客户端。

在 .NET 应用程序中安装客户端非常简单——在你的 nuGet 控制台中调用

Install-Package FeatureBee

在你的配置中设置服务器 URL,然后在你的 `global.asax.cs` 中添加

FeatureBeeBuilder.ForWebApp().UseConfig();

(需要更多代码示例)。更完整的文档可以在该项目的 github wiki 中找到。

应用程序本身使用起来非常直观——它是一个带有便利贴的面板

图 7:如果你成功地移动过便利贴,你就准备好使用 FeatureBee 了

通过这个简单的 UI,我们对工作方式进行了重大改变:在配置中,每个环境可能有不同的功能状态,而现在,每个环境都具有相同的功能状态。你移动便利贴的一秒钟后,该功能就上线了。许多人抱怨这是测试新功能的时间太短。

起初,我们曾考虑过自动为公司内的所有人发布功能,而忽略面板上的状态。但那样我们会引入另一个问题——通过解耦代码和功能发布,我们必须验证是否测试了每种可能的功能状态——因此,这需要我们思考一种方法,允许产品负责人、QA 和自动测试人员只为自己切换功能以在切换到生产环境之前测试该功能。

我们通过在每个页面添加一个托盘来解决这个问题。这个托盘显示当前所有可用的功能、它们的状态以及一个用于开启或关闭它们的按钮。

图 8:按钮是产品负责人的好朋友

这个简单的实现还改变了另一件事:我们现在正在讨论是否真的需要 QA 环境,因为有了这个托盘,每个人都可以在生产系统上进行验收。

尽管它帮助我们实现了自动化的生产部署,从而实现了更成熟的持续部署。

总结

在 AutoScout24,我们目前使用了上面介绍的所有方法,并尝试为每种场景找到最实用的解决方案。

虽然特性分支使我们能够快速地工作和共享那些太简单而无法切换,但又太复杂而无法在一小时内实现的状态,一旦需要验收,我们就使用功能开关/FeatureBee;对于任何较大的更改,我们使用抽象分支,并在切换实现的地方添加一个开关;并且尽可能地“小步快跑”。

加入 FeatureBee 团队

Feature Bee 是我们所知的唯一集中的 .NET 功能开关解决方案。它已被一个国际组织使用,因此能够处理每天数百万的页面浏览量。

我们 AutoScout24 的人认为,这样的项目可能对其他开发者和公司也很有趣,所以我们添加了 MIT 许可证,并将其放在 gitHub 上。

Fork 它,进行你的修改,然后发送拉取请求,我们会非常乐意接受。

等你切换你的功能时再见。 ;)

Copyright

未明确标记的图片由作者创建,并根据 MIT 许可发布。

© . All rights reserved.