实现一个简单的特性开关

2023-04-13 16:25:34 浏览数 (1)

最近接到了一个新的需求。需求本身是一个简单的运营活动,不过这个运营活动并不是长期存在的,需要通过后台设置生效时间。

抽象一下的话就是需要通过开关来控制一个功能是否生效,也就是特性开关(Feature Flags)模式。

Martin Fowler 先生写过一篇特性开关模式的文章,感兴趣的读者可以深入阅读。

针对本次应用场景和日后的类似需求,我用 Redis 作为存储实现了一个简单的特性开关。

数据结构

定义Feature类,open属性表示特性开关是否打开,startend代表特性的生效时间(均为 None 表示该特性长期生效),

代码语言:javascript复制
from datetime import datetime
from typing import Optional

from pydantic import BaseModel
class Feature(BaseModel):
    name: str
    open: bool
    start: Optional[datetime]
    end: Optional[datetime]

设置特性开关状态

直接使用Featurename作为 key, 将Feature对象设置到Redis缓存中。

代码语言:javascript复制
from redis import Redis

client = Redis()

def build_key(name: str):
    return f'FEATURE:{name}'

def set_feature(feature: Feature, client: Redis) -> None:
    client.set(build_key(feature.name), feature.json())

读取特性开关状态

Redis缓存中查询指定nameFeature, 根据openstartend的值来判断该特性是否为开启状态。

代码语言:javascript复制
def get_feature(name: str, client: Redis) -> Feature | None:
    raw = client.get(build_key(name))
    return raw and Feature.parse_raw(raw)

def get_feature_status(name: str, date: datetime, client: Redis) -> bool:
    feature = get_feature(name, client)
    if not (feature and feature.open):
        return False
    if feature.start and feature.end and not( feature.start < date < feature.end):
        return False
    return True

我这里的get_status函数并没有直接判断当前时间是否在特性的生效时间内,而是需要显式的传入date参数(事实client参数也一直是显式传入的)。 这样的设计会确保特性开关相关的函数都是纯函数,没有任何副作用,方便编写单元测试,并且使用起来可以更灵活(例如可以切换数据源为其他数据库或直接存在内存对象中)。

使用特性开关

我们可以在代码逻辑中直接根据指定特性的状态来走不同的分支,也可以将相关接口暴露给前端,有前端根据不同的状态控制页面逻辑。

代码语言:javascript复制
def process() -> None:
    if get_feature_status(FEATURE_A, client):
        do_a()
    do_b()
代码语言:javascript复制
function Component() {
  const featureA = getFeatureFlag(FEATURE_A);
  return <>{featureA && <ComponentA />}</>;
}

使用装饰器

可以将判断特性开关状态的逻辑封装为一个装饰器,使用起来更加灵活,代码也更简洁。

代码语言:javascript复制
from functools import wraps
from typing import Callable


def do_nothing(*args, **kwargs):
    pass

def check_feature(feature_name: str):
    status = get_feature_status(feature_name, datetime.now(), client)

    def outter(func: Callable):

        @wraps(func)
        def inner(*args, **kwargs):
            return func(*args, **kwargs)

        return inner if status else do_nothing

    return outter

@check_feature(FEATURE_A)
def try_do_a():
    do_a()

def do_a():
    print("Do A")

def process():
    try_do_a()

总结

特性开关是一个不错的软件实践,适用于单分支发布的 SASS 项目,一个显著的优势是可以在功能上线前就将代码集成到主分支中(避免较晚合并代码时的痛苦),在测试环境通过打开特性开关来测试功能,同时不影响线上环境的正常使用。 感谢阅读。

0 人点赞