功能开关

功能开关设计模式参考,了解不同类型的Feature Toggle及其最佳实践

🏷️ 开关类型
📐 设计模式
🔄 生命周期
💻 实现代码

1. Release Toggle (发布开关)

控制未完成功能的发布,允许代码合并到主分支但不对外暴露

// 隐藏未完成功能
if (featureToggle.isEnabled('new-search-algorithm')) {
  return newSearch(query);
}
return legacySearch(query);

// 发布后应尽快移除

2. Ops Toggle (运维开关)

运行时控制功能,用于降级、限流、紧急关闭

// 紧急降级
if (!featureToggle.isEnabled('payment-service')) {
  return maintenancePage();
}
processPayment(order);

// 长期存在,需要监控

3. Experiment Toggle (实验开关)

A/B测试,按用户分组展示不同功能

// A/B测试
const variant = featureToggle.getVariant('checkout-flow', userId);
if (variant === 'new-design') {
  renderNewCheckout();
} else {
  renderOldCheckout();
}

// 实验结束后移除

4. Permission Toggle (权限开关)

基于用户权限控制功能访问,如VIP功能、付费功能

// 权限控制
if (featureToggle.isEnabled('premium-analytics', { user })) {
  showAdvancedAnalytics();
} else {
  showBasicAnalytics();
  showUpgradePrompt();
}

// 长期存在

策略模式 (Strategy Pattern)

将开关逻辑封装为策略,便于扩展和测试

class FeatureStrategy {
  constructor(name) { this.name = name; }
  isEnabled(context) { return false; }
}

class BooleanStrategy extends FeatureStrategy {
  constructor(name, enabled) {
    super(name);
    this.enabled = enabled;
  }
  isEnabled() { return this.enabled; }
}

class PercentageStrategy extends FeatureStrategy {
  constructor(name, percentage) {
    super(name);
    this.percentage = percentage;
  }
  isEnabled(context) {
    const hash = hashCode(context.userId + this.name);
    return Math.abs(hash) % 100 < this.percentage;
  }
}

class UserSegmentStrategy extends FeatureStrategy {
  constructor(name, rules) {
    super(name);
    this.rules = rules;
  }
  isEnabled(context) {
    return this.rules.every(rule => 
      evaluateRule(context.user, rule)
    );
  }
}

装饰器模式 (Decorator Pattern)

用装饰器包装功能,开关控制是否启用

function withFeatureToggle(featureName, fn, fallbackFn) {
  return function(...args) {
    if (featureToggle.isEnabled(featureName)) {
      return fn.apply(this, args);
    }
    return fallbackFn ? fallbackFn.apply(this, args) : null;
  };
}

// 使用
const search = withFeatureToggle(
  'new-search',
  newSearchAlgorithm,
  legacySearchAlgorithm
);
const results = search(query);

功能开关生命周期

1

创建

定义开关名称、描述、类型和默认值

2

开发

在代码中添加开关判断,默认关闭,持续集成

3

测试

在测试环境开启开关验证功能

4

灰度发布

逐步增加流量百分比,监控指标

5

全量发布

确认稳定后开启100%流量

6

清理

移除开关代码和配置,避免技术债务

开关技术债务警告

⚠️ 过多的功能开关会导致:
• 代码复杂度指数增长 (2^n 组合)
• 测试覆盖困难
• 代码难以理解和维护

建议:
• Release Toggle: 1-2周内移除
• Experiment Toggle: 实验结束后移除
• Ops Toggle: 长期保留但需文档化
• Permission Toggle: 长期保留
• 定期审计未使用的开关

Node.js 简易实现

class FeatureToggle {
  constructor() {
    this.flags = new Map();
  }
  
  register(name, config) {
    this.flags.set(name, {
      enabled: config.enabled ?? false,
      strategy: config.strategy || 'boolean',
      percentage: config.percentage || 0,
      segments: config.segments || [],
    });
  }
  
  isEnabled(name, context = {}) {
    const flag = this.flags.get(name);
    if (!flag) return false;
    if (!flag.enabled) return false;
    
    switch (flag.strategy) {
      case 'boolean': return flag.enabled;
      case 'percentage':
        if (!context.userId) return false;
        const hash = this._hash(context.userId + name);
        return Math.abs(hash) % 100 < flag.percentage;
      case 'segment':
        return flag.segments.some(seg => 
          this._matchSegment(context, seg)
        );
      default: return flag.enabled;
    }
  }
  
  _hash(str) {
    let hash = 0;
    for (let i = 0; i < str.length; i++) {
      hash = ((hash << 5) - hash) + str.charCodeAt(i);
      hash |= 0;
    }
    return hash;
  }
  
  _matchSegment(context, segment) {
    return Object.entries(segment).every(
      ([key, value]) => context[key] === value
    );
  }
}

// 使用
const features = new FeatureToggle();
features.register('new-ui', { enabled: true, strategy: 'boolean' });
features.register('dark-mode', { enabled: true, strategy: 'percentage', percentage: 50 });
features.register('premium', { enabled: true, strategy: 'segment', segments: [{ plan: 'premium' }] });

if (features.isEnabled('new-ui')) { /* ... */ }
if (features.isEnabled('dark-mode', { userId: 'user123' })) { /* ... */ }
if (features.isEnabled('premium', { plan: 'premium' })) { /* ... */ }

React Hook

function useFeatureFlag(name, context = {}) {
  const [enabled, setEnabled] = useState(
    features.isEnabled(name, context)
  );
  
  useEffect(() => {
    // 订阅开关变更
    const unsubscribe = features.subscribe(name, (value) => {
      setEnabled(value);
    });
    return unsubscribe;
  }, [name]);
  
  return enabled;
}

// 使用
function MyComponent() {
  const showNewUI = useFeatureFlag('new-ui');
  return showNewUI ?  : ;
}