万字长文 - 解读功能开关 | IDCF
共 18107字,需浏览 37分钟
·
2022-01-24 09:42
原文:https://martinfowler.com/articles/feature-toggles.html 作者:Pete Hodgson 译者:冬哥
作者 Pete Hodgson是旧金山湾区的一名独立软件交付顾问。他擅长帮助初创工程团队改进他们的工程实践和技术架构。Pete 之前曾在 Thoughtworks 担任过六年的顾问,领导其西海岸业务的技术实践。他还曾在旧金山多家初创公司担任技术主管。
内容
一个开关的故事 开关的类别 管理不同类别的开关 实施技术 开关配置 使用带有特征开关的系统
一、一个开关的故事
function reticulateSplines () {
// 当前的实现在这里
}
这些示例都使用 JavaScript ES2015
加入开关之后
function reticulateSplines(){
var useNewAlgorithm = false;
// useNewAlgorithm = true; // UNCOMMENT IF YOU ARE WORKING ON THE NEW SR ALGORITHM
if( useNewAlgorithm ){
return enhancedSplineReticulation();
}else{
return oldFashionedSplineReticulation();
}
}
function oldFashionedSplineReticulation(){
// current implementation lives here
}
function enhancedSplineReticulation(){
// TODO: implement better SR algorithm
}
function oldFashionedSplineReticulation () {
// 当前的实现在这里
}
function enhancedSplineReticulation () {
// TODO:实现更好的 SR 算法
}
oldFashionedSplineReticulation
函数中,并将 reticulateSplines
设置为一个开关点。现在,如果有人正在研究新算法,他们可以通过取消注释来启用“使用新算法” 功能useNewAlgorithm = true
。1.2 使标志动态化
useNewAlgorithm = true
行的笨拙机制了:function reticulateSplines () {
if ( featureIsEnabled( "use-new-SR-algorithm" ) ){
return enhancedSplineReticulation();
} else {
return oldFashionedSplineReticulation();
}
}
我们现在介绍了一个featureIsEnabled
功能,一个开关路由器,它可以用来动态控制哪个代码路径是活动的。实现开关路由器的方法有很多,从简单的内存存储到具有精美 UI 的高度复杂的分布式系统。
现在我们将从一个非常简单的系统开始:
function createToggleRouter(featureConfig){
return {
setFeature(featureName,isEnabled){
featureConfig[featureName] = isEnabled;
},
featureIsEnabled(featureName){
return featureConfig[featureName];
}
};
}
请注意,我们使用的是 ES2015 的方法简写
我们可以基于一些默认配置创建一个新的开关路由器——也许从配置文件中读取——但我们也可以动态地打开或关闭一个功能。这允许自动化测试来验证开关功能的两侧:
describe( 'spline reticulation', function(){
let toggleRouter;
let simulationEngine;
beforeEach(function(){
toggleRouter = createToggleRouter();
simulationEngine = createSimulationEngine({toggleRouter:toggleRouter});
});
it('works correctly with old algorithm', function(){
// Given
toggleRouter.setFeature("use-new-SR-algorithm",false);
// When
const result = simulationEngine.doSomethingWhichInvolvesSplineReticulation();
// Then
verifySplineReticulation(result);
});
it('works correctly with new algorithm', function(){
// Given
toggleRouter.setFeature("use-new-SR-algorithm",true);
// When
const result = simulationEngine.doSomethingWhichInvolvesSplineReticulation();
// Then
verifySplineReticulation(result);
});
});
1.3 准备发布
更多的时间过去了,团队相信他们的新算法功能齐全。为了确认这一点,他们一直在修改他们的更高级别的自动化测试,以便他们在功能关闭和打开的情况下运行系统。该团队还希望进行一些手动探索性测试,以确保一切按预期工作——毕竟,样条网状结构是系统行为的关键部分。
要对尚未被验证为可用于一般用途的功能执行手动测试,我们需要能够为生产中的一般用户群关闭该功能,但能够为内部用户打开它。有很多不同的方法可以实现这个目标:
让开关路由器根据开关设置做出决策,并针对特定环境进行配置。仅在预生产环境中启用新功能。 允许通过某种形式的管理 UI 在运行时修改开关配置。使用该管理 UI 在测试环境中启用新功能。 教开关路由器如何根据请求做出动态的开关决策。这些决策将开关上下文考虑在内,例如通过查找特殊的 cookie 或 HTTP 标头。通常开关上下文用作识别发出请求的用户的代理。
团队决定使用按请求开关路由器,因为它为他们提供了很大的灵活性。该团队特别感激,这将使他们能够在不需要单独的测试环境的情况下测试他们的新算法。相反,他们可以在生产环境中简单地打开算法,但仅限于内部用户(通过特殊 cookie 检测到)。团队现在可以自己打开该 cookie 并验证新功能是否按预期执行。
1.4 金丝雀发布
基于迄今为止所做的探索性测试,新的网络渲染算法看起来不错。然而,由于它是游戏模拟引擎的重要组成部分,因此仍然有些人不愿意为所有用户启用此功能。团队决定使用他们的功能开关基础设施来执行金丝雀发布,只为他们总用户群的一小部分——“金丝雀”群组开启新功能。
该团队通过向开关路由器传授用户群组的概念来增强开关路由器 - 用户群组始终体验始终开启或关闭的功能。一组金丝雀用户是通过 1% 的用户群的随机抽样创建的——可能使用用户 ID 的模数。这个金丝雀队列将始终启用该功能,而其他 99% 的用户群仍使用旧算法。监控两组的关键业务指标(用户参与度、总收入等),以确保新算法不会对用户行为产生负面影响。一旦团队确信新功能没有不良影响,他们就会修改他们的开关配置,以便为整个用户群启用它。
1.5 A/B 测试
团队的产品经理了解到这种方法,非常兴奋。她建议团队使用类似的机制来执行一些 A/B 测试。关于修改犯罪率算法以考虑污染水平是否会增加或降低游戏的可玩性,一直存在长期争论。他们现在有能力使用数据来解决争论。他们计划推出一个廉价的实现,它可以捕捉到这个想法的本质,并通过一个功能标志来控制。他们将为相当多的用户群启用该功能,然后研究这些用户与“控制”群相比的行为方式。这种方法将允许团队根据数据解决有争议的产品辩论,
这个简短的场景旨在说明功能开关的基本概念,同时也强调该核心功能可以有多少不同的应用程序。现在我们已经看到了这些应用程序的一些示例,让我们更深入地研究一下。我们将探索不同类别的开关,看看是什么让它们与众不同。我们将介绍如何编写可维护的开关代码,最后分享实践以避免功能开关系统的一些陷阱。
二、开关的类别
我们已经看到了功能开关提供的基本功能 - 能够在一个可部署单元中提供替代代码路径并在运行时在它们之间进行选择。上述场景还表明,该工具可以在各种情况下以各种方式使用。将所有功能开关集中到同一个桶中可能很诱人,但这是一条危险的道路。不同类别开关的设计驱动是完全不同的,并且以相同的方式管理它们可能会导致痛苦。
功能开关可以分为两个主要维度:功能开关将存在多长时间,以及开关决策必须有多动态。还有其他因素需要考虑——例如,谁来管理功能开关——但我认为生存周期和动态性是两个重要因素,可以帮助指导如何管理开关。
让我们通过这两个维度的视角考虑各种类别的开关,看看它们适合什么。
2.1 发布开关
发布开关允许将不完整和未经测试的代码路径作为可能永远不会打开的潜在代码交付到生产环境。
这些是用于为实践持续交付的团队启用基于主干的开发的功能标志。它们允许将正在进行的功能检入到共享集成分支(例如 master 或trunk)中,同时仍然允许随时将该分支部署到生产中。发布开关允许将不完整和未经测试的代码路径作为可能永远不会打开的潜在代码交付到生产环境。
产品经理也可以使用同样方法的以产品为中心的版本来防止半成品的产品功能暴露给最终用户。例如,电子商务网站的产品经理可能不想让用户看到仅适用于网站的一物流合作伙伴的新预计物流日期功能,而是希望等到该功能已为所有物流合作伙伴实施。产品经理可能有其他原因不想公开功能,即使它们已经完全实现和测试。例如,功能发布可能与营销活动相协调。以这种方式使用发布开关是实现“将 [功能] 发布与 [代码] 部署分开”的持续交付原则的最常见方式。
发布开关本质上是过渡的。尽管以产品为中心的开关可能需要保留更长的时间,但它们通常不应停留超过一两周。发布开关的开关决定通常是非常静态的。给定发布版本的每个开关决策都是相同的,通过推出具有开关配置更改的新版本来更改开关决策通常是完全可以接受的。
2.2 实验开关
实验开关用于执行多变量或 A/B 测试。系统的每个用户都被放置在一个群组中,并且在运行时,开关路由器将根据他们所在的群组始终如一地将给定的用户发送到一个或另一个代码路径。通过跟踪不同群组的聚合行为,我们可以比较效果不同的代码路径。这种技术通常用于对电子商务系统的购买流程或按钮上的宣传性用语等事物进行数据驱动的优化。
实验开关需要在相同配置下保持足够长的时间以产生具有统计意义的结果。取决于可能意味着生命周期数小时或数周的流量模式。更长的时间不太可能有用,因为对系统的其他更改可能会使实验结果无效。就其性质而言,实验开关是高度动态的 - 每个传入请求都可能代表不同的用户,因此可能会以不同于上一个的方式路由。
2.3 运维开关
这些标志用于控制我们系统行为的操作方面。我们可能会在推出具有不确定性能影响的新功能时引入运维开关,以便系统操作员可以在需要时在生产中快速禁用或降级该功能。
大多数运维开关的寿命都相对较短——一旦对新功能的操作方面获得信心,就应该停用该标志。然而,系统拥有少量长寿命的“终止开关”并不少见,它们允许生产环境的操作员在系统承受异常高负载时优雅地降低非重要系统功能。
例如,当我们处于繁重的负载下时,我们可能希望禁用我们主页上的推荐面板,该面板的生成成本相对较高。我咨询了一家维护运维开关的在线零售商,这可能会在高需求产品发布之前故意禁用其网站主要采购流程中的许多非关键功能。这种长期存在的运维开关可以视作是一种手工管理的断路器。
如前所述,这些标志中的许多只存在很短的时间,但一些关键控件可能几乎无限期地保留给操作员。由于这些标志的目的是让操作员对生产问题做出快速反应,因此他们需要非常快速地重新配置 - 需要推出新版本以翻转运维开关,不太可能让操作人员满意。
2.4 许可开关
这些标志用于更改某些用户收到的功能或产品体验。例如,我们可能有一组“高级”功能,只为付费客户启用。或者,也许我们有一组仅供内部用户使用的“alpha”功能和另一组仅供内部用户和 beta 用户使用的“beta”功能。我将这种为一组内部或测试版用户打开新功能的技术称为香槟早午餐 - 一个“喝自己的香槟”的早期机会。
香槟早午餐在很多方面与金丝雀发布相似。两者之间的区别在于,金丝雀发布的功能面向随机选择的一组用户,而香槟早午餐功能面向一组特定用户。
当用作管理仅向高级用户公开的功能时,与其他类别的功能开关相比,许可开关的寿命可能非常长 - 以多年为规模。由于权限是特定于用户的,因此许可开关的开关决定将始终按请求进行,因此这是一个非常动态的开关。
三、管理不同类别的开关
现在我们有了一个开关分类方案,我们可以讨论动态性和寿命这两个维度如何影响我们使用不同类别的特征标志的方式。
3.1 静态与动态开关
做出运行时路由决策的开关必然需要更复杂的开关路由器,以及这些路由器的更复杂配置。
对于简单的静态路由决策,开关配置可以是每个功能的简单开或关,开关路由器仅负责将静态开/关状态中继到开关点。正如我们之前所讨论的,其他类别的开关更加动态并且需要更复杂的开关路由器。例如,实验开关的路由器为给定用户动态地做出路由决策,可能使用某种基于该用户 id 的一致群组算法。这个开关路由器不需要从配置中读取静态开关状态,而是需要读取某种队列配置,定义诸如实验队列和控制队列应该有多大。
稍后我们将深入探讨管理此开关配置的不同方法。
3.2 长期开关与瞬态开关
我们还可以将开关类别划分为本质上是短暂的类别与长期存在且可能存在多年的类别。这种区别应该对我们实现功能的开关点的方法有很大的影响。如果我们要添加一个将在几天后删除的发布开关,那么我们可能会使用一个开关点,它对 开关路由器进行简单的 if/else 检查。这就是我们之前使用样条网格示例所做的:
function reticulateSplines () {
if ( featureIsEnabled( "use-new-SR-algorithm" ) ){
return enhancedSplineReticulation();
} else {
return oldFashionedSplineReticulation();
}
}
但是,如果我们要创建一个带有开关点的新许可开关,我们希望它会持续很长时间,那么我们当然不希望通过不加选择地散布 if/else 检查来实现这些开关点。我们需要使用更易于维护的实现技术。
四、实施技术
功能标志似乎会产生相当混乱的开关点代码,并且这些开关点也有在整个代码库中扩散的趋势。保持这种趋势检查代码库中的任何功能标志很重要,如果标志将长期存在,这一点至关重要。有一些实现模式和实践有助于减少这个问题。
4.1 将决策点与决策逻辑解耦
功能开关的一个常见错误是将做出开关决策的位置(开关点)与决策背后的逻辑(开关路由器)结合起来。
让我们看一个例子。我们正在开发下一代电子商务系统。我们的一项新功能将允许用户通过单击订单确认电子邮件(又称发票电子邮件)中的链接轻松取消订单。我们正在使用功能标志来管理我们所有下一代功能的推出。我们最初的特征标记实现如下所示:
invoiceEmailer.js
const features = fetchFeatureTogglesFromSomewhere();
function generateInvoiceEmail(){
const baseEmail = buildEmailForInvoice(this.invoice);
if( features.isEnabled("next-gen-ecomm") ){
return addOrderCancellationContentToEmail(baseEmail);
}else{
return baseEmail;
}
}
在生成发票电子邮件时,我们的 InvoiceEmailler 会检查该next-gen-ecomm
功能是否已启用。如果是,那么电子邮件发送者会在电子邮件中添加一些额外的订单取消内容。
虽然这看起来是一种合理的方法,但它非常脆弱。关于是否在我们的发票电子邮件中包含订单取消功能的决定直接与next-gen-ecomm
功能绑定 - 使用魔术字符串。为什么发票电子邮件代码需要知道订单取消内容是下一代功能集的一部分?如果我们想在不暴露订单取消的情况下打开下一代功能的某些部分会怎样?或反之亦然?如果我们决定只向某些用户推出订单取消功能怎么办?随着功能的开发,这种“开关范围”更改很常见。还要记住,这些开关点往往会在整个代码库中激增。
令人高兴的是,软件中的任何问题都可以通过添加一个间接层来解决。我们可以将开关决策点与该决策背后的逻辑解耦,如下所示:
featureDecisions.js
function createFeatureDecisions(features){
return {
includeOrderCancellationInEmail(){
return features.isEnabled("next-gen-ecomm");
}
// ... additional decision functions also live here ...
};
}
invoiceEmailer.js
const features = fetchFeatureTogglesFromSomewhere();
const featureDecisions = createFeatureDecisions(features);
function generateInvoiceEmail(){
const baseEmail = buildEmailForInvoice(this.invoice);
if( featureDecisions.includeOrderCancellationInEmail() ){
return addOrderCancellationContentToEmail(baseEmail);
}else{
return baseEmail;
}
}
我们引入了一个FeatureDecisions
对象,它充当任何功能开关决策逻辑的收集点。我们为代码中的每个特定开关决策在此对象上创建一个决策方法 - 在这种情况下,“我们是否应该在发票电子邮件中包含订单取消功能”由 includeOrderCancellationInEmail
决策方法表示。
现在,决策“逻辑”是微不足道的通过传递next-gen-ecomm
检查状态,但现在随着逻辑的发展,我们有一个统一的地方来管理它。每当我们想修改特定开关决策的逻辑时,我们只需要去一个地方。
我们可能想要修改决策的范围——例如哪个特定的功能标志控制决策。或者,我们可能需要修改做出决定的原因——从由静态开关配置驱动到由 A/B 实验驱动,或者由操作问题驱动,例如我们的一些订单取消基础设施的中断。在所有情况下,我们的发票电子邮件发送者都可能会很高兴地不知道如何或为什么做出该开关决定。
4.2 反转决定
在前面的示例中,我们的发票电子邮件发送器负责询问功能标记基础设施应该如何执行。这意味着我们的发票电子邮件发送器有一个需要注意的额外概念 - 功能标记 - 以及与之耦合的额外模块。这使得发票电子邮件更难单独使用和思考,包括使其更难测试。随着功能标记在系统中变得越来越普遍,我们将看到越来越多的模块作为全局依赖项与功能标记系统耦合。不是理想的情况。
在软件设计中,我们通常可以通过应用控制反转来解决这些耦合问题。在这种情况下确实如此。以下是我们如何将我们的发票电子邮件与我们的功能标记基础设施分离:
invoiceEmailer.js
function createInvoiceEmailler(config){
return {
generateInvoiceEmail(){
const baseEmail = buildEmailForInvoice(this.invoice);
if( config.includeOrderCancellationInEmail ){
return addOrderCancellationContentToEmail(email);
}else{
return baseEmail;
}
},
// ... other invoice emailer methods ...
};
}
featureAwareFactory.js
function createFeatureAwareFactoryBasedOn(featureDecisions){
return {
invoiceEmailler(){
return createInvoiceEmailler({
includeOrderCancellationInEmail: featureDecisions.includeOrderCancellationInEmail()
});
},
// ... other factory methods ...
};
}
现在,不是我们InvoiceEmailler
接触它,而是在构建时通过一个对象 FeatureDecisions
将这些决定注入它。现在对功能标记一无所知。它只知道可以在运行时配置其行为的某些方面。这也使得 testing的行为更容易 - 我们可以通过在测试期间传递不同的配置选项来测试它生成带有和不带有订单取消内容的电子邮件的方式:configInvoiceEmaillerInvoiceEmailler
describe( 'invoice emailling', function(){
it( 'includes order cancellation content when configured to do so', function(){
// Given
const emailler = createInvoiceEmailler({includeOrderCancellationInEmail:true});
// When
const email = emailler.generateInvoiceEmail();
// Then
verifyEmailContainsOrderCancellationContent(email);
};
it( 'does not includes order cancellation content when configured to not do so', function(){
// Given
const emailler = createInvoiceEmailler({includeOrderCancellationInEmail:false});
// When
const email = emailler.generateInvoiceEmail();
// Then
verifyEmailDoesNotContainOrderCancellationContent(email);
};
});
我们还引入了一个FeatureAwareFactory
集中创建这些决策注入对象的方法。这是一般依赖注入模式的应用。如果控制反转系统在我们的代码库中发挥作用,那么我们可能会使用该系统来实现这种方法。
4.3 避免条件
到目前为止,在我们的示例中,我们的开关点是使用 if 语句实现的。这对于一个简单的、短暂的开关可能是有意义的。但是,在某个功能需要多个开关点或你希望开关点长期存在的地方,不建议使用点条件。一种更易于维护的替代方法是使用某种策略模式来实现替代代码路径:
invoiceEmailler.js
function createInvoiceEmailler(additionalContentEnhancer){
return {
generateInvoiceEmail(){
const baseEmail = buildEmailForInvoice(this.invoice);
return additionalContentEnhancer(baseEmail);
},
// ... other invoice emailer methods ...
};
}
featureAwareFactory.js
function identityFn(x){ return x; }
function createFeatureAwareFactoryBasedOn(featureDecisions){
return {
invoiceEmailler(){
if( featureDecisions.includeOrderCancellationInEmail() ){
return createInvoiceEmailler(addOrderCancellationContentToEmail);
}else{
return createInvoiceEmailler(identityFn);
}
},
// ... other factory methods ...
};
}
在这里,我们通过允许我们的发票电子邮件程序配置内容增强功能来应用策略模式。FeatureAwareFactory
在创建发票电子邮件时选择一种策略,由FeatureDecision
. 如果订单取消应该在电子邮件中,它会传递一个增强功能,将内容添加到电子邮件中。否则,它会传入一个identityFn
增强器——它没有任何效果,只是简单地将电子邮件传回而不做任何修改。
五、开关配置
5.1 动态路由与动态配置
早些时候,我们将功能开关分为对于给定的代码部署而言开关路由决策基本上是静态的,与那些决策在运行时动态变化的。需要注意的是,标志的决定可能会在运行时以两种方式发生变化,这一点很重要。
首先,像运维开关这样的东西可能会被动态重新配置从 On 到 Off 以响应系统中断。 其次,某些类别的开关(例如许可开关和实验开关)根据某些请求上下文(例如哪个用户发出请求)为每个请求做出动态路由决策。
function reticulateSplines(){
//return oldFashionedSplineReticulation();
return enhancedSplineReticulation();
}
比注释方法稍微复杂一点的是使用预处理器的#ifdef
功能,如果可用的话。
因为这种类型的硬编码不允许动态重新配置开关,它仅适用于我们愿意遵循部署代码模式以重新配置标志的功能标志。
5.5 参数化开关配置
硬编码配置提供的构建时配置对于许多用例(包括许多测试场景)来说不够灵活。至少允许在不重新构建应用程序或服务的情况下,重新配置功能标志的简单方法,是通过命令行参数或环境变量指定开关配置。这是一种简单且历史悠久的开关方法,早在有人将该技术称为功能开关或特征标记之前就已经存在。但是,它有局限性。跨大量进程协调配置可能会变得不方便,而且开关配置的更改需要重新部署或者至少重新启动进程(并且可能需要重新配置开关的人对服务器进行特权访问)。
5.6 开关配置文件
另一种选择是从某种结构化文件中读取开关配置。这种开关配置的方法很常见,它作为更通用的应用程序配置文件的一部分开始使用。
使用开关配置文件,你现在可以通过简单地更改该文件而不是重新构建应用程序代码本身来重新配置功能标志。但是,尽管在大多数情况下你不需要重新构建应用程序来开关功能,但你可能仍需要执行重新部署以重新配置标志。
5.7 开关应用数据库中的配置
一旦达到一定规模,使用静态文件来管理开关配置可能会变得很麻烦。通过文件修改配置相对繁琐。确保一组服务器的一致性成为一项挑战,使更改始终如一地更是如此。作为对此的回应,许多组织将开关配置转移到某种类型的集中式存储中,通常是现有的应用程序数据库。这通常伴随着某种形式的管理 UI 的构建,它允许系统操作员、测试人员和产品经理查看和修改功能标志及其配置。
5.8 分布式开关配置
使用已经是系统架构一部分的通用数据库来存储开关配置非常普遍;一旦引入功能标志并开始获得牵引力,这是一个明显的方式。然而,现在有一种特殊用途的分层键值存储更适合管理应用程序配置 - 像 Zookeeper、etcd 或 Consul 这样的服务。这些服务形成一个分布式集群,它为连接到集群的所有节点提供环境配置的共享源。可以在需要时动态修改配置,并且集群中的所有节点都会自动收到更改通知——这是一个非常方便的附加功能。
其中一些系统(例如 Consul)带有一个管理 UI,它提供了一种管理开关配置的基本方法。然而,在某些时候,通常会创建一个用于管理开关配置的小型自定义应用程序。
5.9 覆盖配置
到目前为止,我们的讨论假设所有配置都由单一机制提供。许多系统的实际情况更为复杂,配置的覆盖层来自各种来源。使用开关配置,具有默认配置以及特定于环境的覆盖是很常见的。这些覆盖可能来自像附加配置文件这样简单的东西,也可能来自像 Zookeeper 集群这样复杂的东西。
请注意,任何特定于环境的覆盖都与持续交付的理想背道而驰,即在交付管道中始终拥有完全相同的位和配置流。通常实用主义要求使用一些特定于环境的覆盖,但是努力使你的可部署单元和你的配置尽可能与环境无关,这将导致更简单、更安全的管道。当我们谈论测试功能开关系统时,我们将很快重新讨论这个主题。
针对每个请求覆盖
六、使用带有特征标记的系统
一个好的约定是在功能标志关闭时启用现有的或旧有的行为,而在它打开时启用新的或未来的行为。
在边缘开关
someFile.erb
<%= if featureDecisions.showRecommendationsSection? %>
<%= render 'recommendations_section' %>
<% end %>
当你控制对尚未准备好发布的面向用户的新功能的访问时,将开关点放置在边缘也很有意义。在这种情况下,你可以再次使用简单地显示或隐藏 UI 元素的开关来控制访问。例如,也许你正在构建使用 Facebook 登录应用程序的功能,但还没有准备好将其推广给用户。此功能的实现可能涉及架构各个部分的更改,但你可以通过隐藏“使用 Facebook 登录”按钮的 UI 层的简单功能开关来控制功能的公开。有趣的是,使用其中一些类型的功能标志,大部分未发布的功能本身可能实际上是公开的,但位于用户无法发现的 url 上。
在核心开关
还有其他类型的较低级别的开关必须放置在你的架构中更深的位置。这些开关通常在本质上是技术性的,并且控制着某些功能如何在内部实现。一个示例是发布开关,它控制是在第三方 API 前使用新的缓存基础设施,还是直接将请求路由到该 API。在这些情况下,在功能被开关的服务中本地化这些开关决策是唯一明智的选择。
6.6 管理功能开关的持有成本
功能标志有快速增加的趋势,特别是在首次引入时。它们有用且创建成本低,因此通常会创建很多。然而,开关确实会带来持有成本。它们要求你在代码中引入新的抽象或条件逻辑。它们还引入了显著的测试负担。骑士资本集团的4.6 亿美元错误是一个警示故事,说明当你没有正确管理功能标志时(除其他外)会出现什么问题。
精明的团队将他们的功能开关视为带有持有成本的库存,并努力将库存保持在尽可能低的水平。
精明的团队将其代码库中的功能开关视为带有持有成本的库存,并寻求将库存保持在尽可能低的水平。为了使功能标志的数量保持可管理,团队必须主动删除不再需要的功能标志。一些团队的规则是,每当首次引入发布开关时,总是将开关删除任务添加到团队的待办事项中。其他团队将“到期日期”放在他们的开关按钮上。有些人甚至会制造“定时炸弹”,如果功能标志在其到期日期之后仍然存在,它将无法通过测试(甚至拒绝启动应用程序!)。
我们还可以采用精益方法来减少库存,对系统在任何时候允许拥有的功能标志的数量进行限制。一旦达到该限制,如果有人想要添加新的开关,他们首先需要完成删除现有开关标识的工作。