特性标志(或特性切换)是一种在应用程序中逐步推出功能的技术。还没有完全准备好上线的工作可以分块发布,同时用一个标志来隐藏。这些标志通常是根据环境来配置的,这样你就可以,例如,在生产中启用某项功能之前,在暂存站点上启用该功能。
Martin Fowler的Feature Toggles是关于这种做法的经典文章,它概述了Feature Toggles的完整分类法。
切换分为四类:
- 发布切换:允许正在进行中的代码被运送到生产中
- 实验切换:支持A/B测试多个代码路径
- 操作切换:紧急按钮/断路器
- 许可开关:改变产品功能(高级、早期访问等)

Martin Fowler:功能切换图
每种类型的切换器都有不同的需求和属性。对于大多数中小型应用来说,有一种类型在价值上高于其他类型:发布切换器。
一般来说,对于想要快速发货的团队来说,长期运行的分支是一场自我加压的灾难。对于 "我们如何避免长期运行的分支",人们的答案是进行持续集成--而功能标志往往是缺失的概念环节,它需要将CI的教科书概念与 "好吧,我们如何,你知道的,真正做到这一点?"的实际情况联系起来。
虽然其他类型的切换器更多的是针对特定的环境,但每个应用程序都可以从简单的发布切换器中受益。
我们应该如何使用功能标志?
决定功能标志是一种乐趣还是复杂的老鼠窝的因素:
- 数量:你有多少个旗子在同时运行?
- 持续时间:这些旗子要挂多长时间?
- 紧急性:你需要多快打开/关闭一个标志?
- 深度:标志需要在哪个应用层面存在?
- 风险:你需要多彻底地标记一个功能?
这些因素是交织在一起的,并且有连带效应。
如果你的功能只标记了几天,你可能愿意接受更多的风险,部分地暴露一个功能。
如果你的部署过程目前很慢,你会想选一个允许在运行时改变标志的解决方案。
如果你一次只有一个或两个标志,正在开发可以在高层隐藏的新功能,而且有人手动猜测未发布功能的URL也不是问题,你简直可以用美化的if 语句来解决。
最好的办法是从小处开始。正如Sandi Metz所说。"未来是不确定的,你知道的永远比你现在知道的少"。
保持你的选择开放,做最简单的可能工作的事情,然后从那里开始建立(如果你需要的话)。
我们应该如何实现特征标志?
有两种方法来实现特征标志:基本条件和 "特征管理器 "库。
基本条件式
特征标志的核心是一个if 语句。
你可以把一个非常简单的类包装起来,让开发者有更好的体验,并让你的特性保持有序。对于入门来说,不必理会任何宝石或外部工具。只需在你的Rails项目中添加一个这样的类:
class Feature
def self.enabled?(feature_name)
case feature_name.to_sym
when :meeting_transcripts
!Rails.env.production?
else
true
end
end
end
- if Feature.enabled?(:meeting_transcripts)
# Do your thing
利用既定的模式,如Rails.env 或Current.user ,编写简单的条件,为任何给定的功能返回真或假。由于你完全拥有这段代码,你可以随心所欲地调整它。
这对于现实世界来说似乎太原始了,但即使是这种愚蠢的简单的 "在生产中禁用 "标志,往往也是你唯一需要的逻辑。
突破极限
如果你确实想进一步挑战极限,你可以尝试这些聪明的黑客。
Current.user.admin? || !Rails.env.production - 在测试中对所有人开放,但在生产中只对管理员开放
Current.account.early_access_enabled? - 为 "选择加入 "组的领域模型添加字段
Current.user.email.contains("s") - 使用这个来自电子邮件营销的黑客,将一个组大致分成两半
Current.user.id % 100 < 10 - 向大约10%的用户推出一个功能(这不是一个适当的随机抽样,但你实际上不会做真正的统计分析)。
这些高级条件是很聪明的,但聪明并不代表着好。尽可能选择更无聊的每环境标志。
请记住,我们主要是将这些标志作为释放开关。如果你想做实际的数据驱动的分析(这需要比阅读博文更严格的统计......),或者期望这些逻辑成为你的应用程序的永久组成部分,你应该到其他地方去看看。
特征管理工具
团队采取的另一条路线是在运行时使用像flipper这样的宝石来管理标志,或者使用像Flipper Cloud这样的托管服务。

可悲的是,从来没有产品经理能够理解这一点...
还有一些其他的宝石(rollout、flipflop等),但从概念上讲,它们最终都使用Redis作为键值存储,应用一些逻辑(百分比计算、分组等),并返回真/假。
作为对额外机械的交换,你获得了在应用程序运行时打开和关闭功能的能力,甚至可以让非开发人员管理推出的功能。
在现实中,我发现我很少需要这些功能中的任何一个。对于一个简单的案例(0-3个标志)来说,较重的功能标志工具是多余的,如果你有5个以上的标志,则很难管理(哪些功能是打开的?)
有了flipper,部署过程就与你的正常开发工作流程脱钩了。根据你的情况,这可能是一个积极的或消极的因素。
愚蠢的简单的Feature 类需要一个代码提交来改变,这稍微有点烦人,但随后就不需要了。使用flipper ,需要你在正确的环境中拉出管理页面,点击一些按钮,或者远程进入,记得在rails console 中翻动一些位。
你应该把功能标志放在哪里?
只要有可能,就在功能的 "边缘 "标记东西。如果你正在添加一个新的部分,把它隐藏在导航菜单中。如果你要在现有的页面上建立一个新的动作,就隐藏按钮。
你在某个时间需要在你的代码库中放入的标志越少,管理起来就越容易。
如果你能摆脱它,简单地在视图层面上隐藏东西。对于99%的用户来说,如果某些东西不存在于用户界面中,它就不存在于应用程序中。该死的,要让用户注意到那些没有被隐藏的新功能往往已经很困难了
如果你想在一个没有正式启用的环境中进行快速测试,一个 "松散的 "标记的功能将允许你手动键入URL。
但是,如果你正在改变一些内部逻辑,或者如果允许用户访问仍在开发中的功能存在风险,你就需要将你的标记移到系统的 "核心 "位置。
在控制器、模型或服务的深处添加标志是可以的,但应该只在必要时使用。记住同样的 "保持愚蠢的简单 "方法,即使你离边缘更远。
例如,如果你想为一个新的功能404所有的端点,你可以把这个20行的关注点丢进去,以避免到处都有条件:
module FeatureFlaggableController
extend ActiveSupport::Concern
class_methods do
attr_reader :feature_flaggable_name
def feature_flag(feature_name)
@feature_flaggable_name = feature_name
end
end
included do
prepend_before_action :enforce_feature_flag!
private
def enforce_feature_flag!
if self.class.feature_flaggable_name.nil?
raise ArgumentError.new("No feature flag specified! \n\nPlease call `feature_flag(:some_flag)` in #{self.class.name}.")
end
unless Feature.enabled?(self.class.feature_flaggable_name)
raise AbstractController::ActionNotFound
end
end
end
end
class PostsController < ApplicationController
include FeatureFlaggableController
feature_flag(:posts)
# ...
end
你如何用这种基本方法推出功能?
推出功能可能会有压力,但通过以渐进的方式使用功能标志,你可以使你的发布更加平静。
我一般尝试按照以下步骤进行:
- 在开始一个大功能的工作时,添加一个功能标志(在非生产环境中默认为 "on")。
- 当功能准备就绪时,经常合并,并在标志后面进行部署
- 在标志仍然开启的情况下,将完成的功能部署到生产环境中,并测试/检查事情。
- 更新功能逻辑,使其总是返回true,部署并观察
- 如果一切正常,就彻底删除功能逻辑。
这些并不是硬性规定,但关键的概念是要避免长期运行的分支和高压部署,在这种情况下,大量的代码会立即在生产中运行。
这似乎会拖慢你的速度,但对于大多数功能来说,这只是增加了几分钟的额外时间而已。在你开始对任何中大型功能使用发布标志之前,只需要一次生产部署的失误就可以让你焦头烂额。
包裹起来
快速发货意味着频繁部署,将正在进行的工作隐藏在特性标志后面,可以让你以更可控的方式安全地推出变化。不再有 "希望我们不会遇到问题 "的大规模部署,从而产生高压力的情况。不再有长期运行的特性分支。
功能标志在抽象的持续交付概念和战术性的功能发布之间架起了桥梁。虽然有很多种类的功能切换,但简单的发布标志提供了最好的效益成本比。
你可以通过使功能发布成为一个琐碎的事件来实现更平静的部署。在投资于花哨的企业级特性管理工具之前,从小处着手,使用枯燥的Feature 。