zumba/swivel

以策略驱动的功能开关

v5.0.0 2024-04-03 13:26 UTC

README

Build Status Coverage Status

Swivel 是对旧想法的新诠释: 功能标志(开关、位、开关等)。

典型的功能标志是 全部:要么功能对所有用户开启,要么对所有用户关闭。

// Old School Feature Flag

if ($flagIsOn) {
    // Do something new
} else {
    // Do something old
}

典型的功能标志基于很少抽象的布尔条件语句。虽然它们的简单性非常强大,但这通常会导致 循环复杂度 增加,并最终产生技术债务。

Swivel 不同

Swivel 在两个方面与典型的功能标志有根本的不同

  • 功能可以为应用用户的一个子集启用。
  • 功能不是简单的条件语句;开发者定义一个或多个策略(行为),Swivel 负责确定使用哪个策略。

使用 Swivel,用户被分成十个“桶”中的一个,允许为用户子集启用功能。这种方法的优势是显而易见的

  • 将新功能部署给 10% 的用户可以使开发者在不影响所有用户的情况下捕获新代码中未预见的错误/问题。这类部署被称为 金丝雀发布。一旦确定新代码是安全的,就可以逐步将新功能推广给更多用户(30%、50% 等);最终可以安全地为所有用户启用功能。
  • A/B 测试变得轻而易举。想象一下运行多达 9 个新功能版本,同时保留一个作为对照组。功能“A”是否会对 10% 的用户的收入指标产生负面影响?没问题:关闭它并改用版本“B”。使用 Swivel 就可以这样做。

行为

敏捷代码需要简单且易于更改。典型的功能标志允许开发者在业务规则更改或实施新功能时快速迭代,但这往往会导致复杂、设计不足、脆弱的代码块。

Swivel 鼓励开发者将业务逻辑更改实现为独立的、高级的 策略,而不是简单的、低级的偏差。

示例:快速查看

$formula = $swivel->forFeature('AwesomeSauce')
    ->addBehavior('formulaSpicy', [$this, 'getNewSpicyFormula'])
    ->addBehavior('formulaSaucy', [$this, 'getNewSaucyFormula'])
    ->defaultBehavior([$this, 'getFormula'])
    ->execute();

入门指南

你首先想做的事情是为你的应用程序中的每个用户生成一个介于 1 和 10 之间的随机数。我们称这个数字为用户的“桶”索引。这是 Swivel 用于确定对用户启用哪些功能的方法。

注意:作为一个最佳实践,一旦用户被分配到某个桶,他们应该永远留在该桶中。你将想要像存储其他基本用户信息那样存储这个值,例如在会话或 cookie 中。

接下来,你需要创建一个功能映射。这个映射表明哪些桶应该启用某些功能。以下是一个简单的功能映射示例

$map = [
    // This is a parent feature slug, arbitrarily named "Payment."
    // The "Payment" feature is enabled for users in buckets 4, 5, and 6
    'Payment' => [4,5,6],

    // This is a behavior slug.  It is a subset of the parent slug,
    // and it is only enabled for users in buckets 4 and 5
    'Payment.Test' => [4, 5],

    // Behavior slugs can be nested.
    // This one is only enabled for users in bucket 5.
    'Payment.Test.VersionA' => [5]
];

当你的应用程序启动时,配置 Swivel 并创建一个新的管理器实例

// Get this value from the session or from persistent storage.
$userBucket = 5; // $_SESSION['bucket'];

// Get the feature map data from persistent storage.
$mapData = [ 'Feature' => [4,5,6], 'Feature.Test' => [4,5] ];

// Make a new configuration object
$config = new \Zumba\Swivel\Config($mapData, $userBucket);

// Make a new Swivel Manager.  This is your primary API to the Swivel library.
$swivel = new \Zumba\Swivel\Manager($config);

干得好!Swivel 现在可以使用了。

使用策略

现在您已经创建了一个新的 Swivel 管理器,您可以在应用程序中使用它。要使用 Swivel,您需要为代码的功能定义行为;Swivel 将根据当前用户的桶和配置步骤中加载的功能映射来决定执行哪个行为。

策略示例

假设您为您的网站编写了一个新的搜索算法。您的网站搜索功能对您的业务至关重要,因此您只想首先将新算法推出给10%的用户。您决定只为桶 5 中的用户启用该算法。您配置 Swivel 并将其注册到您的应用程序中

$map = [ 'Search' => [5], 'Search.NewAlgorithm' => [5] ];
$config = new \Zumba\Swivel\Config($map, $_SESSION['bucketIndex']);
$swivel = new \Zumba\Swivel\Manager($config);

// ServiceLocator is fictional in this example.  Use your own framework or repository to store the
// swivel instance.
ServiceLocator::add('Swivel', $swivel);

在您的代码中搜索网站时,您定义了两种不同的策略,并告诉 Swivel 关于它们

public function search($params = []) {
    $swivel = ServiceLocator::get('Swivel');
    return $swivel->forFeature('Search')
        ->addBehavior('NewAlgorithm', [$this, 'awesomeSearch'], [$params])
        ->defaultBehavior([$this, 'normalSearch'], [$params])
        ->execute();
}

protected function normalSearch($params) {
    // Tried and True method.
}

protected function awesomeSearch($params) {
    // Super cool new search method.
}

现在,当您调用 search 时,Swivel 将为桶 5 中的用户执行 awesomeSearch,并为所有其他用户执行 normalSearch

Swivel API

Zumba\Swivel\Config

constructor($map, $index, $logger)

用于配置您的 Swivel 管理器实例。

示例
$features = [ 'Feature' => [1,2,3] ];
$bucket = 3;

// array
$config = new \Zumba\Swivel\Config($features, $bucket);

// \Zumba\Swivel\Map
$config = new \Zumba\Swivel\Config(new \Zumba\Swivel\Map($features), $bucket);

 // $driver implements \Zumba\Swivel\DriverInterface
$config = new \Zumba\Swivel\Config($driver, $bucket);

Zumba\Swivel\Manager

constructor($config)

这是您在应用程序中将使用的 primary Swivel 对象。

示例
$config = new \Zumba\Swivel\Config($features, $bucket);
$swivel = new \Zumba\Swivel\Manager($config);

forFeature($slug)

在您的代码中创建一个偏差点。返回一个新的 Zumba\Swivel\Builder,它接受多个行为、默认行为,并执行用户桶的适当代码。

示例
$builder = $swivel->forFeature('Test');

invoke($slug, $a, $b)

简单功能行为的简写语法糖。对于三元风格代码很有用。

示例
// without Swivel
$result = $newSearch ? $this->search() : $this->noOp();

// Zumba\Swivel\Manager::invoke
$result = $swivel->invoke('Search.New', [$this, 'search'], [$this, 'noOp']);
$result = $swivel->invoke('Search.New', [$this, 'search']);

returnValue($slug, $a, $b)

使用 Builder::addValue 调用简单功能行为的简写语法糖。对于三元风格代码很有用。

示例
// without Swivel
$result = $newSearch ? 'Everything' : null;

// Zumba\Swivel\Manager::returnValue
$result = $swivel->returnValue('Search.New', 'Everything', null);
$result = $swivel->returnValue('Search.New', 'Everything');

Zumba\Swivel\Builder

Builder API 是您编写 Swivel 代码的主要方式。当您调用 Manager::forFeature 时,您会得到一个 Builder 的新实例。

addBehavior($slug, $strategy, $args)

延迟向此功能添加行为,只有当该功能对用户的桶启用时才会执行。

示例
$builder = $swivel->forFeature('Test');

$builder
    // Inline function.  This one will return 'ab'
    ->addBehavior('versionA', function($a, $b) { return $a . $b; }, ['a', 'b'])

    // Callable.  Will return the result of $obj->someMethod('c', 'd');
    ->addBehavior('versionB', [$obj, 'someMethod'], ['c', 'd'])

     // Since version 2.0.0, this will throw a \LogicException.  Use `addValue` instead.
    ->addBehavior('versionC', 'result');

addValue($slug, $value)

延迟向此功能添加行为,它将返回提供的值。只有当该功能对用户的桶启用时才会执行。

示例
$builder = $swivel->forFeature('Test');

$builder
    // This will return `'result'`
    ->addValue('versionA', 'result')

    // Callable.  This will not be executed; Swivel will just return the unexecuted callable.
    ->addValue('versionB', [$obj, 'someMethod']);

defaultBehavior($strategy, $args)

延迟向此功能添加行为,只有当没有其他功能行为对用户的桶启用时才会执行。

示例
$swivel
    ->forFeature('Test');
    ->addBehavior('New.Version', [$this, 'someMethod'], $args)
    ->defaultBehavior([$this, 'defaultMethod'], $args);

defaultValue($value)

延迟向此功能添加行为,它将返回提供的值。只有当没有其他功能行为对用户的桶启用时才会执行。

示例
$swivel
    ->forFeature('Test');
    ->addBehavior('New.Version', [$this, 'someMethod'], $args)
    ->defaultValue('some default value');

execute()

根据用户的桶执行适当的行为策略。

示例
// $result will contain either the result of
// $this->someMethod(1, 2, 3) or $this->defaultMethod('test')
// depending on the user's bucket
$result = $swivel
    ->forFeature('Test');
    ->addBehavior('New.Version', [$this, 'someMethod'], [1, 2, 3])
    ->defaultBehavior([$this, 'defaultMethod'], ['test'])
    ->execute();

noDefault()

如果您不需要为用户桶未启用的功能定义要执行默认行为,请在 Builder 上调用 noDefault。如果忽略定义默认行为且未调用 noDefaultSwivel 将抛出 \LogicException。同样,如果同时在同一 Builder 实例上调用 noDefaultdefaultBehaviorSwivel 将抛出 \LogicException

示例
$swivel
    ->forFeature('Test');
    ->addBehavior('A', [$obj, 'someMethod'])
    ->execute(); // throws \LogicException here.

$swivel
    ->forFeature('Test');
    ->addBehavior('A', [$obj, 'someMethod'])
    ->noDefault()
    ->execute(); // no exception thrown.

$swivel
    ->forFeature('Test');
    ->addBehavior('A', [$obj, 'someMethod'])
    ->defaultBehavior([$obj, 'anotherMethod'])
    ->noDefault() // throws \LogicException here.
    ->execute();