活动架构(二):告别if-else地狱,用“策略模式”构建可插拔的玩法引擎

83 阅读6分钟

👋 大家好,我是十三!

在上一篇 《活动架构的“第一性原理”》 中,我们回归本质,建立了任务、资格、奖品三大核心基石。它们如同三块坚固的“乐高积木”,为我们提供了稳固的原子能力。

但只有零件还不够。当业务方带着层出不穷的新玩法(“大转盘抽奖”、“签到领积分”、“玩游戏解锁皮肤”)冲过来时,我们如何快速、优雅地将这些“积木”拼装起来,满足需求?这,就是本篇要解决的核心问题:构建一个能驱动业务百变的“玩法编排引擎”


1. 问题的提出:从“积木”到“成品”的挑战 🧩

我们已经有了三大独立的服务中心,现在,让我们直面第一个需求:“实现一个大转盘抽奖玩法”。

这个流程听起来很简单:

  1. 消耗用户一张“抽奖券”资格。
  2. 从奖池中随机抽取一个奖品。
  3. 为用户发放抽中的奖品。

1.1. 反模式警示:万恶的 if-else 地狱 🔥

最直观、最“朴素”的想法,就是创建一个ActivityLogic,然后用if-else来处理不同的活动类型。

// internal/logic/activity_logic.go
func (l *ActivityLogic) Execute(req *types.ActivityRequest) (*types.ActivityResponse, error) {
    // 如果是抽奖活动
    if req.ActivityType == "LOTTERY" {
        // 1. 消耗资格
        if err := l.svcCtx.QualificationService.Consume(l.ctx, req.UserId, "lottery_ticket", 1); err != nil {
            return nil, err
        }
        // 2. 抽奖 & 发奖...
        prize, err := l.svcCtx.PrizeService.DrawAndIssue(...)
        // ...
        return &types.ActivityResponse{Prize: prize}, nil

    // 如果是签到领积分活动
    } else if req.ActivityType == "SIGN_IN_REWARD" {
        // 1. 检查是否已签到 (调用任务中心)
        taskStatus, err := l.svcCtx.TaskService.GetTaskStatus(l.ctx, req.UserId, "daily_sign_in")
        if !taskStatus.IsCompleted {
             return nil, errors.New("任务未完成")
        }
        // 2. 发放固定积分 (调用奖品中心)
        err := l.svcCtx.PrizeService.Issue(l.ctx, req.UserId, "points_10")
        // ...
        return &types.ActivityResponse{Message: "积分已发放"}, nil

    // 如果未来还有N个活动...
    } else if req.ActivityType == "..." {
        // ... 无穷无尽的 else if
    }

    return nil, errors.New("不支持的活动类型")
}

这种写法的危害是毁灭性的:

  • 违反开闭原则:每次新增一种玩法,都必须冒着风险修改这个巨大的ActivityLogic文件。
  • 职责不清晰ActivityLogic成了一个“上帝类”,它知道所有玩法的实现细节,耦合了所有依赖。
  • 复杂度爆炸:随着if-else分支的增多,代码的阅读、维护、测试成本呈指数级增长。

用不了多久,这里就会变成团队中人人避之不及的“代码禁区”。我们必须寻找一种更优雅的方案。


2. 核心设计:拥抱变化的“玩法编排引擎” 🚀

2.1. 架构原则:开闭原则 (Open/Closed Principle)

破局的指导思想,是软件设计中最核心的原则之一:开闭原则

软件实体(类、模块、函数)应该对扩展开放,对修改关闭。

换言之,我们的目标是:当新增一种玩法时,我们应该通过增加新代码(扩展)来完成,而不是修改已有的旧代码(修改)

2.2. 设计模式的选择:策略模式 (Strategy Pattern)

策略模式是实现开闭原则的绝佳武器。它的定义是:

定义一系列算法,把它们一个个封装起来,并且使它们可以相互替换。

在我们的场景中:

  • 一系列算法:就是各种不同的活动玩法(抽奖、签到等)。
  • 封装:将每种玩法的业务逻辑,封装到独立的struct中。
  • 相互替换:我们的引擎可以根据活动类型,自由地切换和执行不同的玩法struct

架构图 (UML):

classDiagram
    class ActivityEngine {
        -strategies: map
        +Register(type, strategy)
        +Execute(type, context)
    }
    class IActivityStrategy {
        <<interface>>
        +Execute(context) ActivityResponse
    }
    class LotteryStrategy {
        +Execute(context) ActivityResponse
    }
    class SignInRewardStrategy {
        +Execute(context) ActivityResponse
    }

    ActivityEngine o-- IActivityStrategy : uses
    LotteryStrategy ..|> IActivityStrategy : implements
    SignInRewardStrategy ..|> IActivityStrategy : implements

2.3. Go 实现:构建引擎骨架

1. 定义策略接口 IActivityStrategy:

// internal/engine/strategy.go
package engine

type ActivityContext struct {
    UserId      int64
    ActivityId  int64
    // ... 其他上下文需要的信息
}

type ActivityResponse struct {
    // ... 玩法执行结果
}

// IActivityStrategy a.k.a. 玩法策略接口
type IActivityStrategy interface {
    Execute(ctx context.Context, activityCtx *ActivityContext) (*ActivityResponse, error)
}

2. 实现玩法引擎 ActivityEngine:

// internal/engine/engine.go
package engine

type ActivityEngine struct {
    logx.Logger
    svcCtx     *svc.ServiceContext
    strategies map[string]IActivityStrategy // 存储所有已注册的玩法策略
}

func NewActivityEngine(svcCtx *svc.ServiceContext) *ActivityEngine {
    return &ActivityEngine{
        svcCtx:     svcCtx,
        strategies: make(map[string]IActivityStrategy),
    }
}

// Register 注册玩法策略
func (e *ActivityEngine) Register(activityType string, strategy IActivityStrategy) {
    e.strategies[activityType] = strategy
}

// Execute 根据活动类型,执行对应的策略
func (e *ActivityEngine) Execute(ctx context.Context, activityType string, activityCtx *ActivityContext) (*ActivityResponse, error) {
    strategy, ok := e.strategies[activityType]
    if !ok {
        return nil, errors.New("unsupported activity type")
    }
    return strategy.Execute(ctx, activityCtx)
}

通过这个设计,ActivityEngine本身变得非常稳定,它不关心任何具体玩法。它的唯一职责,就是作为一个“路由器”,根据activityType将请求转发给正确的strategy实现。


3. 实战演练:编排两种典型玩法 🛠️

现在,我们来实现两种具体的玩法策略。

3.1. 具体策略一:大转盘抽奖 (LotteryStrategy)

// internal/engine/lottery_strategy.go
package engine

// LotteryStrategy 实现了 IActivityStrategy 接口
type LotteryStrategy struct {
    logx.Logger
    svcCtx *svc.ServiceContext
}

func (s *LotteryStrategy) Execute(ctx context.Context, activityCtx *ActivityContext) (*ActivityResponse, error) {
    // 1. 编排第一步:调用“资格中心”,消耗一张抽奖券
    if err := s.svcCtx.QualificationService.Consume(ctx, activityCtx.UserId, "lottery_ticket", 1); err != nil {
        return nil, err // 资格不足或消耗失败
    }

    // 2. 编排第二步:调用“奖品中心”,执行抽奖并扣减库存
    prize, err := s.svcCtx.PrizeService.DrawAndIssue(ctx, activityCtx.ActivityId, activityCtx.UserId)
    if err != nil {
        // 注意:这里的失败可能需要补偿逻辑(如返还资格)
        return nil, err
    }
    
    // 3. 封装并返回结果
    return &ActivityResponse{Prize: prize}, nil
}

交互时序图:

sequenceDiagram
    participant Client
    participant Engine as ActivityEngine
    participant Strategy as LotteryStrategy
    participant QualSvc as QualificationService
    participant PrizeSvc as PrizeService
    
    Client->>Engine: Execute("LOTTERY", context)
    Engine->>Strategy: Execute(context)
    Strategy->>QualSvc: Consume(userId, "lottery_ticket")
    QualSvc-->>Strategy: OK
    Strategy->>PrizeSvc: DrawAndIssue(activityId, userId)
    PrizeSvc-->>Strategy: Prize
    Strategy-->>Engine: ActivityResponse{Prize}
    Engine-->>Client: ActivityResponse{Prize}

3.2. 具体策略二:签到领积分 (SignInRewardStrategy)

这个玩法更简单,没有随机性,是确定性的奖励。

// internal/engine/signin_reward_strategy.go
package engine

type SignInRewardStrategy struct {
    logx.Logger
    svcCtx *svc.ServiceContext
}

func (s *SignInRewardStrategy) Execute(ctx context.Context, activityCtx *ActivityContext) (*ActivityResponse, error) {
    // 1. 编排第一步:检查“每日签到”任务是否已完成
    //    (注:通常此玩法由任务中心完成事件触发,这里只做演示)
    taskStatus, err := s.svcCtx.TaskService.GetTaskStatus(ctx, activityCtx.UserId, "daily_sign_in")
    if err != nil || !taskStatus.IsCompleted {
        return nil, errors.New("任务未完成,无法领奖")
    }

    // 2. 编排第二步:调用“奖品中心”发放固定奖励
    if err := s.svcCtx.PrizeService.Issue(ctx, activityCtx.UserId, "points_10"); err != nil {
        return nil, err
    }

    return &ActivityResponse{Message: "10积分已到账!"}, nil
}

现在,我们只需要在服务启动时,将这些策略注册到引擎中即可:

// main.go (或 service/servicecontext.go)
engine := engine.NewActivityEngine(svcCtx)
engine.Register("LOTTERY", &engine.LotteryStrategy{svcCtx: svcCtx})
engine.Register("SIGN_IN_REWARD", &engine.SignInRewardStrategy{svcCtx: svcCtx})

4. 总结与展望

通过引入策略模式,我们构建了一个高度可扩展的玩法编排引擎。

本篇成果:✅

  • 拥抱了变化: 现在新增一个“玩游戏领奖励”的玩法,只需要新增一个PlayGameRewardStrategy文件并注册即可,完全无需改动任何现有代码
  • 职责更清晰: ActivityEngine负责“路由”,具体的Strategy实现负责“编排”,底层的三大中心负责“执行”。各司其职,互不干扰。

最终架构图:

graph TD
    subgraph "玩法编排引擎 (本篇核心)"
        direction LR
        A[ActivityEngine] --> B{IActivityStrategy};
        C[LotteryStrategy] -- implements --> B;
        D[SignInRewardStrategy] -- implements --> B;
        E[...] -- implements --> B;
    end
    
    subgraph "三大核心基石 (第一篇)"
        F[任务中心];
        G[资格中心];
        H[奖品中心];
    end

    A -- "调度" --> F;
    A -- "调度" --> G;
    A -- "调度" --> H;

    style A fill:#9cf,stroke:#333

👨‍💻 关于十三Tech

资深服务端研发工程师,AI编程实践者。
专注分享真实的技术实践经验,相信AI是程序员的最佳搭档。
希望能和大家一起写出更优雅的代码!

📧 联系方式569893882@qq.com
🌟 GitHub@TriTechAI
💬 微信:TriTechAI(备注:十三Tech)

qrcode_for_gh_013bec198bc7_258.jpg

点个关注不迷路