TIGERB

TIGERB 查看完整档案

北京编辑天津师范大学  |  电信 编辑Xiaomi  |  Web开发 编辑 tigerb.cn 编辑
编辑

// Trying to be the person you want to be.

// 时刻夯实基础
// 时刻对新技术保持热忱

// 个人博客 http://TIGERB.cn
// 轻量级PHP框架EasyPHP 作者 http://easy-php.tigerb.cn
// 电商设计手册|SkrShop 作者 https://github.com/skr-shop/m...

// 新的目标成为一名优秀的 Gopher

个人动态

TIGERB 分享了头条 · 10月27日

千呼万唤始出来😁 SkrShop《订单中心》第1篇 🎉🎉🎉~ 尽力为你解开「电商订单系统」的面纱🤷‍♀️

赞 0 收藏 0 评论 0

TIGERB 关注了专栏 · 7月9日

人云思云

码农一只

关注 200

TIGERB 发布了文章 · 7月1日

你想知道的优惠券业务,SkrShop来告诉你

经过两年的更新「SkrShop」已经构成了下面的架构:

图中紫色的内容就是本编文章的主要内容:营销体系的基础服务「优惠券服务」。但是呢,首先要说的是关于不断被催更的事。

关于催更?

我给出了如下解释:人逢假日懒🤷‍♀️(我没错😭)、工作紧、需要保证质量,就酱。但是我一定能保证的是一直会更新下去,希望得到大家理解。

关于下期内容?

之前在Github上的Issues大家一致想看关于订单相关的内容,所以更新完本期「优惠券」之后就开始了订单之旅

Issues如下:

1. https://github.com/skr-shop/manuals/issues/25
2. https://github.com/skr-shop/manuals/issues/18

进入正题,营销体系的基础服务「优惠券服务」。通过如下问题来介绍优惠券:

  • 优惠券有哪些类型
  • 优惠券有哪些适用范围
  • 优惠券有哪些常见的场景
  • 优惠券服务要有哪些服务能力
  • 优惠券服务的风控怎么做?

优惠券有哪些类型?

对于获取优惠券的用户而言:关注的是优惠券的优惠能力,所以按优惠能力维度优惠券主要分为下面三类:

优惠能力维度描述
满减券满多少金额(不含邮费)可以减多少金额
现金券抵扣多少现金(无门槛)
抵扣券抵扣某Sku全部金额(一个数量)
折扣券打折

对于发放优惠券的运营人员而言:

一种是「固定有效期」,优惠券的生效时间戳和过期时间戳,在创建优惠券的时候已经确定。用户在任意时间领取该券,该券的有效时间都是之前设置的有效时间的开始结束时间。

另一种是「动态有效期」,创建优惠券设置的是有效时间段,比如7天有效时间、12小时有效时间等。这类优惠券以用户领取优惠券的时间为优惠券的有效时间的开始时间,以以用户领取优惠券的时间+有效时间为有效时间的结束时间。

有效期维度优惠券类型优惠券生效时间优惠券失效时间描述
固定固定有效期优惠券类型被创建时已确定优惠券类型被创建时已确定无论用户什么时间领取该优惠券,优惠券生效的时间都是设置好的统一时间
动态动态有效期用户领取优惠券时,当前时间戳用户领取优惠券时,当前时间戳 + N*24*60*60优惠券类型被创建时,只确定了该优惠券的有效,例如6小时、7天、一个月

小结如下:

优惠券有哪些适用范围?

运营策略

运营策略描述
(非)指定SkuSku券
(非)指定SpuSpu券
(非)指定类别类别券
指定店铺店铺券
全场通用平台券

适用终端

适用终端(复选框)描述
Android安卓端
iOSiOS端
PC网页电脑端
Mobile网页手机端
Wechat微信端
微信小程序微信小程序
All以上所有

适用人群

适用人群描述
白名单测试用户
会员会员专属

小结如下:

优惠券有哪些常见的场景?

领取优惠券场景

领取优惠券场景描述
活动页面大促、节假日活动页面展示获取优惠券的按钮
游戏页面通过游戏获取优惠券
店铺首页店铺首页展示领券入口
商品详情商品详情页面展示领券入口
积分中心积分兑换优惠券

展示优惠券场景

展示优惠券场景描述
活动页面大促、节假日活动页面展示可以领取的优惠券
商品详情商品详情页面展示可以领取、可以使用的优惠券列表
个人中心-我的优惠券我的优惠券列表
订单结算页面结算页面,适用该订单的优惠券列表以及推荐
积分中心展示可以兑换的优惠券详情

选择优惠券场景

选择优惠券场景描述
商品详情商品详情页面展示该用户已有的,且适用于该商品的优惠券
订单结算页面-优惠券列表选择可用优惠券结算
订单结算页面-输入优惠码输入优惠码结算

返还优惠券场景

返还优惠券场景描述
未支付订单取消未支付的订单,用户主动取消返还优惠券,或超时关单返还优惠券
已支付订单全款取消已支付的订单,订单部分退款不返还,当整个订单全部退款返还优惠券

场景示例

场景示例描述
活动页领券大促、节假日活动页面展示获取优惠券的按钮
游戏发券游戏奖励
商品页领券-
店铺页领券-
购物返券购买某个Sku,订单妥投后发放优惠券
新用户发券新用户注册发放优惠券
积分兑券积分换取优惠券

小结如下:

优惠券服务要有哪些服务能力?

服务能力1: 发放优惠券

发放方式描述
同步发放适用于用户点击领券等实时性要求较高的获取券场景
异步发放适用于实时性要求不高的发放券场景,比如新用户注册发券等场景
发放能力描述
单张发放指定一个优惠券类型ID,且指定一个UID只发一张该券
批量发放指定一个优惠券类型ID,且指定一批UID,每个UID只发一张该券
发放类型描述
优惠券类型标识通过该优惠券类型的身份标识发放,比如创建一个优惠券类型时会生成一个16位标识码,用户通过16位标识码领取优惠券;这里不使用自增ID(避免对外泄露历史创建了的优惠券数量),
优惠码code创建一个优惠券类型时,运营人员会给该券填写一个6位左右的Ascall码,比如SKR6a6,用户通过该码领取优惠券

服务能力2: 撤销优惠券

撤销能力描述
单张撤销指定一个优惠券类型ID,且指定一个UID只撤销一张该券
批量撤销指定一个优惠券类型ID,且指定一批UID,每个UID撤销一张该券

服务能力3: 查询优惠券

用户优惠券列表子类描述
全部-查询该用户所有的优惠券
可以使用全部查询该用户所有可以使用的优惠券
-适用于某个spu或sku查询该用户适用于某个spu或sku可以使用的优惠券
-适用于某个类别查询该用户适用于某个类别可以使用的优惠券
-适用于某个店铺查询该用户适用于某个店铺可以使用的优惠券
无效全部查询该用户所有无效的优惠券
-过期查询该用户所有过期的优惠券
-失效查询该用户所有失效的优惠券

服务能力4: 结算页优惠券推荐

订单结算页面推荐一张最适合该订单的优惠券

小结如下:

优惠券服务的风控怎么做?

一旦有发生风险的可能则触发风控:

  • 对用户,提示稍后再试或联系客服
  • 对内部,报警提示,核查校验报警是否存在问题

频率限制

领取描述
设备ID每天领取某优惠券的个数限制
UID每天领取某优惠券的个数限制
IP每天领取某优惠券的个数限制
使用描述
设备ID每天使用某优惠券的个数限制
UID每天使用某优惠券的个数限制
IP每天使用某优惠券的个数限制
手机号每天使用某优惠券的个数限制
邮编比如注重邮编的海外地区,每天使用某优惠券的个数限制

用户风险等级

依托用户历史订单数据,得到用户成功完成交易(比如成功妥投15天+)的比率,根据此比率对用户进行等级划分,高等级进入通行Unblock名单,低等级进入Block名单,根据不同用户级别设置限制策略。等其他大数据分析手段。

阈值

  • 发券预算
  • 实际使用券预算

根据预算值设置发券总数阈值,当触发阈值时阻断并报警。

优惠券不要支持虚拟商品

优惠券尽量不要支持虚拟商品以防止可能被利用的不法活动。


SkrShop历史分享:https://github.com/skr-shop/m...

3911642037-d2bb08d8702e7c91_articlex.jpg

查看原文

赞 1 收藏 1 评论 0

TIGERB 发布了文章 · 6月2日

客户决策 | Go语言设计模式实战

嗯,我的代码没有else系列,一个设计模式业务真实使用的golang系列。

我的代码没有else系列.jpg

前言

本系列主要分享,如何在我们的真实业务场景中使用设计模式。

本系列文章主要采用如下结构:

  • 什么是「XX设计模式」?
  • 什么真实业务场景可以使用「XX设计模式」?
  • 怎么用「XX设计模式」?

本文主要介绍「策略模式」如何在真实业务场景中使用。

什么是「策略模式」?

「策略模式」比较简单,大家平常工作中应该经常使用到,所以本文作为复习,帮助大家温故知新。我们先来看下定义:

不同的算法按照统一的标准封装,客户端根据不同的场景,决策使用何种算法。

上面的概念的关键词:

  • 算法:就是行为
  • 标准:就是interface
  • 客户端:客户端是相对的,谁调用谁就是客户端
  • 场景:判断条件
  • 决策:判断的过程

概念很容易理解,不多说。

「策略模式」的优势:

  • 典型的高内聚:算法和算法之间完全独立、互不干扰
  • 典型的松耦合:客户端依赖的是接口的抽象方法
  • 沉淀:每一个封装好的算法都是这个技术团队的财富,且未来可以被轻易的修改、复用

什么真实业务场景可以用「策略模式」?

每一行代码下面的十字路口

当代码的下一步面临选择的时候都可以使用「策略模式」,我们把不同选择的算法按照统一的标准封装,得到一类算法集的过程,就是实现「策略模式」的过程。

我们有哪些真实业务场景可以用「策略模式」呢?

比如:

  • 缓存: 使用什么样的nosql
  • 存储: 使用什么样的DB
  • 支付: 使用什么样的支付方式
  • 等等...

本文以支付接口举例,说明「策略模式」的具体使用。

怎么用「策略模式」?

关于怎么用,完全可以生搬硬套我总结的使用设计模式的四个步骤:

  • 业务梳理
  • 业务流程图
  • 代码建模
  • 代码demo

业务梳理

我们以某团的订单支付页面为例,页面上的每一个支付选项都是一个支付策略。如下:

用户可以使用:

  • 美团支付(策略)
  • 微信支付(策略)
  • 支付宝支付(策略)

用户决定使用美团支付下的银行卡支付方式的参数

用户决定使用支付宝网页版支付方式的参数

注:不一定完全准确。

业务流程图

我们通过梳理的文本业务流程得到了如下的业务流程图:

注:流程不一定完全准确。

代码建模

「策略模式」的核心是接口:

  • PaymentInterface

    • Pay(ctx *Context) error 当前支付方式的支付逻辑
    • Refund(ctx *Context) error 当前支付方式的退款逻辑

伪代码如下:

// 定义一个支付接口
- `PaymentInterface`
    + 抽象方法`Pay(ctx *Context) error`: 当前支付方式的支付逻辑
    + 抽象方法`Refund(ctx *Context) error`: 当前支付方式的退款逻辑

// 定义具体的支付方式 实现接口`PaymentInterface`

- 具体的微信支付方式`WechatPay`
    +  实现方法`Pay`: 支付逻辑
    +  实现方法`Refund`: 支付逻辑
- 具体的支付宝支付网页版方式`AliPayWap`
    +  实现方法`Pay`: 支付逻辑
    +  实现方法`Refund`: 支付逻辑
- 具体的支付宝支付网页版方式`BankPay`
    +  实现方法`Pay`: 支付逻辑
    +  实现方法`Refund`: 支付逻辑

// 客户端代码
通过接口参数pay_type的值判断是哪种支付方式策略

同时得到了我们的UML图:

代码demo

package main

import (
    "fmt"
    "runtime"
)

//------------------------------------------------------------
//我的代码没有`else`系列
//策略模式
//@auhtor TIGERB<https://github.com/TIGERB>
//------------------------------------------------------------

const (
    // ConstWechatPay 微信支付
    ConstWechatPay = "wechat_pay"
    // ConstAliPayWap 支付宝支付 网页版
    ConstAliPayWap = "AliPayWapwap"
    // ConstBankPay 银行卡支付
    ConstBankPay = "quickbank"
)

// Context 上下文
type Context struct {
    // 用户选择的支付方式
    PayType string `json:"pay_type"`
}

// PaymentInterface 支付方式接口
type PaymentInterface interface {
    Pay(ctx *Context) error    // 支付
    Refund(ctx *Context) error // 退款
}

// WechatPay 微信支付
type WechatPay struct {
}

// Pay 当前支付方式的支付逻辑
func (p *WechatPay) Pay(ctx *Context) (err error) {
    // 当前策略的业务逻辑写这
    fmt.Println(runFuncName(), "使用微信支付...")
    return
}

// Refund 当前支付方式的支付逻辑
func (p *WechatPay) Refund(ctx *Context) (err error) {
    // 当前策略的业务逻辑写这
    fmt.Println(runFuncName(), "使用微信退款...")
    return
}

// AliPayWap 支付宝网页版
type AliPayWap struct {
}

// Pay 当前支付方式的支付逻辑
func (p *AliPayWap) Pay(ctx *Context) (err error) {
    // 当前策略的业务逻辑写这
    fmt.Println(runFuncName(), "使用支付宝网页版支付...")
    return
}

// Refund 当前支付方式的支付逻辑
func (p *AliPayWap) Refund(ctx *Context) (err error) {
    // 当前策略的业务逻辑写这
    fmt.Println(runFuncName(), "使用支付宝网页版退款...")
    return
}

// BankPay 银行卡支付
type BankPay struct {
}

// Pay 当前支付方式的支付逻辑
func (p *BankPay) Pay(ctx *Context) (err error) {
    // 当前策略的业务逻辑写这
    fmt.Println(runFuncName(), "使用银行卡支付...")
    return
}

// Refund 当前支付方式的支付逻辑
func (p *BankPay) Refund(ctx *Context) (err error) {
    // 当前策略的业务逻辑写这
    fmt.Println(runFuncName(), "使用银行卡退款...")
    return
}

// 获取正在运行的函数名
func runFuncName() string {
    pc := make([]uintptr, 1)
    runtime.Callers(2, pc)
    f := runtime.FuncForPC(pc[0])
    return f.Name()
}

func main() {
    // 相对于被调用的支付策略 这里就是支付策略的客户端

    // 业务上下文
    ctx := &Context{
        PayType: "wechat_pay",
    }

    // 获取支付方式
    var instance PaymentInterface
    switch ctx.PayType {
    case ConstWechatPay:
        instance = &WechatPay{}
    case ConstAliPayWap:
        instance = &AliPayWap{}
    case ConstBankPay:
        instance = &BankPay{}
    default:
        panic("无效的支付方式")
    }

    // 支付
    instance.Pay(ctx)
}

代码运行结果:

[Running] go run "../easy-tips/go/src/patterns/strategy/strategy.go"
main.(*WechatPay).Pay 使用微信支付...

结语

最后总结下,「策略模式」抽象过程的核心是:

每一行代码下面的十字路口

  • 声明标准:定义interface
  • 封装算法:按照标准interface封装分支代码,得到每一个具体策略
  • 构建算法集:每一个具体策略构成策略池子 -> 这就是沉淀的过程
特别说明:
1. 我的代码没有`else`,只是一个在代码合理设计的情况下自然而然无限接近或者达到的结果,并不是一个硬性的目标,务必较真。
2. 本系列的一些设计模式的概念可能和原概念存在差异,因为会结合实际使用,取其精华,适当改变,灵活使用。

文章列表

我的代码没有else系列 更多文章 点击此处查看

3911642037-d2bb08d8702e7c91_articlex.jpg

查看原文

赞 12 收藏 11 评论 0

TIGERB 发布了文章 · 6月2日

状态变换 | Go语言设计模式实战

嗯,我的代码没有else系列,一个设计模式业务真实使用的golang系列。

我的代码没有else系列.jpg

前言

本系列主要分享,如何在我们的真实业务场景中使用设计模式。

本系列文章主要采用如下结构:

  • 什么是「XX设计模式」?
  • 什么真实业务场景可以使用「XX设计模式」?
  • 怎么用「XX设计模式」?

本文主要介绍「状态模式」如何在真实业务场景中使用。

「状态模式」比较简单,就是算法的选取取决于于自己的内部状态。相较于「策略模式」算法的选取由用户决策变成内部状态决策,「策略模式」是用户(客户端)选择具体的算法,「状态模式」只是通过内部不同的状态选择具体的算法。

什么是「状态模式」?

不同的算法按照统一的标准封装,根据不同的内部状态,决策使用何种算法

「状态模式」和「策略模式」的区别

  • 策略模式:依靠客户决策
  • 状态模式:依靠内部状态决策

什么真实业务场景可以用「状态模式」?

具体算法的选取是由内部状态决定的
  • 首先,内部存在多种状态
  • 其次,不同的状态的业务逻辑各不相同
我们有哪些真实业务场景可以用「状态模式」呢?

比如,发送短信接口、限流等等。

  • 短信接口

    • 服务内部根据最优算法,实时推举出最优的短信服务商,并修改使用何种短信服务商的状态
  • 限流

    • 服务内部根据当前的实时流量,选择不同的限流算法,并修改使用何种限流算法的状态

怎么用「状态模式」?

关于怎么用,完全可以生搬硬套我总结的使用设计模式的四个步骤:

  • 业务梳理
  • 业务流程图
  • 代码建模
  • 代码demo

业务梳理

先来看看一个短信验证码登录的界面。

可以得到:

  • 发送短信,用户只需要输入手机号即可
  • 至于短信服务使用何种短信服务商,是由短信服务自身的当前短信服务商实例的状态决定
  • 当前短信服务商实例的状态又是由服务自身的算法修改

业务流程图

我们通过梳理的文本业务流程得到了如下的业务流程图:

代码建模

「状态模式」的核心是:

  • 一个接口:

    • 短信服务接口SmsServiceInterface
  • 一个实体类:

    • 状态管理实体类StateManager

伪代码如下:

// 定义一个短信服务接口
- 接口`SmsServiceInterface`
    + 抽象方法`Send(ctx *Context) error`发送短信的抽象方法

// 定义具体的短信服务实体类 实现接口`SmsServiceInterface`

- 实体类`ServiceProviderAliyun`
    + 成员方法`Send(ctx *Context) error`具体的发送短信逻辑
- 实体类`ServiceProviderTencent`
    + 成员方法`Send(ctx *Context) error`具体的发送短信逻辑
- 实体类`ServiceProviderYunpian`
    + 成员方法`Send(ctx *Context) error`具体的发送短信逻辑

// 定义状态管理实体类`StateManager`
- 成员属性
    + `currentProviderType ProviderType`当前使用的服务提供商类型
    + `currentProvider SmsServiceInterface`当前使用的服务提供商实例
    + `setStateDuration time.Duration`更新状态时间间隔
- 成员方法
    + `initState(duration time.Duration)`初始化状态
    + `setState(t time.Time)`设置状态

同时得到了我们的UML图:

代码demo

package main

//------------------------------------------------------------
//我的代码没有`else`系列
//状态模式
//@auhtor TIGERB<https://github.com/TIGERB>
//------------------------------------------------------------

import (
    "fmt"
    "math/rand"
    "runtime"
    "time"
)

// Context 上下文
type Context struct {
    Tel        string // 手机号
    Text       string // 短信内容
    TemplateID string // 短信模板ID
}

// SmsServiceInterface 短信服务接口
type SmsServiceInterface interface {
    Send(ctx *Context) error
}

// ServiceProviderAliyun 阿里云
type ServiceProviderAliyun struct {
}

// Send Send
func (s *ServiceProviderAliyun) Send(ctx *Context) error {
    fmt.Println(runFuncName(), "【阿里云】短信发送成功,手机号:"+ctx.Tel)
    return nil
}

// ServiceProviderTencent 腾讯云
type ServiceProviderTencent struct {
}

// Send Send
func (s *ServiceProviderTencent) Send(ctx *Context) error {
    fmt.Println(runFuncName(), "【腾讯云】短信发送成功,手机号:"+ctx.Tel)
    return nil
}

// ServiceProviderYunpian 云片
type ServiceProviderYunpian struct {
}

// Send Send
func (s *ServiceProviderYunpian) Send(ctx *Context) error {
    fmt.Println(runFuncName(), "【云片】短信发送成功,手机号:"+ctx.Tel)
    return nil
}

// 获取正在运行的函数名
func runFuncName() string {
    pc := make([]uintptr, 1)
    runtime.Callers(2, pc)
    f := runtime.FuncForPC(pc[0])
    return f.Name()
}

// ProviderType 短信服务提供商类型
type ProviderType string

const (
    // ProviderTypeAliyun 阿里云
    ProviderTypeAliyun ProviderType = "aliyun"
    // ProviderTypeTencent 腾讯云
    ProviderTypeTencent ProviderType = "tencent"
    // ProviderTypeYunpian 云片
    ProviderTypeYunpian ProviderType = "yunpian"
)

var (
    // stateManagerInstance 当前使用的服务提供商实例
    // 默认aliyun
    stateManagerInstance *StateManager
)

// StateManager 状态管理
type StateManager struct {
    // CurrentProviderType 当前使用的服务提供商类型
    // 默认aliyun
    currentProviderType ProviderType

    // CurrentProvider 当前使用的服务提供商实例
    // 默认aliyun
    currentProvider SmsServiceInterface

    // 更新状态时间间隔
    setStateDuration time.Duration
}

// initState 初始化状态
func (m *StateManager) initState(duration time.Duration) {
    // 初始化
    m.setStateDuration = duration
    m.setState(time.Now())

    // 定时器更新状态
    go func() {
        for {
            // 每一段时间后根据回调的发送成功率 计算得到当前应该使用的 厂商
            select {
            case t := <-time.NewTicker(m.setStateDuration).C:
                m.setState(t)
            }
        }
    }()
}

// setState 设置状态
// 根据短信云商回调的短信发送成功率 得到下阶段发送短信使用哪个厂商的服务
func (m *StateManager) setState(t time.Time) {
    // 这里用随机模拟
    ProviderTypeArray := [3]ProviderType{
        ProviderTypeAliyun,
        ProviderTypeTencent,
        ProviderTypeYunpian,
    }
    m.currentProviderType = ProviderTypeArray[rand.Intn(len(ProviderTypeArray))]

    switch m.currentProviderType {
    case ProviderTypeAliyun:
        m.currentProvider = &ServiceProviderAliyun{}
    case ProviderTypeTencent:
        m.currentProvider = &ServiceProviderTencent{}
    case ProviderTypeYunpian:
        m.currentProvider = &ServiceProviderYunpian{}
    default:
        panic("无效的短信服务商")
    }
    fmt.Printf("时间:%s| 变更短信发送厂商为: %s \n", t.Format("2006-01-02 15:04:05"), m.currentProviderType)
}

// getState 获取当前状态
func (m *StateManager) getState() SmsServiceInterface {
    return m.currentProvider
}

// GetState 获取当前状态
func GetState() SmsServiceInterface {
    return stateManagerInstance.getState()
}

func main() {

    // 初始化状态管理
    stateManagerInstance = &StateManager{}
    stateManagerInstance.initState(300 * time.Millisecond)

    // 模拟发送短信的接口
    sendSms := func() {
        // 发送短信
        GetState().Send(&Context{
            Tel:        "+8613666666666",
            Text:       "3232",
            TemplateID: "TYSHK_01",
        })
    }

    // 模拟用户调用发送短信的接口
    sendSms()
    time.Sleep(1 * time.Second)
    sendSms()
    time.Sleep(1 * time.Second)
    sendSms()
    time.Sleep(1 * time.Second)
    sendSms()
    time.Sleep(1 * time.Second)
    sendSms()
}

代码运行结果:

[Running] go run "./easy-tips/go/src/patterns/state/state.go"
时间:2020-05-30 18:02:37| 变更短信发送厂商为: yunpian 
main.(*ServiceProviderYunpian).Send 【云片】短信发送成功,手机号:+8613666666666
时间:2020-05-30 18:02:37| 变更短信发送厂商为: aliyun 
时间:2020-05-30 18:02:38| 变更短信发送厂商为: yunpian 
时间:2020-05-30 18:02:38| 变更短信发送厂商为: yunpian 
main.(*ServiceProviderYunpian).Send 【云片】短信发送成功,手机号:+8613666666666
时间:2020-05-30 18:02:38| 变更短信发送厂商为: tencent 
时间:2020-05-30 18:02:39| 变更短信发送厂商为: aliyun 
时间:2020-05-30 18:02:39| 变更短信发送厂商为: tencent 
main.(*ServiceProviderTencent).Send 【腾讯云】短信发送成功,手机号:+8613666666666
时间:2020-05-30 18:02:39| 变更短信发送厂商为: yunpian 
时间:2020-05-30 18:02:40| 变更短信发送厂商为: tencent 
时间:2020-05-30 18:02:40| 变更短信发送厂商为: aliyun 
main.(*ServiceProviderAliyun).Send 【阿里云】短信发送成功,手机号:+8613666666666
时间:2020-05-30 18:02:40| 变更短信发送厂商为: yunpian 
时间:2020-05-30 18:02:40| 变更短信发送厂商为: tencent 
时间:2020-05-30 18:02:41| 变更短信发送厂商为: aliyun 
时间:2020-05-30 18:02:41| 变更短信发送厂商为: yunpian 
main.(*ServiceProviderYunpian).Send 【云片】短信发送成功,手机号:+8613666666666

结语

最后总结下,「状态模式」抽象过程的核心是:

  • 每一个状态映射对应行为
  • 行为实现同一个接口interface
  • 行为是内部的一个状态
  • 状态是不断变化的
特别说明:
1. 我的代码没有`else`,只是一个在代码合理设计的情况下自然而然无限接近或者达到的结果,并不是一个硬性的目标,务必较真。
2. 本系列的一些设计模式的概念可能和原概念存在差异,因为会结合实际使用,取其精华,适当改变,灵活使用。

文章列表

我的代码没有else系列 更多文章 点击此处查看

3911642037-d2bb08d8702e7c91_articlex.jpg

查看原文

赞 1 收藏 1 评论 0

TIGERB 分享了头条 · 5月7日

5张图送你5种秒杀系统,再加点骚操作,再顺带些点心里话🤷‍♀️。

赞 0 收藏 3 评论 0

TIGERB 发布了文章 · 5月7日

什么,秒杀系统也有这么多种!

前言

本文结构很简单:

5张图送你5种秒杀系统,再加点骚操作,再顺带些点心里话🤷‍♀️。

一个简单的秒杀系统

实现原理: 通过redis原子操作减库存

图一

优点缺点
简单好用考验redis服务能力
是否公平
公平
先到先得

我们称这类秒杀系统为:

简单秒杀系统

如果刚开始QPS并不高,redis完全抗的下来的情况,完全可以依赖这个「简单秒杀系统」。

一个够用的秒杀系统

实现原理: 服务内存限流算法 + redis原子操作减库存

图二

优点缺点
简单好用-
是否公平
不是很公平
相对的先到先得

我们称这类秒杀系统为:

够用秒杀系统

性能再好点的秒杀系统

实现原理: 服务本地内存原子操作减库存

服务本地内存的库存怎么来的?

活动开始前分配好每台机器的库存,推送到机器上。

图三

优点缺点
高性能不支持动态伸缩容(活动进行期间),因为库存是活动开始前分配好的
释放redis压力-
是否公平
不是很公平
不是绝对的先到先得

我们称这类秒杀系统为:

预备库存秒杀系统

支持动态伸缩容的秒杀系统

实现原理: 服务本地协程Coroutine定时redis原子操作减部分库存到本地内存 + 服务本地内存原子操作减库存

图四

优点缺点
高性能支持动态伸缩容(活动进行期间)
释放redis压力-
具备通用性-
是否公平
不是很公平,但是好了点
几乎先到先得

我们称这类秒杀系统为:

实时预备库存秒杀系统

公平的秒杀系统

实现原理: 服务本地Goroutine定时同步是否售罄到本地内存 + 队列 + 排队成功轮训(或主动Push)结果

图五

优点缺点
高性能开发成本高(需主动通知或轮训排队结果)
真公平-
具备通用性-
是否公平
很公平
绝对的先到先得

我们称这类秒杀系统为:

公平排队秒杀系统

骚操作

上面的秒杀系统还不够完美吗?

答案:是的。

还有什么优化的空间?

答案:静态化获取秒杀活动信息的接口。

静态化是什么意思?

答案:比如获取秒杀活动信息是通过接口 https://seckill.skrshop.tech/v1/acticity/get 获取的。现在呢,我们需要通过https://static-api.skrshop.tech/seckill/v1/acticity/get 这个接口获取。有什么区别呢?看下面:

服务名接口数据存储位置
秒杀服务https://seckill.skrshop.tech/...秒杀服务内存或redis等
接口静态化服务https://static-api.skrshop.te...CDN、本地文件

以前是这样

变成了这样

结果:可以通过接口https://static-api.skrshop.tech/seckill/v1/acticity/get就获取到了秒杀活动信息,流量都分摊到了cdn,秒杀服务自身没了这部分的负载。

小声点说:“秒杀结果我也敢推CDN😏😏😏。”
备注:
之后我们会分享`如何用Golang设计一个好用的「接口静态化服务」`。

总结

上面我们得到了如下几类秒杀系统

秒杀系统
简单秒杀系统
够用秒杀系统
预备库存秒杀系统
实时预备库存秒杀系统
公平排队秒杀系统

我想说的是里面没有最好的方案,也没有最坏的方案,只有适合你的。

先到先得来说,一定要看你们的产品对外宣传,切勿上来就追逐绝对的先到先得。其实你看所有的方案,相对而言都是“先到先得”,比如,活动开始一个小时了你再来抢,那相对于准时的用户自然抢不过,对吧。

又如预备库存秒杀系统,虽然不支持动态伸缩容。但是如果你的环境满足如下任意条件,就完全够用了。

  • 秒杀场景结束时间之快,通常几秒就结束了,真实活动可能会发生如下情况:

    • 服务压力大还没挂:根本就来不及动态伸缩容
    • 服务压力大已经挂了:可以先暂停活动,服务起来&扩容结束,用剩余库存重新推送
  • 运维自身不具备动态伸缩容的能力

所以:

合适好用就行,切勿过度设计。

最后

这次算是把老本都吐露出来了,真是慌得一匹。


SkrShop历史分享:https://github.com/skr-shop/m...

3911642037-d2bb08d8702e7c91_articlex.jpg

查看原文

赞 58 收藏 38 评论 2

TIGERB 发布了文章 · 4月12日

订阅通知 | Go语言设计模式实战

嗯,我的代码没有else系列,一个设计模式业务真实使用的golang系列。

我的代码没有else系列.jpg

前言

本系列主要分享,如何在我们的真实业务场景中使用设计模式。

本系列文章主要采用如下结构:

  • 什么是「XX设计模式」?
  • 什么真实业务场景可以使用「XX设计模式」?
  • 怎么用「XX设计模式」?

虽然本文的题目叫做“订阅通知”,但是呢,本文却主要介绍「观察者模式」如何在真实业务场景中使用。是不是有些不理解?解释下:

  • 原因一,「观察者模式」其实看起来像“订阅通知”
  • 原因二,“订阅通知”更容易被理解

什么是「观察者模式」?

观察者观察被观察者,被观察者通知观察者

我们用“订阅通知”翻译下「观察者模式」的概念,结果:

“订阅者订阅主题,主题通知订阅者”

是不是容易理解多了,我们再来拆解下这句话,得到:

  • 两个对象

    • 被观察者 -> 主题
    • 观察者 -> 订阅者
  • 两个动作

    • 订阅 -> 订阅者订阅主题
    • 通知 -> 主题发生变动通知订阅者

观察者模式的优势:

  • 高内聚 -> 不同业务代码变动互不影响
  • 可复用 -> 新的业务(就是新的订阅者)订阅不同接口(主题,就是这里的接口)
  • 极易扩展 -> 新增接口(就是新增主题);新增业务(就是新增订阅者);

其实说白了,就是分布式架构中使用消息机制MQ解耦业务的优势,是不是这么一想很容易理解了。

什么真实业务场景可以用「观察者模式」?

所有发生变更,需要通知的业务场景

详细说:只要发生了某些变化,需要通知依赖了这些变化的具体事物的业务场景。

我们有哪些真实业务场景可以用「观察者模式」呢?

比如,订单逆向流,也就是订单成立之后的各种取消操作(本文不讨论售后),主要有如下取消类型:

订单取消类型
未支付取消订单
超时关单
已支付取消订单
取消发货单
拒收

在触发这些取消操作都要进行各种各样的子操作,显而易见不同的取消操作所涉及的子操作是存在交集的。其次,已支付取消订单的子操作应该是所有订单取消类型最全的,其他类型的复用代码即可,除了分装成函数片段,还有什么更好的封装方式吗?答案:「观察者模式」。

接着我们来分析下订单逆向流业务中的不变

    • 新增取消类型
    • 新增子操作
    • 修改某个子操作的逻辑
    • 取消类型和子操作的对应关系
  • 不变

    • 已存在的取消类型
    • 已存在的子操作(在外界看来)

怎么用「观察者模式」?

关于怎么用,完全可以生搬硬套我总结的使用设计模式的四个步骤:

  • 业务梳理
  • 业务流程图
  • 代码建模
  • 代码demo

业务梳理

注:本文于单体架构背景探讨业务的实现过程,简单容易理解。

第一步,梳理出所有存在的的逆向业务的子操作,如下:

所有子操作
修改订单状态
记录订单状态变更日志
退优惠券
还优惠活动资格
还库存
还礼品卡
退钱包余额
修改发货单状态
记录发货单状态变更日志
生成退款单
生成发票-红票
发邮件
发短信
发微信消息

第二步,找到不同订单取消类型和这些子操作的关系,如下:

订单取消类型(“主题”)(被观察者)子操作(“订阅者”)(观察者)
取消未支付订单-
-修改订单状态
-记录订单状态变更日志
-退优惠券
-还优惠活动资格
-还库存
超时关单-
-修改订单状态
-记录订单状态变更日志
-退优惠券
-还优惠活动资格
-还库存
-发邮件
-发短信
-发微信消息
已支付取消订单(未生成发货单)-
-修改订单状态
-记录订单状态变更日志
-还优惠活动资格(看情况)
-还库存
-还礼品卡
-退钱包余额
-生成退款单
-生成发票-红票
-发邮件
-发短信
-发微信消息
取消发货单(未发货)-
-修改订单状态
-记录订单状态变更日志
-修改发货单状态
-记录发货单状态变更日志
-还库存
-还礼品卡
-退钱包余额
-生成退款单
-生成发票-红票
-发邮件
-发短信
-发微信消息
拒收-
-修改订单状态
-记录订单状态变更日志
-修改发货单状态
-记录发货单状态变更日志
-还库存
-还礼品卡
-退钱包余额
-生成退款单
-生成发票-红票
-发邮件
-发短信
-发微信消息
注:流程不一定完全准确、全面。

结论:

  • 不同的订单取消类型的子操作存在交集,子操作可被复用。
  • 子操作可被看作“订阅者”(也就是观察者)
  • 订单取消类型可被看作是“主题”(也就是被观察者)
  • 不同子操作(“订阅者”)(观察者)订阅订单取消类型(“主题”)(被观察者)
  • 订单取消类型(“主题”)(被观察者)通知子操作(“订阅者”)(观察者)

业务流程图

我们通过梳理的文本业务流程得到了如下的业务流程图:

注:本文于单体架构背景探讨业务的实现过程,简单容易理解。

代码建模

「观察者模式」的核心是两个接口:

  • “主题”(被观察者)接口Observable

    • 抽象方法Attach: 增加“订阅者”
    • 抽象方法Detach: 删除“订阅者”
    • 抽象方法Notify: 通知“订阅者”
  • “订阅者”(观察者)接口ObserverInterface

    • 抽象方法Do: 自身的业务

订单逆向流的业务下,我们需要实现这两个接口:

  • 具体订单取消的动作实现“主题”接口Observable
  • 子逻辑实现“订阅者”接口ObserverInterface

伪代码如下:

// ------------这里实现一个具体的“主题”------------

具体订单取消的动作实现“主题”(被观察者)接口`Observable`。得到一个具体的“主题”:

- 订单取消的动作的“主题”结构体`ObservableConcrete`
    +  成员属性`observerList []ObserverInterface`:订阅者列表
    +  具体方法`Attach`: 增加子逻辑
    +  具体方法`Detach`: 删除子逻辑
    +  具体方法`Notify`: 通知子逻辑

// ------------这里实现所有具体的“订阅者”------------

子逻辑实现“订阅者”接口`ObserverInterface`:

- 具体“订阅者”也就是子逻辑`OrderStatus`
    +  实现方法`Do`: 修改订单状态
- 具体“订阅者”也就是子逻辑`OrderStatusLog`
    +  实现方法`Do`: 记录订单状态变更日志
- 具体“订阅者”也就是子逻辑`CouponRefund`
    +  实现方法`Do`: 退优惠券
- 具体“订阅者”也就是子逻辑`PromotionRefund`
    +  实现方法`Do`: 还优惠活动资格
- 具体“订阅者”也就是子逻辑`StockRefund`
    +  实现方法`Do`: 还库存
- 具体“订阅者”也就是子逻辑`GiftCardRefund`
    +  实现方法`Do`: 还礼品卡
- 具体“订阅者”也就是子逻辑`WalletRefund`
    +  实现方法`Do`: 退钱包余额
- 具体“订阅者”也就是子逻辑`DeliverBillStatus`
    +  实现方法`Do`: 修改发货单状态
- 具体“订阅者”也就是子逻辑`DeliverBillStatusLog`
    +  实现方法`Do`: 记录发货单状态变更日志
- 具体“订阅者”也就是子逻辑`Refund`
    +  实现方法`Do`: 生成退款单
- 具体“订阅者”也就是子逻辑`Invoice`
    +  实现方法`Do`: 生成发票-红票
- 具体“订阅者”也就是子逻辑`Email`
    +  实现方法`Do`: 发邮件
- 具体“订阅者”也就是子逻辑`Sms`
    +  实现方法`Do`: 发短信
- 具体“订阅者”也就是子逻辑`WechatNotify`
    +  实现方法`Do`: 发微信消息

同时得到了我们的UML图:

代码demo

package main

//------------------------------------------------------------
//我的代码没有`else`系列
//观察者模式
//@auhtor TIGERB<https://github.com/TIGERB>
//------------------------------------------------------------

import (
    "fmt"
    "reflect"
    "runtime"
)

// Observable 被观察者
type Observable interface {
    Attach(observer ...ObserverInterface) Observable
    Detach(observer ObserverInterface) Observable
    Notify() error
}

// ObservableConcrete 一个具体的 订单状态变化的被观察者
type ObservableConcrete struct {
    observerList []ObserverInterface
}

// Attach 注册观察者
// @param $observer ObserverInterface 观察者列表
func (o *ObservableConcrete) Attach(observer ...ObserverInterface) Observable {
    o.observerList = append(o.observerList, observer...)
    return o
}

// Detach 注销观察者
// @param $observer ObserverInterface 待注销的观察者
func (o *ObservableConcrete) Detach(observer ObserverInterface) Observable {
    if len(o.observerList) == 0 {
        return o
    }
    for k, observerItem := range o.observerList {
        if observer == observerItem {
            fmt.Println(runFuncName(), "注销:", reflect.TypeOf(observer))
            o.observerList = append(o.observerList[:k], o.observerList[k+1:]...)
        }
    }
    return o
}

// Notify 通知观察者
func (o *ObservableConcrete) Notify() (err error) {
    // code ...
    for _, observer := range o.observerList {
        if err = observer.Do(o); err != nil {
            return err
        }
    }
    return nil
}

// ObserverInterface 定义一个观察者的接口
type ObserverInterface interface {
    // 自身的业务
    Do(o Observable) error
}

// OrderStatus 修改订单状态
type OrderStatus struct {
}

// Do 具体业务
func (observer *OrderStatus) Do(o Observable) (err error) {
    // code...
    fmt.Println(runFuncName(), "修改订单状态...")
    return
}

// OrderStatusLog 记录订单状态变更日志
type OrderStatusLog struct {
}

// Do 具体业务
func (observer *OrderStatusLog) Do(o Observable) (err error) {
    // code...
    fmt.Println(runFuncName(), "记录订单状态变更日志...")
    return
}

// CouponRefund 退优惠券
type CouponRefund struct {
}

// Do 具体业务
func (observer *CouponRefund) Do(o Observable) (err error) {
    // code...
    fmt.Println(runFuncName(), "退优惠券...")
    return
}

// PromotionRefund 还优惠活动资格
type PromotionRefund struct {
}

// Do 具体业务
func (observer *PromotionRefund) Do(o Observable) (err error) {
    // code...
    fmt.Println(runFuncName(), "还优惠活动资格...")
    return
}

// StockRefund 还库存
type StockRefund struct {
}

// Do 具体业务
func (observer *StockRefund) Do(o Observable) (err error) {
    // code...
    fmt.Println(runFuncName(), "还库存...")
    return
}

// GiftCardRefund 还礼品卡
type GiftCardRefund struct {
}

// Do 具体业务
func (observer *GiftCardRefund) Do(o Observable) (err error) {
    // code...
    fmt.Println(runFuncName(), "还礼品卡...")
    return
}

// WalletRefund 退钱包余额
type WalletRefund struct {
}

// Do 具体业务
func (observer *WalletRefund) Do(o Observable) (err error) {
    // code...
    fmt.Println(runFuncName(), "退钱包余额...")
    return
}

// DeliverBillStatus 修改发货单状态
type DeliverBillStatus struct {
}

// Do 具体业务
func (observer *DeliverBillStatus) Do(o Observable) (err error) {
    // code...
    fmt.Println(runFuncName(), "修改发货单状态...")
    return
}

// DeliverBillStatusLog 记录发货单状态变更日志
type DeliverBillStatusLog struct {
}

// Do 具体业务
func (observer *DeliverBillStatusLog) Do(o Observable) (err error) {
    // code...
    fmt.Println(runFuncName(), "记录发货单状态变更日志...")
    return
}

// Refund 生成退款单
type Refund struct {
}

// Do 具体业务
func (observer *Refund) Do(o Observable) (err error) {
    // code...
    fmt.Println(runFuncName(), "生成退款单...")
    return
}

// Invoice 生成发票-红票
type Invoice struct {
}

// Do 具体业务
func (observer *Invoice) Do(o Observable) (err error) {
    // code...
    fmt.Println(runFuncName(), "生成发票-红票...")
    return
}

// Email 发邮件
type Email struct {
}

// Do 具体业务
func (observer *Email) Do(o Observable) (err error) {
    // code...
    fmt.Println(runFuncName(), "发邮件...")
    return
}

// Sms 发短信
type Sms struct {
}

// Do 具体业务
func (observer *Sms) Do(o Observable) (err error) {
    // code...
    fmt.Println(runFuncName(), "发短信...")
    return
}

// WechatNotify 发微信消息
type WechatNotify struct {
}

// Do 具体业务
func (observer *WechatNotify) Do(o Observable) (err error) {
    // code...
    fmt.Println(runFuncName(), "发微信消息...")
    return
}

// 客户端调用
func main() {

    // 创建 未支付取消订单 “主题”
    fmt.Println("----------------------- 未支付取消订单 “主题”")
    orderUnPaidCancelSubject := &ObservableConcrete{}
    orderUnPaidCancelSubject.Attach(
        &OrderStatus{},
        &OrderStatusLog{},
        &CouponRefund{},
        &PromotionRefund{},
        &StockRefund{},
    )
    orderUnPaidCancelSubject.Notify()

    // 创建 超时关单 “主题”
    fmt.Println("----------------------- 超时关单 “主题”")
    orderOverTimeSubject := &ObservableConcrete{}
    orderOverTimeSubject.Attach(
        &OrderStatus{},
        &OrderStatusLog{},
        &CouponRefund{},
        &PromotionRefund{},
        &StockRefund{},
        &Email{},
        &Sms{},
        &WechatNotify{},
    )
    orderOverTimeSubject.Notify()

    // 创建 已支付取消订单 “主题”
    fmt.Println("----------------------- 已支付取消订单 “主题”")
    orderPaidCancelSubject := &ObservableConcrete{}
    orderPaidCancelSubject.Attach(
        &OrderStatus{},
        &OrderStatusLog{},
        &CouponRefund{},
        &PromotionRefund{},
        &StockRefund{},
        &GiftCardRefund{},
        &WalletRefund{},
        &Refund{},
        &Invoice{},
        &Email{},
        &Sms{},
        &WechatNotify{},
    )
    orderPaidCancelSubject.Notify()

    // 创建 取消发货单 “主题”
    fmt.Println("----------------------- 取消发货单 “主题”")
    deliverBillCancelSubject := &ObservableConcrete{}
    deliverBillCancelSubject.Attach(
        &OrderStatus{},
        &OrderStatusLog{},
        &DeliverBillStatus{},
        &DeliverBillStatusLog{},
        &StockRefund{},
        &GiftCardRefund{},
        &WalletRefund{},
        &Refund{},
        &Invoice{},
        &Email{},
        &Sms{},
        &WechatNotify{},
    )
    deliverBillCancelSubject.Notify()

    // 创建 拒收 “主题”
    fmt.Println("----------------------- 拒收 “主题”")
    deliverBillRejectSubject := &ObservableConcrete{}
    deliverBillRejectSubject.Attach(
        &OrderStatus{},
        &OrderStatusLog{},
        &DeliverBillStatus{},
        &DeliverBillStatusLog{},
        &StockRefund{},
        &GiftCardRefund{},
        &WalletRefund{},
        &Refund{},
        &Invoice{},
        &Email{},
        &Sms{},
        &WechatNotify{},
    )
    deliverBillRejectSubject.Notify()

    // 未来可以快速的根据业务的变化 创建新的主题 从而快速构建新的业务接口
    fmt.Println("----------------------- 未来的扩展...")

}

// 获取正在运行的函数名
func runFuncName() string {
    pc := make([]uintptr, 1)
    runtime.Callers(2, pc)
    f := runtime.FuncForPC(pc[0])
    return f.Name()
}

代码运行结果:

[Running] go run "../easy-tips/go/src/patterns/observer/observer.go"
----------------------- 未支付取消订单 “主题”
main.(*OrderStatus).Do 修改订单状态...
main.(*OrderStatusLog).Do 记录订单状态变更日志...
main.(*CouponRefund).Do 退优惠券...
main.(*PromotionRefund).Do 还优惠活动资格...
main.(*StockRefund).Do 还库存...
----------------------- 超时关单 “主题”
main.(*OrderStatus).Do 修改订单状态...
main.(*OrderStatusLog).Do 记录订单状态变更日志...
main.(*CouponRefund).Do 退优惠券...
main.(*PromotionRefund).Do 还优惠活动资格...
main.(*StockRefund).Do 还库存...
main.(*Email).Do 发邮件...
main.(*Sms).Do 发短信...
main.(*WechatNotify).Do 发微信消息...
----------------------- 已支付取消订单 “主题”
main.(*OrderStatus).Do 修改订单状态...
main.(*OrderStatusLog).Do 记录订单状态变更日志...
main.(*CouponRefund).Do 退优惠券...
main.(*PromotionRefund).Do 还优惠活动资格...
main.(*StockRefund).Do 还库存...
main.(*GiftCardRefund).Do 还礼品卡...
main.(*WalletRefund).Do 退钱包余额...
main.(*Refund).Do 生成退款单...
main.(*Invoice).Do 生成发票-红票...
main.(*Email).Do 发邮件...
main.(*Sms).Do 发短信...
main.(*WechatNotify).Do 发微信消息...
----------------------- 取消发货单 “主题”
main.(*OrderStatus).Do 修改订单状态...
main.(*OrderStatusLog).Do 记录订单状态变更日志...
main.(*DeliverBillStatus).Do 修改发货单状态...
main.(*DeliverBillStatusLog).Do 记录发货单状态变更日志...
main.(*StockRefund).Do 还库存...
main.(*GiftCardRefund).Do 还礼品卡...
main.(*WalletRefund).Do 退钱包余额...
main.(*Refund).Do 生成退款单...
main.(*Invoice).Do 生成发票-红票...
main.(*Email).Do 发邮件...
main.(*Sms).Do 发短信...
main.(*WechatNotify).Do 发微信消息...
----------------------- 拒收 “主题”
main.(*OrderStatus).Do 修改订单状态...
main.(*OrderStatusLog).Do 记录订单状态变更日志...
main.(*DeliverBillStatus).Do 修改发货单状态...
main.(*DeliverBillStatusLog).Do 记录发货单状态变更日志...
main.(*StockRefund).Do 还库存...
main.(*GiftCardRefund).Do 还礼品卡...
main.(*WalletRefund).Do 退钱包余额...
main.(*Refund).Do 生成退款单...
main.(*Invoice).Do 生成发票-红票...
main.(*Email).Do 发邮件...
main.(*Sms).Do 发短信...
main.(*WechatNotify).Do 发微信消息...

结语

最后总结下,「观察者模式」抽象过程的核心是:

  • 被依赖的“主题”
  • 被通知的“订阅者”
  • “订阅者”按需订阅“主题”
  • “主题”变化通知“订阅者”
特别说明:
1. 我的代码没有`else`,只是一个在代码合理设计的情况下自然而然无限接近或者达到的结果,并不是一个硬性的目标,务必较真。
2. 本系列的一些设计模式的概念可能和原概念存在差异,因为会结合实际使用,取其精华,适当改变,灵活使用。
3. 观察者模式与订阅通知实际还是有差异,本文均加上了双引号。订阅通知:订阅方不是直接依赖主题方(联想下mq等消息中间件的使用);而观察者模式:观察者是直接依赖了被观察者,从上面的代码我们也可以清晰的看出来这个差异。

文章列表

我的代码没有else系列 更多文章 点击此处查看

3911642037-d2bb08d8702e7c91_articlex.jpg

查看原文

赞 5 收藏 4 评论 0

TIGERB 发布了文章 · 4月11日

代码组件 | Go语言设计模式实战

嗯,我的代码没有else系列,一个设计模式业务真实使用的golang系列。

我的代码没有else系列.jpg

前言

本系列主要分享,如何在我们的真实业务场景中使用设计模式。

本系列文章主要采用如下结构:

  • 什么是「XX设计模式」?
  • 什么真实业务场景可以使用「XX设计模式」?
  • 怎么用「XX设计模式」?

本文主要介绍「组合模式」如何在真实业务场景中使用。

什么是「组合模式」?

一个具有层级关系的对象由一系列拥有父子关系的对象通过树形结构组成。

组合模式的优势:

  • 所见即所码:你所看见的代码结构就是业务真实的层级关系,比如Ui界面你真实看到的那样。
  • 高度封装:单一职责。
  • 可复用:不同业务场景,相同的组件可被重复使用。

什么真实业务场景可以用「组合模式」?

满足如下要求的所有场景:

Get请求获取页面数据的所有接口

前端大行组件化的当今,我们在写后端接口代码的时候还是按照业务思路一头写到尾吗?我们是否可以思索,「后端接口业务代码如何可以简单快速组件化?」,答案是肯定的,这就是「组合模式」的作用。

我们利用「组合模式」的定义和前端模块的划分去构建后端业务代码结构:

  • 前端单个模块 -> 对应后端:具体单个类 -> 封装的过程
  • 前端模块父子组件 -> 对应后端:父类内部持有多个子类(非继承关系,合成复用关系) -> 父子关系的树形结构
我们有哪些真实业务场景可以用「组合模式」呢?

比如我们以“复杂的订单结算页面”为例,下面是某东的订单结算页面:

image

从页面的展示形式上,可以看出:

  • 页面由多个模块构成,比如:

    • 地址模块
    • 支付方式模块
    • 店铺模块
    • 发票模块
    • 优惠券模块
    • 某豆模块
    • 礼品卡模块
    • 订单详细金额模块
  • 单个模块可以由多个子模块构成

    • 店铺模块,又由如下模块构成:

      • 商品模块
      • 售后模块
      • 优惠模块
      • 物流模块

怎么用「组合模式」?

关于怎么用,完全可以生搬硬套我总结的使用设计模式的四个步骤:

  • 业务梳理
  • 业务流程图
  • 代码建模
  • 代码demo

业务梳理

按照如上某东的订单结算页面的示例,我们得到了如下的订单结算页面模块组成图:

注:模块不一定完全准确

代码建模

责任链模式主要类主要包含如下特性:

  • 成员属性

    • ChildComponents: 子组件列表 -> 稳定不变的
  • 成员方法

    • Mount: 添加一个子组件 -> 稳定不变的
    • Remove: 移除一个子组件 -> 稳定不变的
    • Do: 执行组件&子组件 -> 变化的

套用到订单结算页面信息接口伪代码实现如下:

一个父类(抽象类):
- 成员属性
    + `ChildComponents`: 子组件列表
- 成员方法
    + `Mount`: 实现添加一个子组件
    + `Remove`: 实现移除一个子组件
    + `Do`: 抽象方法

组件一,订单结算页面组件类(继承父类、看成一个大的组件): 
- 成员方法
    + `Do`: 执行当前组件的逻辑,执行子组件的逻辑

组件二,地址组件(继承父类):
- 成员方法
    + `Do`: 执行当前组件的逻辑,执行子组件的逻辑

组件三,支付方式组件(继承父类):
- 成员方法
    + `Do`: 执行当前组件的逻辑,执行子组件的逻辑

组件四,店铺组件(继承父类):
- 成员方法
    + `Do`: 执行当前组件的逻辑,执行子组件的逻辑

组件五,商品组件(继承父类):
- 成员方法
    + `Do`: 执行当前组件的逻辑,执行子组件的逻辑

组件六,优惠信息组件(继承父类):
- 成员方法
    + `Do`: 执行当前组件的逻辑,执行子组件的逻辑

组件七,物流组件(继承父类):
- 成员方法
    + `Do`: 执行当前组件的逻辑,执行子组件的逻辑

组件八,发票组件(继承父类):
- 成员方法
    + `Do`: 执行当前组件的逻辑,执行子组件的逻辑

组件九,优惠券组件(继承父类):
- 成员方法
    + `Do`: 执行当前组件的逻辑,执行子组件的逻辑

组件十,礼品卡组件(继承父类):
- 成员方法
    + `Do`: 执行当前组件的逻辑,执行子组件的逻辑

组件十一,订单金额详细信息组件(继承父类):
- 成员方法
    + `Do`: 执行当前组件的逻辑,执行子组件的逻辑
组件十二,售后组件(继承父类,未来扩展的组件):
- 成员方法
    + `Do`: 执行当前组件的逻辑,执行子组件的逻辑

但是,golang里没有的继承的概念,要复用成员属性ChildComponents、成员方法Mount、成员方法Remove怎么办呢?我们使用合成复用的特性变相达到“继承复用”的目的,如下:

一个接口(interface):
+ 抽象方法`Mount`: 添加一个子组件
+ 抽象方法`Remove`: 移除一个子组件
+ 抽象方法`Do`: 执行组件&子组件

一个基础结构体`BaseComponent`:
- 成员属性
    + `ChildComponents`: 子组件列表
- 成员方法
    + 实体方法`Mount`: 添加一个子组件
    + 实体方法`Remove`: 移除一个子组件
    + 实体方法`ChildsDo`: 执行子组件

组件一,订单结算页面组件类: 
- 合成复用基础结构体`BaseComponent` 
- 成员方法
    + `Do`: 执行当前组件的逻辑,执行子组件的逻辑

组件二,地址组件:
- 合成复用基础结构体`BaseComponent` 
- 成员方法
    + `Do`: 执行当前组件的逻辑,执行子组件的逻辑

组件三,支付方式组件:
- 合成复用基础结构体`BaseComponent` 
- 成员方法
    + `Do`: 执行当前组件的逻辑,执行子组件的逻辑

...略

组件十一,订单金额详细信息组件:
- 合成复用基础结构体`BaseComponent` 
- 成员方法
    + `Do`: 执行当前组件的逻辑,执行子组件的逻辑

同时得到了我们的UML图:

代码demo

package main

import (
    "fmt"
    "reflect"
    "runtime"
)

//------------------------------------------------------------
//我的代码没有`else`系列
//组合模式
//@auhtor TIGERB<https://github.com/TIGERB>
//------------------------------------------------------------

// Context 上下文
type Context struct{}

// Component 组件接口
type Component interface {
    // 添加一个子组件
    Mount(c Component, components ...Component) error
    // 移除一个子组件
    Remove(c Component) error
    // 执行组件&子组件
    Do(ctx *Context) error
}

// BaseComponent 基础组件
// 实现Add:添加一个子组件
// 实现Remove:移除一个子组件
type BaseComponent struct {
    // 子组件列表
    ChildComponents []Component
}

// Mount 挂载一个子组件
func (bc *BaseComponent) Mount(c Component, components ...Component) (err error) {
    bc.ChildComponents = append(bc.ChildComponents, c)
    if len(components) == 0 {
        return
    }
    bc.ChildComponents = append(bc.ChildComponents, components...)
    return
}

// Remove 移除一个子组件
func (bc *BaseComponent) Remove(c Component) (err error) {
    if len(bc.ChildComponents) == 0 {
        return
    }
    for k, childComponent := range bc.ChildComponents {
        if c == childComponent {
            fmt.Println(runFuncName(), "移除:", reflect.TypeOf(childComponent))
            bc.ChildComponents = append(bc.ChildComponents[:k], bc.ChildComponents[k+1:]...)
        }
    }
    return
}

// Do 执行组件&子组件
func (bc *BaseComponent) Do(ctx *Context) (err error) {
    // do nothing
    return
}

// ChildsDo 执行子组件
func (bc *BaseComponent) ChildsDo(ctx *Context) (err error) {
    // 执行子组件
    for _, childComponent := range bc.ChildComponents {
        if err = childComponent.Do(ctx); err != nil {
            return err
        }
    }
    return
}

// CheckoutPageComponent 订单结算页面组件
type CheckoutPageComponent struct {
    // 合成复用基础组件
    BaseComponent
}

// Do 执行组件&子组件
func (bc *CheckoutPageComponent) Do(ctx *Context) (err error) {
    // 当前组件的业务逻辑写这
    fmt.Println(runFuncName(), "订单结算页面组件...")

    // 执行子组件
    bc.ChildsDo(ctx)

    // 当前组件的业务逻辑写这

    return
}

// AddressComponent 地址组件
type AddressComponent struct {
    // 合成复用基础组件
    BaseComponent
}

// Do 执行组件&子组件
func (bc *AddressComponent) Do(ctx *Context) (err error) {
    // 当前组件的业务逻辑写这
    fmt.Println(runFuncName(), "地址组件...")

    // 执行子组件
    bc.ChildsDo(ctx)

    // 当前组件的业务逻辑写这

    return
}

// PayMethodComponent 支付方式组件
type PayMethodComponent struct {
    // 合成复用基础组件
    BaseComponent
}

// Do 执行组件&子组件
func (bc *PayMethodComponent) Do(ctx *Context) (err error) {
    // 当前组件的业务逻辑写这
    fmt.Println(runFuncName(), "支付方式组件...")

    // 执行子组件
    bc.ChildsDo(ctx)

    // 当前组件的业务逻辑写这

    return
}

// StoreComponent 店铺组件
type StoreComponent struct {
    // 合成复用基础组件
    BaseComponent
}

// Do 执行组件&子组件
func (bc *StoreComponent) Do(ctx *Context) (err error) {
    // 当前组件的业务逻辑写这
    fmt.Println(runFuncName(), "店铺组件...")

    // 执行子组件
    bc.ChildsDo(ctx)

    // 当前组件的业务逻辑写这

    return
}

// SkuComponent 商品组件
type SkuComponent struct {
    // 合成复用基础组件
    BaseComponent
}

// Do 执行组件&子组件
func (bc *SkuComponent) Do(ctx *Context) (err error) {
    // 当前组件的业务逻辑写这
    fmt.Println(runFuncName(), "商品组件...")

    // 执行子组件
    bc.ChildsDo(ctx)

    // 当前组件的业务逻辑写这

    return
}

// PromotionComponent 优惠信息组件
type PromotionComponent struct {
    // 合成复用基础组件
    BaseComponent
}

// Do 执行组件&子组件
func (bc *PromotionComponent) Do(ctx *Context) (err error) {
    // 当前组件的业务逻辑写这
    fmt.Println(runFuncName(), "优惠信息组件...")

    // 执行子组件
    bc.ChildsDo(ctx)

    // 当前组件的业务逻辑写这

    return
}

// ExpressComponent 物流组件
type ExpressComponent struct {
    // 合成复用基础组件
    BaseComponent
}

// Do 执行组件&子组件
func (bc *ExpressComponent) Do(ctx *Context) (err error) {
    // 当前组件的业务逻辑写这
    fmt.Println(runFuncName(), "物流组件...")

    // 执行子组件
    bc.ChildsDo(ctx)

    // 当前组件的业务逻辑写这

    return
}

// AftersaleComponent 售后组件
type AftersaleComponent struct {
    // 合成复用基础组件
    BaseComponent
}

// Do 执行组件&子组件
func (bc *AftersaleComponent) Do(ctx *Context) (err error) {
    // 当前组件的业务逻辑写这
    fmt.Println(runFuncName(), "售后组件...")

    // 执行子组件
    bc.ChildsDo(ctx)

    // 当前组件的业务逻辑写这

    return
}

// InvoiceComponent 发票组件
type InvoiceComponent struct {
    // 合成复用基础组件
    BaseComponent
}

// Do 执行组件&子组件
func (bc *InvoiceComponent) Do(ctx *Context) (err error) {
    // 当前组件的业务逻辑写这
    fmt.Println(runFuncName(), "发票组件...")

    // 执行子组件
    bc.ChildsDo(ctx)

    // 当前组件的业务逻辑写这

    return
}

// CouponComponent 优惠券组件
type CouponComponent struct {
    // 合成复用基础组件
    BaseComponent
}

// Do 执行组件&子组件
func (bc *CouponComponent) Do(ctx *Context) (err error) {
    // 当前组件的业务逻辑写这
    fmt.Println(runFuncName(), "优惠券组件...")

    // 执行子组件
    bc.ChildsDo(ctx)

    // 当前组件的业务逻辑写这

    return
}

// GiftCardComponent 礼品卡组件
type GiftCardComponent struct {
    // 合成复用基础组件
    BaseComponent
}

// Do 执行组件&子组件
func (bc *GiftCardComponent) Do(ctx *Context) (err error) {
    // 当前组件的业务逻辑写这
    fmt.Println(runFuncName(), "礼品卡组件...")

    // 执行子组件
    bc.ChildsDo(ctx)

    // 当前组件的业务逻辑写这

    return
}

// OrderComponent 订单金额详细信息组件
type OrderComponent struct {
    // 合成复用基础组件
    BaseComponent
}

// Do 执行组件&子组件
func (bc *OrderComponent) Do(ctx *Context) (err error) {
    // 当前组件的业务逻辑写这
    fmt.Println(runFuncName(), "订单金额详细信息组件...")

    // 执行子组件
    bc.ChildsDo(ctx)

    // 当前组件的业务逻辑写这

    return
}

func main() {
    // 初始化订单结算页面 这个大组件
    checkoutPage := &CheckoutPageComponent{}

    // 挂载子组件
    storeComponent := &StoreComponent{}
    skuComponent := &SkuComponent{}
    skuComponent.Mount(
        &PromotionComponent{},
        &AftersaleComponent{},
    )
    storeComponent.Mount(
        skuComponent,
        &ExpressComponent{},
    )

    // 挂载组件
    checkoutPage.Mount(
        &AddressComponent{},
        &PayMethodComponent{},
        storeComponent,
        &InvoiceComponent{},
        &CouponComponent{},
        &GiftCardComponent{},
        &OrderComponent{},
    )

    // 移除组件测试
    // checkoutPage.Remove(storeComponent)

    // 开始构建页面组件数据
    checkoutPage.Do(&Context{})
}

// 获取正在运行的函数名
func runFuncName() string {
    pc := make([]uintptr, 1)
    runtime.Callers(2, pc)
    f := runtime.FuncForPC(pc[0])
    return f.Name()
}

代码运行结果:

[Running] go run "../easy-tips/go/src/patterns/composite/composite.go"
main.(*CheckoutPageComponent).Do 订单结算页面组件...
main.(*AddressComponent).Do 地址组件...
main.(*PayMethodComponent).Do 支付方式组件...
main.(*StoreComponent).Do 店铺组件...
main.(*SkuComponent).Do 商品组件...
main.(*PromotionComponent).Do 优惠信息组件...
main.(*AftersaleComponent).Do 售后组件...
main.(*ExpressComponent).Do 物流组件...
main.(*InvoiceComponent).Do 发票组件...
main.(*CouponComponent).Do 优惠券组件...
main.(*GiftCardComponent).Do 礼品卡组件...
main.(*OrderComponent).Do 订单金额详细信息组件...

结语

最后总结下,「组合模式」抽象过程的核心是:

  • 按模块划分:业务逻辑归类,收敛的过程。
  • 父子关系(树):把收敛之后的业务对象按父子关系绑定,依次被执行。

与「责任链模式」的区别:

  • 责任链模式: 链表
  • 组合模式:树
特别说明:
1. 我的代码没有`else`,只是一个在代码合理设计的情况下自然而然无限接近或者达到的结果,并不是一个硬性的目标,务必较真。
2. 本系列的一些设计模式的概念可能和原概念存在差异,因为会结合实际使用,取其精华,适当改变,灵活使用。
我的代码没有else系列 更多文章 点击此处查看

3911642037-d2bb08d8702e7c91_articlex.jpg

查看原文

赞 1 收藏 1 评论 0

TIGERB 发布了文章 · 4月11日

链式调用 | Go语言设计模式实战

嗯,我的代码没有else系列,一个设计模式业务真实使用的golang系列。
我的代码没有else系列.jpg

前言

本系列主要分享,如何在我们的真实业务场景中使用设计模式。

本系列文章主要采用如下结构:

  • 什么是「XX设计模式」?
  • 什么真实业务场景可以使用「XX设计模式」?
  • 怎么用「XX设计模式」?

本文主要介绍「责任链模式」如何在真实业务场景中使用。

什么是「责任链模式」?

首先把一系列业务按职责划分成不同的对象,接着把这一系列对象构成一个链,然后在这一系列对象中传递请求对象,直到被处理为止。

我们从概念中可以看出责任链模式有如下明显的优势:

  • 按职责划分:解耦
  • 对象链:逻辑清晰

但是有一点直到被处理为止,代表最终只会被一个实际的业务对象执行了实际的业务逻辑,明显适用的场景并不多。但是除此之外,上面的那两点优势还是让人很心动,所以,为了适用于目前所接触的绝大多数业务场景,把概念进行了简单的调整,如下:

首先把一系列业务按职责划分成不同的对象,接着把这一系列对象构成一个链,直到“链路结束”为止。(结束:异常结束,或链路执行完毕结束)

简单的直到“链路结束”为止转换可以让我们把责任链模式适用于任何复杂的业务场景。

以下是责任链模式的具体优势:

  • 直观:一眼可观的业务调用过程
  • 无限扩展:可无限扩展的业务逻辑
  • 高度封装:复杂业务代码依然高度封装
  • 极易被修改:复杂业务代码下修改代码只需要专注对应的业务类(结构体)文件即可,以及极易被调整的业务执行顺序

什么真实业务场景可以用「责任链模式(改)」?

满足如下要求的场景:

业务极度复杂的所有场景

任何杂乱无章的业务代码,都可以使用责任链模式(改)去重构、设计。

我们有哪些真实业务场景可以用「责任链模式(改)」呢?

比如电商系统的下单接口,随着业务发展不断的发展,该接口会充斥着各种各样的业务逻辑。

怎么用「责任链模式(改)」?

关于怎么用,完全可以生搬硬套我总结的使用设计模式的四个步骤:

  • 业务梳理
  • 业务流程图
  • 代码建模
  • 代码demo

业务梳理

步骤逻辑
1参数校验
2获取地址信息
3地址信息校验
4获取购物车数据
5获取商品库存信息
6商品库存校验
7获取优惠信息
8获取运费信息
9使用优惠信息
10扣库存
11清理购物车
12写订单表
13写订单商品表
14写订单优惠信息表
XX以及未来会增加的逻辑...

业务的不断发展变化的:

  • 新的业务被增加
  • 旧的业务被修改

比如增加的新的业务,订金预售:

  • 4|获取购物车数据后,需要校验商品参见订金预售活动的有效性等逻辑。
  • 等等逻辑
注:流程不一定完全准确

业务流程图

我们通过梳理的文本业务流程得到了如下的业务流程图:

代码建模

责任链模式主要类主要包含如下特性:

  • 成员属性

    • nextHandler: 下一个等待被调用的对象实例 -> 稳定不变的
  • 成员方法

    • SetNext: 把下一个对象的实例绑定到当前对象的nextHandler属性上 -> 稳定不变的
    • Do: 当前对象业务逻辑入口 -> 变化的
    • Run: 调用当前对象的DonextHandler不为空则调用nextHandler.Do -> 稳定不变的

套用到下单接口伪代码实现如下:

一个父类(抽象类):

- 成员属性
    + `nextHandler`: 下一个等待被调用的对象实例
- 成员方法
    + 实体方法`SetNext`: 实现把下一个对象的实例绑定到当前对象的`nextHandler`属性上
    + 抽象方法`Do`: 当前对象业务逻辑入口
    + 实体方法`Run`: 实现调用当前对象的`Do`,`nextHandler`不为空则调用`nextHandler.Do`

子类一(参数校验)
- 继承抽象类父类
- 实现抽象方法`Do`:具体的参数校验逻辑

子类二(获取地址信息)
- 继承抽象类父类
- 实现抽象方法`Do`:具体获取地址信息的逻辑

子类三(获取购物车数据)
- 继承抽象类父类
- 实现抽象方法`Do`:具体获取购物车数据的逻辑

......略

子类X(以及未来会增加的逻辑)
- 继承抽象类父类
- 实现抽象方法`Do`:以及未来会增加的逻辑

但是,golang里没有的继承的概念,要复用成员属性nextHandler、成员方法SetNext、成员方法Run怎么办呢?我们使用合成复用的特性变相达到“继承复用”的目的,如下:

一个接口(interface):

- 抽象方法`SetNext`: 待实现把下一个对象的实例绑定到当前对象的`nextHandler`属性上
- 抽象方法`Do`: 待实现当前对象业务逻辑入口
- 抽象方法`Run`: 待实现调用当前对象的`Do`,`nextHandler`不为空则调用`nextHandler.Do`

一个基础结构体:

- 成员属性
    + `nextHandler`: 下一个等待被调用的对象实例
- 成员方法
    + 实体方法`SetNext`: 实现把下一个对象的实例绑定到当前对象的`nextHandler`属性上
    + 实体方法`Run`: 实现调用当前对象的`Do`,`nextHandler`不为空则调用`nextHandler.Do`

子类一(参数校验)
- 合成复用基础结构体
- 实现抽象方法`Do`:具体的参数校验逻辑

子类二(获取地址信息)
- 合成复用基础结构体
- 实现抽象方法`Do`:具体获取地址信息的逻辑

子类三(获取购物车数据)
- 合成复用基础结构体
- 实现抽象方法`Do`:具体获取购物车数据的逻辑

......略

子类X(以及未来会增加的逻辑)
- 合成复用基础结构体
- 实现抽象方法`Do`:以及未来会增加的逻辑

同时得到了我们的UML图:

代码demo

package main

//---------------
//我的代码没有`else`系列
//责任链模式
//@auhtor TIGERB<https://github.com/TIGERB>
//---------------

import (
    "fmt"
    "runtime"
)

// Context Context
type Context struct {
}

// Handler 处理
type Handler interface {
    // 自身的业务
    Do(c *Context) error
    // 设置下一个对象
    SetNext(h Handler) Handler
    // 执行
    Run(c *Context) error
}

// Next 抽象出来的 可被合成复用的结构体
type Next struct {
    // 下一个对象
    nextHandler Handler
}

// SetNext 实现好的 可被复用的SetNext方法
// 返回值是下一个对象 方便写成链式代码优雅
// 例如 nullHandler.SetNext(argumentsHandler).SetNext(signHandler).SetNext(frequentHandler)
func (n *Next) SetNext(h Handler) Handler {
    n.nextHandler = h
    return h
}

// Run 执行
func (n *Next) Run(c *Context) (err error) {
    // 由于go无继承的概念 这里无法执行当前handler的Do
    // n.Do(c)
    if n.nextHandler != nil {
        // 合成复用下的变种
        // 执行下一个handler的Do
        if err = (n.nextHandler).Do(c); err != nil {
            return
        }
        // 执行下一个handler的Run
        return (n.nextHandler).Run(c)
    }
    return
}

// NullHandler 空Handler
// 由于go无继承的概念 作为链式调用的第一个载体 设置实际的下一个对象
type NullHandler struct {
    // 合成复用Next的`nextHandler`成员属性、`SetNext`成员方法、`Run`成员方法
    Next
}

// Do 空Handler的Do
func (h *NullHandler) Do(c *Context) (err error) {
    // 空Handler 这里什么也不做 只是载体 do nothing...
    return
}

// ArgumentsHandler 校验参数的handler
type ArgumentsHandler struct {
    // 合成复用Next
    Next
}

// Do 校验参数的逻辑
func (h *ArgumentsHandler) Do(c *Context) (err error) {
    fmt.Println(runFuncName(), "校验参数成功...")
    return
}

// AddressInfoHandler 地址信息handler
type AddressInfoHandler struct {
    // 合成复用Next
    Next
}

// Do 校验参数的逻辑
func (h *AddressInfoHandler) Do(c *Context) (err error) {
    fmt.Println(runFuncName(), "获取地址信息...")
    fmt.Println(runFuncName(), "地址信息校验...")
    return
}

// CartInfoHandler 获取购物车数据handler
type CartInfoHandler struct {
    // 合成复用Next
    Next
}

// Do 校验参数的逻辑
func (h *CartInfoHandler) Do(c *Context) (err error) {
    fmt.Println(runFuncName(), "获取购物车数据...")
    return
}

// StockInfoHandler 商品库存handler
type StockInfoHandler struct {
    // 合成复用Next
    Next
}

// Do 校验参数的逻辑
func (h *StockInfoHandler) Do(c *Context) (err error) {
    fmt.Println(runFuncName(), "获取商品库存信息...")
    fmt.Println(runFuncName(), "商品库存校验...")
    return
}

// PromotionInfoHandler 获取优惠信息handler
type PromotionInfoHandler struct {
    // 合成复用Next
    Next
}

// Do 校验参数的逻辑
func (h *PromotionInfoHandler) Do(c *Context) (err error) {
    fmt.Println(runFuncName(), "获取优惠信息...")
    return
}

// ShipmentInfoHandler 获取运费信息handler
type ShipmentInfoHandler struct {
    // 合成复用Next
    Next
}

// Do 校验参数的逻辑
func (h *ShipmentInfoHandler) Do(c *Context) (err error) {
    fmt.Println(runFuncName(), "获取运费信息...")
    return
}

// PromotionUseHandler 使用优惠信息handler
type PromotionUseHandler struct {
    // 合成复用Next
    Next
}

// Do 校验参数的逻辑
func (h *PromotionUseHandler) Do(c *Context) (err error) {
    fmt.Println(runFuncName(), "使用优惠信息...")
    return
}

// StockSubtractHandler 库存操作handler
type StockSubtractHandler struct {
    // 合成复用Next
    Next
}

// Do 校验参数的逻辑
func (h *StockSubtractHandler) Do(c *Context) (err error) {
    fmt.Println(runFuncName(), "扣库存...")
    return
}

// CartDelHandler 清理购物车handler
type CartDelHandler struct {
    // 合成复用Next
    Next
}

// Do 校验参数的逻辑
func (h *CartDelHandler) Do(c *Context) (err error) {
    fmt.Println(runFuncName(), "清理购物车...")
    // err = fmt.Errorf("CartDelHandler.Do fail")
    return
}

// DBTableOrderHandler 写订单表handler
type DBTableOrderHandler struct {
    // 合成复用Next
    Next
}

// Do 校验参数的逻辑
func (h *DBTableOrderHandler) Do(c *Context) (err error) {
    fmt.Println(runFuncName(), "写订单表...")
    return
}

// DBTableOrderSkusHandler 写订单商品表handler
type DBTableOrderSkusHandler struct {
    // 合成复用Next
    Next
}

// Do 校验参数的逻辑
func (h *DBTableOrderSkusHandler) Do(c *Context) (err error) {
    fmt.Println(runFuncName(), "写订单商品表...")
    return
}

// DBTableOrderPromotionsHandler 写订单优惠信息表handler
type DBTableOrderPromotionsHandler struct {
    // 合成复用Next
    Next
}

// Do 校验参数的逻辑
func (h *DBTableOrderPromotionsHandler) Do(c *Context) (err error) {
    fmt.Println(runFuncName(), "写订单优惠信息表...")
    return
}

// 获取正在运行的函数名
func runFuncName() string {
    pc := make([]uintptr, 1)
    runtime.Callers(2, pc)
    f := runtime.FuncForPC(pc[0])
    return f.Name()
}

func main() {
    // 初始化空handler
    nullHandler := &NullHandler{}

    // 链式调用 代码是不是很优雅
    // 很明显的链 逻辑关系一览无余
    nullHandler.SetNext(&ArgumentsHandler{}).
        SetNext(&AddressInfoHandler{}).
        SetNext(&CartInfoHandler{}).
        SetNext(&StockInfoHandler{}).
        SetNext(&PromotionInfoHandler{}).
        SetNext(&ShipmentInfoHandler{}).
        SetNext(&PromotionUseHandler{}).
        SetNext(&StockSubtractHandler{}).
        SetNext(&CartDelHandler{}).
        SetNext(&DBTableOrderHandler{}).
        SetNext(&DBTableOrderSkusHandler{}).
        SetNext(&DBTableOrderPromotionsHandler{})
        //无限扩展代码...

    // 开始执行业务
    if err := nullHandler.Run(&Context{}); err != nil {
        // 异常
        fmt.Println("Fail | Error:" + err.Error())
        return
    }
    // 成功
    fmt.Println("Success")
    return
}

代码运行结果:

[Running] go run "../easy-tips/go/src/patterns/responsibility/responsibility-order-submit.go"
main.(*ArgumentsHandler).Do 校验参数成功...
main.(*AddressInfoHandler).Do 获取地址信息...
main.(*AddressInfoHandler).Do 地址信息校验...
main.(*CartInfoHandler).Do 获取购物车数据...
main.(*StockInfoHandler).Do 获取商品库存信息...
main.(*StockInfoHandler).Do 商品库存校验...
main.(*PromotionInfoHandler).Do 获取优惠信息...
main.(*ShipmentInfoHandler).Do 获取运费信息...
main.(*PromotionUseHandler).Do 使用优惠信息...
main.(*StockSubtractHandler).Do 扣库存...
main.(*CartDelHandler).Do 清理购物车...
main.(*DBTableOrderHandler).Do 写订单表...
main.(*DBTableOrderSkusHandler).Do 写订单商品表...
main.(*DBTableOrderPromotionsHandler).Do 写订单优惠信息表...
Success

结语

最后总结下,「责任链模式(改)」抽象过程的核心是:

  • 按职责划分:业务逻辑归类,收敛的过程。
  • 对象链:把收敛之后的业务对象构成对象链,依次被执行。
特别说明:
1. 我的代码没有`else`,只是一个在代码合理设计的情况下自然而然无限接近或者达到的结果,并不是一个硬性的目标,务必较真。
2. 本系列的一些设计模式的概念可能和原概念存在差异,因为会结合实际使用,取其精华,适当改变,灵活使用。
我的代码没有else系列 更多文章 点击此处查看

3911642037-d2bb08d8702e7c91_articlex.jpg

查看原文

赞 2 收藏 2 评论 2

TIGERB 发布了文章 · 4月11日

代码模板 | Go语言设计模式实战

嗯,我的代码没有else系列,一个设计模式业务真实使用的golang系列。

我的代码没有else系列.jpg

前言

本系列主要分享,如何在我们的真实业务场景中使用设计模式。

本系列文章主要采用如下结构:

  • 什么是「XX设计模式」?
  • 什么真实业务场景可以使用「XX设计模式」?
  • 怎么用「XX设计模式」?

本文主要介绍「模板模式」如何在真实业务场景中使用。

什么是「模板模式」?

抽象类里定义好算法的执行步骤具体算法,以及可能发生变化的算法定义为抽象方法。不同的子类继承该抽象类,并实现父类的抽象方法。

模板模式的优势:

  • 不变的算法被继承复用:不变的部分高度封装、复用。
  • 变化的算法子类继承并具体实现:变化的部分子类只需要具体实现抽象的部分即可,方便扩展,且可无限扩展。

什么真实业务场景可以用「模板模式」?

满足如下要求的所有场景:

算法执行的步骤是稳定不变的,但是具体的某些算法可能存在化的场景。

怎么理解,举个例子:比如说你煮个面,必然需要先烧水,水烧开之后再放面进去,以上的流程我们称之为煮面过程。可知:这个煮面过程的步骤是稳定不变的,但是在不同的环境烧水的方式可能不尽相同,也许有的人用天然气烧水、有的人用电磁炉烧水、有的人用柴火烧水,等等。我们可以得到以下结论:

  • 煮面过程的步骤是稳定不变的
  • 煮面过程的烧水方式是可变的
我们有哪些真实业务场景可以用「模板模式」呢?

比如抽奖系统的抽奖接口,为什么:

  • 抽奖的步骤是稳定不变的 -> 不变的算法执行步骤
  • 不同抽奖类型活动在某些逻辑处理方式可能不同 -> 变的某些算法

怎么用「模板模式」?

关于怎么用,完全可以生搬硬套我总结的使用设计模式的四个步骤:

  • 业务梳理
  • 业务流程图
  • 代码建模
  • 代码demo

业务梳理

我通过历史上接触过的各种抽奖场景(红包雨、糖果雨、打地鼠、大转盘(九宫格)、考眼力、答题闯关、游戏闯关、支付刮刮乐、积分刮刮乐等等),按照真实业务需求梳理了以下抽奖业务抽奖接口的大致文本流程。

了解具体业务请点击《通用抽奖工具之需求分析 | SkrShop》

主步骤主逻辑抽奖类型子步骤子逻辑
1校验活动编号(serial_no)是否存在、并获取活动信息---
2校验活动、场次是否正在进行---
3其他参数校验(不同活动类型实现不同)---
4活动抽奖次数校验(同时扣减)---
5活动是否需要消费积分---
6场次抽奖次数校验(同时扣减)---
7获取场次奖品信息---
8获取node奖品信息(不同活动类型实现不同)按时间抽奖类型1do nothing(抽取该场次的奖品即可,无需其他逻辑)
8按抽奖次数抽奖类型1判断是该用户第几次抽奖
82获取对应node的奖品信息
83复写原所有奖品信息(抽取该node节点的奖品)
8按数额范围区间抽奖1判断属于哪个数额区间
82获取对应node的奖品信息
83复写原所有奖品信息(抽取该node节点的奖品)
9抽奖---
10奖品数量判断---
11组装奖品信息---
注:流程不一定完全准确

结论:

  • 主逻辑是稳定不变的
  • 其他参数校验获取node奖品信息的算法是可变的

业务流程图

我们通过梳理的文本业务流程得到了如下的业务流程图:

代码建模

通过上面的分析我们可以得到:

一个抽象类
- 具体共有方法`Run`,里面定义了算法的执行步骤
- 具体私有方法,不会发生变化的具体方法
- 抽象方法,会发生变化的方法

子类一(按时间抽奖类型)
- 继承抽象类父类
- 实现抽象方法

子类二(按抽奖次数抽奖类型)
- 继承抽象类父类
- 实现抽象方法

子类三(按数额范围区间抽奖)
- 继承抽象类父类
- 实现抽象方法

但是golang里面没有继承的概念,我们就把对抽象类里抽象方法的依赖转化成对接口interface里抽象方法的依赖,同时也可以利用合成复用的方式“继承”模板:

抽象行为的接口`BehaviorInterface`(包含如下需要实现的方法)
- 其他参数校验的方法`checkParams`
- 获取node奖品信息的方法`getPrizesByNode`

抽奖结构体类
- 具体共有方法`Run`,里面定义了算法的执行步骤
- 具体私有方法`checkParams` 里面的逻辑实际依赖的接口BehaviorInterface.checkParams(ctx)的抽象方法
- 具体私有方法`getPrizesByNode` 里面的逻辑实际依赖的接口BehaviorInterface.getPrizesByNode(ctx)的抽象方法
- 其他具体私有方法,不会发生变化的具体方法

实现`BehaviorInterface`的结构体一(按时间抽奖类型)
- 实现接口方法

实现`BehaviorInterface`的结构体二(按抽奖次数抽奖类型)
- 实现接口方法

实现`BehaviorInterface`的结构体三(按数额范围区间抽奖)
- 实现接口方法

同时得到了我们的UML图:

代码demo

package main

import (
    "fmt"
    "runtime"
)

//------------------------------------------------------------
//我的代码没有`else`系列
//模板模式
//@auhtor TIGERB<https://github.com/TIGERB>
//------------------------------------------------------------

const (
    // ConstActTypeTime 按时间抽奖类型
    ConstActTypeTime int32 = 1
    // ConstActTypeTimes 按抽奖次数抽奖
    ConstActTypeTimes int32 = 2
    // ConstActTypeAmount 按数额范围区间抽奖
    ConstActTypeAmount int32 = 3
)

// Context 上下文
type Context struct {
    ActInfo *ActInfo
}

// ActInfo 上下文
type ActInfo struct {
    // 活动抽奖类型1: 按时间抽奖 2: 按抽奖次数抽奖 3:按数额范围区间抽奖
    ActivityType int32
    // 其他字段略
}

// BehaviorInterface 不同抽奖类型的行为差异的抽象接口
type BehaviorInterface interface {
    // 其他参数校验(不同活动类型实现不同)
    checkParams(ctx *Context) error
    // 获取node奖品信息(不同活动类型实现不同)
    getPrizesByNode(ctx *Context) error
}

// TimeDraw 具体抽奖行为
// 按时间抽奖类型 比如红包雨
type TimeDraw struct{}

// checkParams 其他参数校验(不同活动类型实现不同)
func (draw TimeDraw) checkParams(ctx *Context) (err error) {
    fmt.Println(runFuncName(), "按时间抽奖类型:特殊参数校验...")
    return
}

// getPrizesByNode 获取node奖品信息(不同活动类型实现不同)
func (draw TimeDraw) getPrizesByNode(ctx *Context) (err error) {
    fmt.Println(runFuncName(), "do nothing(抽取该场次的奖品即可,无需其他逻辑)...")
    return
}

// TimesDraw 具体抽奖行为
// 按抽奖次数抽奖类型 比如答题闯关
type TimesDraw struct{}

// checkParams 其他参数校验(不同活动类型实现不同)
func (draw TimesDraw) checkParams(ctx *Context) (err error) {
    fmt.Println(runFuncName(), "按抽奖次数抽奖类型:特殊参数校验...")
    return
}

// getPrizesByNode 获取node奖品信息(不同活动类型实现不同)
func (draw TimesDraw) getPrizesByNode(ctx *Context) (err error) {
    fmt.Println(runFuncName(), "1. 判断是该用户第几次抽奖...")
    fmt.Println(runFuncName(), "2. 获取对应node的奖品信息...")
    fmt.Println(runFuncName(), "3. 复写原所有奖品信息(抽取该node节点的奖品)...")
    return
}

// AmountDraw 具体抽奖行为
// 按数额范围区间抽奖 比如订单金额刮奖
type AmountDraw struct{}

// checkParams 其他参数校验(不同活动类型实现不同)
func (draw *AmountDraw) checkParams(ctx *Context) (err error) {
    fmt.Println(runFuncName(), "按数额范围区间抽奖:特殊参数校验...")
    return
}

// getPrizesByNode 获取node奖品信息(不同活动类型实现不同)
func (draw *AmountDraw) getPrizesByNode(ctx *Context) (err error) {
    fmt.Println(runFuncName(), "1. 判断属于哪个数额区间...")
    fmt.Println(runFuncName(), "2. 获取对应node的奖品信息...")
    fmt.Println(runFuncName(), "3. 复写原所有奖品信息(抽取该node节点的奖品)...")
    return
}

// Lottery 抽奖模板
type Lottery struct {
    // 不同抽奖类型的抽象行为
    concreteBehavior BehaviorInterface
}

// Run 抽奖算法
// 稳定不变的算法步骤
func (lottery *Lottery) Run(ctx *Context) (err error) {
    // 具体方法:校验活动编号(serial_no)是否存在、并获取活动信息
    if err = lottery.checkSerialNo(ctx); err != nil {
    return err
    }

    // 具体方法:校验活动、场次是否正在进行
    if err = lottery.checkStatus(ctx); err != nil {
    return err
    }

    // ”抽象方法“:其他参数校验
    if err = lottery.checkParams(ctx); err != nil {
    return err
    }

    // 具体方法:活动抽奖次数校验(同时扣减)
    if err = lottery.checkTimesByAct(ctx); err != nil {
    return err
    }

    // 具体方法:活动是否需要消费积分
    if err = lottery.consumePointsByAct(ctx); err != nil {
    return err
    }

    // 具体方法:场次抽奖次数校验(同时扣减)
    if err = lottery.checkTimesBySession(ctx); err != nil {
    return err
    }

    // 具体方法:获取场次奖品信息
    if err = lottery.getPrizesBySession(ctx); err != nil {
    return err
    }

    // ”抽象方法“:获取node奖品信息
    if err = lottery.getPrizesByNode(ctx); err != nil {
    return err
    }

    // 具体方法:抽奖
    if err = lottery.drawPrizes(ctx); err != nil {
    return err
    }

    // 具体方法:奖品数量判断
    if err = lottery.checkPrizesStock(ctx); err != nil {
    return err
    }

    // 具体方法:组装奖品信息
    if err = lottery.packagePrizeInfo(ctx); err != nil {
    return err
    }
    return
}

// checkSerialNo 校验活动编号(serial_no)是否存在
func (lottery *Lottery) checkSerialNo(ctx *Context) (err error) {
    fmt.Println(runFuncName(), "校验活动编号(serial_no)是否存在、并获取活动信息...")
    // 获取活动信息伪代码
    ctx.ActInfo = &ActInfo{
    // 假设当前的活动类型为按抽奖次数抽奖
    ActivityType: ConstActTypeTimes,
    }

    // 获取当前抽奖类型的具体行为
    switch ctx.ActInfo.ActivityType {
    case 1:
    // 按时间抽奖
    lottery.concreteBehavior = &TimeDraw{}
    case 2:
    // 按抽奖次数抽奖
    lottery.concreteBehavior = &TimesDraw{}
    case 3:
    // 按数额范围区间抽奖
    lottery.concreteBehavior = &AmountDraw{}
    default:
    return fmt.Errorf("不存在的活动类型")
    }
    return
}

// checkStatus 校验活动、场次是否正在进行
func (lottery *Lottery) checkStatus(ctx *Context) (err error) {
    fmt.Println(runFuncName(), "校验活动、场次是否正在进行...")
    return
}

// checkParams 其他参数校验(不同活动类型实现不同)
// 不同场景变化的算法 转化为依赖抽象
func (lottery *Lottery) checkParams(ctx *Context) (err error) {
    // 实际依赖的接口的抽象方法
    return lottery.concreteBehavior.checkParams(ctx)
}

// checkTimesByAct 活动抽奖次数校验
func (lottery *Lottery) checkTimesByAct(ctx *Context) (err error) {
    fmt.Println(runFuncName(), "活动抽奖次数校验...")
    return
}

// consumePointsByAct 活动是否需要消费积分
func (lottery *Lottery) consumePointsByAct(ctx *Context) (err error) {
    fmt.Println(runFuncName(), "活动是否需要消费积分...")
    return
}

// checkTimesBySession 活动抽奖次数校验
func (lottery *Lottery) checkTimesBySession(ctx *Context) (err error) {
    fmt.Println(runFuncName(), "活动抽奖次数校验...")
    return
}

// getPrizesBySession 获取场次奖品信息
func (lottery *Lottery) getPrizesBySession(ctx *Context) (err error) {
    fmt.Println(runFuncName(), "获取场次奖品信息...")
    return
}

// getPrizesByNode 获取node奖品信息(不同活动类型实现不同)
// 不同场景变化的算法 转化为依赖抽象
func (lottery *Lottery) getPrizesByNode(ctx *Context) (err error) {
    // 实际依赖的接口的抽象方法
    return lottery.concreteBehavior.getPrizesByNode(ctx)
}

// drawPrizes 抽奖
func (lottery *Lottery) drawPrizes(ctx *Context) (err error) {
    fmt.Println(runFuncName(), "抽奖...")
    return
}

// checkPrizesStock 奖品数量判断
func (lottery *Lottery) checkPrizesStock(ctx *Context) (err error) {
    fmt.Println(runFuncName(), "奖品数量判断...")
    return
}

// packagePrizeInfo 组装奖品信息
func (lottery *Lottery) packagePrizeInfo(ctx *Context) (err error) {
    fmt.Println(runFuncName(), "组装奖品信息...")
    return
}

func main() {
    (&Lottery{}).Run(&Context{})
}

// 获取正在运行的函数名
func runFuncName() string {
    pc := make([]uintptr, 1)
    runtime.Callers(2, pc)
    f := runtime.FuncForPC(pc[0])
    return f.Name()
}

以下是代码执行结果:

[Running] go run ".../easy-tips/go/src/patterns/template/template.go"
main.(*Lottery).checkSerialNo 校验活动编号(serial_no)是否存在、并获取活动信息...
main.(*Lottery).checkStatus 校验活动、场次是否正在进行...
main.TimesDraw.checkParams 按抽奖次数抽奖类型:特殊参数校验...
main.(*Lottery).checkTimesByAct 活动抽奖次数校验...
main.(*Lottery).consumePointsByAct 活动是否需要消费积分...
main.(*Lottery).checkTimesBySession 活动抽奖次数校验...
main.(*Lottery).getPrizesBySession 获取场次奖品信息...
main.TimesDraw.getPrizesByNode 1. 判断是该用户第几次抽奖...
main.TimesDraw.getPrizesByNode 2. 获取对应node的奖品信息...
main.TimesDraw.getPrizesByNode 3. 复写原所有奖品信息(抽取该node节点的奖品)...
main.(*Lottery).drawPrizes 抽奖...
main.(*Lottery).checkPrizesStock 奖品数量判断...
main.(*Lottery).packagePrizeInfo 组装奖品信息...

demo代码地址:https://github.com/TIGERB/eas...

代码demo2(利用golang的合成复用特性实现)

package main

import (
    "fmt"
    "runtime"
)

//------------------------------------------------------------
//我的代码没有`else`系列
//模板模式
//@auhtor TIGERB<https://github.com/TIGERB>
//------------------------------------------------------------

const (
    // ConstActTypeTime 按时间抽奖类型
    ConstActTypeTime int32 = 1
    // ConstActTypeTimes 按抽奖次数抽奖
    ConstActTypeTimes int32 = 2
    // ConstActTypeAmount 按数额范围区间抽奖
    ConstActTypeAmount int32 = 3
)

// Context 上下文
type Context struct {
    ActInfo *ActInfo
}

// ActInfo 上下文
type ActInfo struct {
    // 活动抽奖类型1: 按时间抽奖 2: 按抽奖次数抽奖 3:按数额范围区间抽奖
    ActivityType int32
    // 其他字段略
}

// BehaviorInterface 不同抽奖类型的行为差异的抽象接口
type BehaviorInterface interface {
    // 其他参数校验(不同活动类型实现不同)
    checkParams(ctx *Context) error
    // 获取node奖品信息(不同活动类型实现不同)
    getPrizesByNode(ctx *Context) error
}

// TimeDraw 具体抽奖行为
// 按时间抽奖类型 比如红包雨
type TimeDraw struct {
    // 合成复用模板
    Lottery
}

// checkParams 其他参数校验(不同活动类型实现不同)
func (draw TimeDraw) checkParams(ctx *Context) (err error) {
    fmt.Println(runFuncName(), "按时间抽奖类型:特殊参数校验...")
    return
}

// getPrizesByNode 获取node奖品信息(不同活动类型实现不同)
func (draw TimeDraw) getPrizesByNode(ctx *Context) (err error) {
    fmt.Println(runFuncName(), "do nothing(抽取该场次的奖品即可,无需其他逻辑)...")
    return
}

// TimesDraw 具体抽奖行为
// 按抽奖次数抽奖类型 比如答题闯关
type TimesDraw struct {
    // 合成复用模板
    Lottery
}

// checkParams 其他参数校验(不同活动类型实现不同)
func (draw TimesDraw) checkParams(ctx *Context) (err error) {
    fmt.Println(runFuncName(), "按抽奖次数抽奖类型:特殊参数校验...")
    return
}

// getPrizesByNode 获取node奖品信息(不同活动类型实现不同)
func (draw TimesDraw) getPrizesByNode(ctx *Context) (err error) {
    fmt.Println(runFuncName(), "1. 判断是该用户第几次抽奖...")
    fmt.Println(runFuncName(), "2. 获取对应node的奖品信息...")
    fmt.Println(runFuncName(), "3. 复写原所有奖品信息(抽取该node节点的奖品)...")
    return
}

// AmountDraw 具体抽奖行为
// 按数额范围区间抽奖 比如订单金额刮奖
type AmountDraw struct {
    // 合成复用模板
    Lottery
}

// checkParams 其他参数校验(不同活动类型实现不同)
func (draw *AmountDraw) checkParams(ctx *Context) (err error) {
    fmt.Println(runFuncName(), "按数额范围区间抽奖:特殊参数校验...")
    return
}

// getPrizesByNode 获取node奖品信息(不同活动类型实现不同)
func (draw *AmountDraw) getPrizesByNode(ctx *Context) (err error) {
    fmt.Println(runFuncName(), "1. 判断属于哪个数额区间...")
    fmt.Println(runFuncName(), "2. 获取对应node的奖品信息...")
    fmt.Println(runFuncName(), "3. 复写原所有奖品信息(抽取该node节点的奖品)...")
    return
}

// Lottery 抽奖模板
type Lottery struct {
    // 不同抽奖类型的抽象行为
    ConcreteBehavior BehaviorInterface
}

// Run 抽奖算法
// 稳定不变的算法步骤
func (lottery *Lottery) Run(ctx *Context) (err error) {
    // 具体方法:校验活动编号(serial_no)是否存在、并获取活动信息
    if err = lottery.checkSerialNo(ctx); err != nil {
    return err
    }

    // 具体方法:校验活动、场次是否正在进行
    if err = lottery.checkStatus(ctx); err != nil {
    return err
    }

    // ”抽象方法“:其他参数校验
    if err = lottery.checkParams(ctx); err != nil {
    return err
    }

    // 具体方法:活动抽奖次数校验(同时扣减)
    if err = lottery.checkTimesByAct(ctx); err != nil {
    return err
    }

    // 具体方法:活动是否需要消费积分
    if err = lottery.consumePointsByAct(ctx); err != nil {
    return err
    }

    // 具体方法:场次抽奖次数校验(同时扣减)
    if err = lottery.checkTimesBySession(ctx); err != nil {
    return err
    }

    // 具体方法:获取场次奖品信息
    if err = lottery.getPrizesBySession(ctx); err != nil {
    return err
    }

    // ”抽象方法“:获取node奖品信息
    if err = lottery.getPrizesByNode(ctx); err != nil {
    return err
    }

    // 具体方法:抽奖
    if err = lottery.drawPrizes(ctx); err != nil {
    return err
    }

    // 具体方法:奖品数量判断
    if err = lottery.checkPrizesStock(ctx); err != nil {
    return err
    }

    // 具体方法:组装奖品信息
    if err = lottery.packagePrizeInfo(ctx); err != nil {
    return err
    }
    return
}

// checkSerialNo 校验活动编号(serial_no)是否存在
func (lottery *Lottery) checkSerialNo(ctx *Context) (err error) {
    fmt.Println(runFuncName(), "校验活动编号(serial_no)是否存在、并获取活动信息...")
    return
}

// checkStatus 校验活动、场次是否正在进行
func (lottery *Lottery) checkStatus(ctx *Context) (err error) {
    fmt.Println(runFuncName(), "校验活动、场次是否正在进行...")
    return
}

// checkParams 其他参数校验(不同活动类型实现不同)
// 不同场景变化的算法 转化为依赖抽象
func (lottery *Lottery) checkParams(ctx *Context) (err error) {
    // 实际依赖的接口的抽象方法
    return lottery.ConcreteBehavior.checkParams(ctx)
}

// checkTimesByAct 活动抽奖次数校验
func (lottery *Lottery) checkTimesByAct(ctx *Context) (err error) {
    fmt.Println(runFuncName(), "活动抽奖次数校验...")
    return
}

// consumePointsByAct 活动是否需要消费积分
func (lottery *Lottery) consumePointsByAct(ctx *Context) (err error) {
    fmt.Println(runFuncName(), "活动是否需要消费积分...")
    return
}

// checkTimesBySession 活动抽奖次数校验
func (lottery *Lottery) checkTimesBySession(ctx *Context) (err error) {
    fmt.Println(runFuncName(), "活动抽奖次数校验...")
    return
}

// getPrizesBySession 获取场次奖品信息
func (lottery *Lottery) getPrizesBySession(ctx *Context) (err error) {
    fmt.Println(runFuncName(), "获取场次奖品信息...")
    return
}

// getPrizesByNode 获取node奖品信息(不同活动类型实现不同)
// 不同场景变化的算法 转化为依赖抽象
func (lottery *Lottery) getPrizesByNode(ctx *Context) (err error) {
    // 实际依赖的接口的抽象方法
    return lottery.ConcreteBehavior.getPrizesByNode(ctx)
}

// drawPrizes 抽奖
func (lottery *Lottery) drawPrizes(ctx *Context) (err error) {
    fmt.Println(runFuncName(), "抽奖...")
    return
}

// checkPrizesStock 奖品数量判断
func (lottery *Lottery) checkPrizesStock(ctx *Context) (err error) {
    fmt.Println(runFuncName(), "奖品数量判断...")
    return
}

// packagePrizeInfo 组装奖品信息
func (lottery *Lottery) packagePrizeInfo(ctx *Context) (err error) {
    fmt.Println(runFuncName(), "组装奖品信息...")
    return
}

func main() {
    ctx := &Context{
    ActInfo: &ActInfo{
    ActivityType: ConstActTypeAmount,
    },
    }

    switch ctx.ActInfo.ActivityType {
    case ConstActTypeTime: // 按时间抽奖类型
    instance := &TimeDraw{}
    instance.ConcreteBehavior = instance
    instance.Run(ctx)
    case ConstActTypeTimes: // 按抽奖次数抽奖
    instance := &TimesDraw{}
    instance.ConcreteBehavior = instance
    instance.Run(ctx)
    case ConstActTypeAmount: // 按数额范围区间抽奖
    instance := &AmountDraw{}
    instance.ConcreteBehavior = instance
    instance.Run(ctx)
    default:
    // 报错
    return
    }
}

// 获取正在运行的函数名
func runFuncName() string {
    pc := make([]uintptr, 1)
    runtime.Callers(2, pc)
    f := runtime.FuncForPC(pc[0])
    return f.Name()
}

以下是代码执行结果:

[Running] go run ".../easy-tips/go/src/patterns/template/templateOther.go"
main.(*Lottery).checkSerialNo 校验活动编号(serial_no)是否存在、并获取活动信息...
main.(*Lottery).checkStatus 校验活动、场次是否正在进行...
main.(*AmountDraw).checkParams 按数额范围区间抽奖:特殊参数校验...
main.(*Lottery).checkTimesByAct 活动抽奖次数校验...
main.(*Lottery).consumePointsByAct 活动是否需要消费积分...
main.(*Lottery).checkTimesBySession 活动抽奖次数校验...
main.(*Lottery).getPrizesBySession 获取场次奖品信息...
main.(*AmountDraw).getPrizesByNode 1. 判断属于哪个数额区间...
main.(*AmountDraw).getPrizesByNode 2. 获取对应node的奖品信息...
main.(*AmountDraw).getPrizesByNode 3. 复写原所有奖品信息(抽取该node节点的奖品)...
main.(*Lottery).drawPrizes 抽奖...
main.(*Lottery).checkPrizesStock 奖品数量判断...
main.(*Lottery).packagePrizeInfo 组装奖品信息...

demo2代码地址:https://github.com/TIGERB/eas...

结语

最后总结下,「模板模式」抽象过程的核心是把握不变

  • 不变:Run方法里的抽奖步骤 -> 被继承复用
  • 变:不同场景下 -> 被具体实现

    • checkParams参数校验逻辑
    • getPrizesByNode获取该节点奖品的逻辑
特别说明:
1. 我的代码没有`else`,只是一个在代码合理设计的情况下自然而然无限接近或者达到的结果,并不是一个硬性的目标,务必较真。
2. 本系列的一些设计模式的概念可能和原概念存在差异,因为会结合实际使用,取其精华,适当改变,灵活使用。
我的代码没有else系列 更多文章 点击此处查看

3911642037-d2bb08d8702e7c91_articlex.jpg

查看原文

赞 0 收藏 0 评论 1

TIGERB 分享了头条 · 4月3日

嗯,我的代码没有else系列,一个设计模式业务真实使用的golang系列。

赞 2 收藏 2 评论 0

TIGERB 分享了头条 · 3月31日

嗯,我的代码没有else系列,一个设计模式业务真实使用的golang系列。

赞 1 收藏 2 评论 0

TIGERB 发布了文章 · 1月2日

通用抽奖工具之系统设计

前言

上篇文章《通用抽奖工具之需求分析》我们已经通过一些常见的抽奖场景,得到了符合这些抽奖场景的抽奖工具五要素:

抽奖五要素要素名称
第一要素活动
第二要素场次
第三要素奖品
第四要素中奖概率
第五要素均匀投奖

以及创建一个抽奖活动的5个基本步骤,如下:

  1. 活动配置
  2. 场次配置
  3. 奖品配置
  4. 奖品中奖概率配置
  5. 奖品投奖配置
上篇文章回顾 《通用抽奖工具之需求分析》

需求已经分析完了,今天我们就来看看这通用抽奖工具具体的设计,分为如下三个部分:

  • DB设计
  • 配置后台设计
  • 接口设计

DB设计

第一要素活动配置抽奖活动表

-- 通用抽奖工具(万能胶Glue) glue_activity 抽奖活动表
CREATE TABLE `glue_activity` (
    `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '活动ID',
    `serial_no` char(16) unsigned NOT NULL DEFAULT '' COMMENT '活动编号(md5值中间16位)',
    `name` varchar(255) unsigned NOT NULL DEFAULT '' COMMENT '活动名称',
    `description` varchar(255) unsigned NOT NULL DEFAULT '' COMMENT '活动描述',
    `activity_type` tinyint(1) unsigned NOT NULL DEFAULT '1' COMMENT '活动抽奖类型1: 按时间抽奖 2: 按抽奖次数抽奖 3:按数额范围区间抽奖',
    `probability_type` tinyint(1) unsigned NOT NULL DEFAULT '1' COMMENT '中奖概率类型1: static 2: dynamic',
    `times_limit` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '抽奖次数限制,0默认不限制',
    `start_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '活动开始时间',
    `end_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '活动结束时间',
    `create_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间',
    `create_by` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建人staff_id',
    `update_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间',
    `update_by` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '修改人staff_id',
    `status` tinyint(1)  NOT NULL DEFAULT '0' COMMENT '状态 -1:deleted, 0:disable, 1:enable',
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='抽奖活动表';

第二要素场次配置抽奖场次表

-- 通用抽奖工具(万能胶Glue) glue_session 抽奖场次表
CREATE TABLE `glue_session` (
    `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '场次ID',
    `activity_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '活动ID',
    `times_limit` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '抽奖次数限制,0默认不限制',
    `start_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '场次开始时间',
    `end_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '场次结束时间',
    `create_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间',
    `create_by` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建人staff_id',
    `update_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间',
    `update_by` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '修改人staff_id',
    `status` tinyint(1)  NOT NULL DEFAULT '0' COMMENT '状态 -1:deleted, 0:disable, 1:enable',
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='抽奖场次表';

第三、四要素奖品配置抽奖场次奖品表

-- 通用抽奖工具(万能胶Glue) glue_session_prizes 抽奖场次奖品表
CREATE TABLE `glue_session_prizes` (
    `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增ID',
    `session_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '场次ID',
    `node` varchar(255) unsigned NOT NULL DEFAULT '' COMMENT '节点标识 按时间抽奖: 空值, 按抽奖次数抽奖: 第几次参与值, 按数额范围区间抽奖: 数额区间上限值',
    `prize_type` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '奖品类型 1:优惠券, 2:积分, 3:实物, 4:空奖 ...',
    `name` varchar(255) unsigned NOT NULL DEFAULT '' COMMENT '奖品名称',
    `pic_url` varchar(255) unsigned NOT NULL DEFAULT '' COMMENT '奖品图片',
    `value` varchar(255) unsigned NOT NULL DEFAULT '' COMMENT '奖品抽象值 优惠券:优惠券ID, 积分:积分值, 实物: sku ID',
    `probability` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '中奖概率1~100',
    `create_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间',
    `create_by` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建人staff_id',
    `update_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间',
    `update_by` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '修改人staff_id',
    `status` tinyint(1)  NOT NULL DEFAULT '0' COMMENT '状态 -1:deleted, 0:disable, 1:enable',
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='抽奖场次奖品表';

第五要素均匀投奖抽奖场次奖品定时投放器表

-- 通用抽奖工具(万能胶Glue) glue_session_prizes_timer 抽奖场次奖品定时投放器表
CREATE TABLE `glue_session_prizes_timer` (
    `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增ID',
    `session_prizes_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '抽奖场次奖品ID',
    `delivery_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '定时投放奖品数量的时间',
    `prize_quantity` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '奖品数量',
    `create_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间',
    `create_by` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建人staff_id',
    `update_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间',
    `update_by` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '修改人staff_id',
    `status` tinyint(1)  NOT NULL DEFAULT '0' COMMENT '状态 -1:deleted, 0:wait, 1:success',
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='抽奖场次奖品定时投放器表';

其他表,抽奖记录&奖品发放记录表:

-- 通用抽奖工具(万能胶Glue) glue_user_draw_record 用户抽奖记录表
CREATE TABLE `glue_user_draw_record` (
    `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增ID',
    `activity_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '活动ID',
    `session_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '场次ID',
    `prize_type_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '奖品类型ID',
    `user_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建人user_id',
    `create_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间',
    `update_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间',
    `status` tinyint(1)  NOT NULL DEFAULT '0' COMMENT '状态 -1:未中奖, 1:已中奖 , 2: 发奖失败 , 3: 已发奖',
    `log` text COMMENT '操作信息等记录',
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户抽奖记录表';

配置后台设计

创建活动

http://cdn.tigerb.cn/20191229224816.png?imageMogr2/thumbnail/1934x1567!/format/webp/blur/1x0/quality/75|imageslim

创建活动场次-按数额范围区间抽奖

http://cdn.tigerb.cn/20191230081157.png?imageMogr2/thumbnail/971x2069!/format/webp/blur/1x0/quality/75|imageslim

http://cdn.tigerb.cn/20191229224543.png?imageMogr2/thumbnail/971x2214!/format/webp/blur/1x0/quality/75|imageslim

http://cdn.tigerb.cn/20191229224834.png?imageMogr2/thumbnail/971x1693!/format/webp/blur/1x0/quality/75|imageslim

活动列表

http://cdn.tigerb.cn/20191229223706.png?imageMogr2/thumbnail/1338x761!/format/webp/blur/1x0/quality/75|imageslim

接口设计

1、获取活动信息 GET {version}/glue/activity

请求参数:

字段类型是否必传描述
serial_nostringY活动编号

响应内容:

{
    "code": "200",
    "msg": "OK",
    "result": {
        "serial_no": "string, 活动编号",
        "type": "number, 活动抽奖类型1: 按时间抽奖 2: 按抽奖次数抽奖 3:按数额范围区间抽奖",
        "name": "string, 活动名称",
        "description": "string, 活动描述",
        "start_time": "number, 活动开始时间",
        "end_time": "number, 活动开始时间",
        "remaining_times": "number, 活动抽奖次数限制,0不限制",
        "sessions_list":[
            {
                "start_time": "number, 场次开始时间",
                "end_time": "number, 场次开始时间",
                "remaining_times": "number, 场次抽奖次数限制,0不限制",
                "prizes_list": [
                    {
                        "name": "string, 奖品名称",
                        "pic_url": "string, 奖品图片"
                    }
                ]
            }
        ]
    }
}

2、抽奖 POST {version}/glue/activity/draw

请求参数:

字段类型是否必传描述
serial_nostringY活动编号
uidnumberY用户ID

响应内容:

// 中奖
{
    "code": "200",
    "msg": "OK",
    "result": {
        "serial_no": "string, spu id",
        "act_remaining_times": "number, 本活动抽奖剩余次数,0不限制",
        "session_remaining_times": "number, 本场次抽奖剩余次数,0不限制",
        "prizes_info": 
        {
            "name": "string, 奖品名称",
            "pic_url": "string, 奖品图片"
        }
    }
}

// 未中奖
{
    "code": "401",
    "msg": "",
    "result": {
        
    }
}

结语

活动营销系统中的第一个字系统通用抽奖工具今天讲完了,希望对大家有一定的帮助或启示。

彩蛋

通用抽奖工具的代码设计特别适合设计模式中的模板模式,你们觉着呢😏😏😏。所以,新的一年我会再写一篇《[Skr-Shop]通用抽奖工具之代码设计》吗?

(O_O)?

2020

最后后,祝大家2020年新年🆕快乐~


[Skr Shop] 项目地址长按进入:https://github.com/skr-shop/m...


Skr Shop系列更多文章:

查看原文

赞 13 收藏 11 评论 7

TIGERB 发布了文章 · 2019-12-26

[Skr-Shop]通用抽奖工具之需求分析

前言

首先我们先来回顾下营销体系的组成:

营销体系
活动营销系统
销售营销系统

今天带来的是活动营销系统下的第一个独立子系统通用抽奖工具的介绍,本篇文章主要分为如下4部分:

  • 常见抽奖场景与归类
  • 抽奖需求配置
  • 常见奖品类型
  • 抽奖五要素

常见抽奖场景与归类

下面是我列出来的一些常见的抽奖场景,红包雨、糖果雨、打地鼠、大转盘(九宫格)、考眼力、答题闯关、游戏闯关、支付刮刮乐、积分刮刮乐等等活动营销场景。

活动名称描述
红包雨每日整点抢红包🧧抽奖,每个整点一般可参与一次
糖果雨每日整点抢糖果🍬抽奖,每个整点一般可参与一次
打地鼠每日整点打地鼠抽奖,每个整点一般可参与一次
大转盘(九宫格)某个时间段,转盘抽奖,每个场一般可参N次
考眼力某个时间段,旋转杯子猜小球在哪个被子里,猜对可抽奖,一般每日可参与N次
答题闯关每过一关,可参与抽奖,越到后面奖品越贵重
游戏闯关每过一关,可参与抽奖,越到后面奖品越贵重
支付刮刮乐支付订单后可刮奖,支付金额越大奖品越贵重
积分刮刮乐积分刮奖,消费积分额度越大奖品越贵重

通过上面的活动描述,我们把整个抽奖场景归为以下三类:

类型活动名称维度
按时间抽奖红包雨、糖果雨、打地鼠、幸运大转盘(九宫格)、考眼力时间维度
按抽奖次数抽奖答题闯关、游戏闯关参与该活动次数维度
按数额范围区间抽奖支付刮刮乐、积分刮刮乐数额区间维度

接着我们来看下每类抽奖活动具体的抽奖需求配置。

抽奖需求配置

本小节每类抽奖活动的需求配置,分为如下三个部分:

  • 活动配置
  • 场次配置
  • 奖品配置

首先,第一类: 按时间抽奖的需求配置

类型活动名称特点
按时间抽奖红包雨、糖果雨、打地鼠、幸运大转盘(九宫格)、考眼力时间维度
按时间抽奖是否多场次单场次次数限制(次)总场次次数限制(次)
红包雨1N
糖果雨1N
打地鼠NN
幸运大转盘(九宫格)NN
考眼力NN

通过上面的分析我们得到了活动场次的概念: 一个活动需要支持多场次的配置。

  • 活动activity:配置活动的日期范围
  • 场次session:配置每场的具体时间范围

红包雨的需求配置示例:

活动特征:红包雨需要支持多场次。

比如双十二期间三天、每天三场整点红包雨配置如下:

活动、场次配置:

双十二红包雨
活动配置:
2019-12-10 ~ 2019-12-12
场次配置:
10:00:00 ~ 10:01:00
12:00:00 ~ 12:01:00
18:00:00 ~ 18:01:00

奖品配置:

场次奖品1奖品2---奖品N
场次10:00:00 ~ 10:01:00优惠券2元空奖---
场次12:00:00 ~ 12:01:00优惠券5元空奖---
场次18:00:00 ~ 18:01:00优惠券10元优惠券20元---空奖
上面配置的结果如下:

2019-12-10日三场整点红包雨:
2019-12-10 10:00:00 ~ 10:01:00
2019-12-10 12:00:00 ~ 12:01:00
2019-12-10 18:00:00 ~ 18:01:00

2019-12-11日三场整点红包雨:
2019-12-11 10:00:00 ~ 10:01:00
2019-12-11 12:00:00 ~ 12:01:00
2019-12-11 18:00:00 ~ 18:01:00

2019-12-12日三场整点红包雨:
2019-12-12 10:00:00 ~ 10:01:00
2019-12-12 12:00:00 ~ 12:01:00
2019-12-12 18:00:00 ~ 18:01:00

幸运大转盘的需求配置示例:

活动特征:幸运大转盘不需要多场次。

比如年货节2020-01-20 ~ 2020-02-10期间幸运大转盘配置如下:

活动、场次配置:

双十二幸运大转盘
活动配置:
2019-12-10 ~ 2019-12-12
场次配置:
00:00:00 ~ 23:59:59

奖品配置:

场次奖品1奖品2---奖品N
场次00:00:00 ~ 23:59:59优惠券2元空奖---
上面配置的结果如下:

幸运大转盘抽奖活动将于 2019-12-10 00:00:00 ~ 2019-12-12 23:59:59 进行

注意与思考:双十二幸运大转盘不需要多个场次,只配置一个场次即可,完全复用活动场次模型。

接着,第二类: 按抽奖次数抽奖的需求配置

类型活动名称特点
按抽奖次数抽奖答题闯关、游戏闯关(成功参与)当前活动次数维度

答题闯关的需求配置示例:

活动特征:每一关的奖品不同,一般越到后面中大奖的几率越大。

活动、场次配置:

双十二答题闯关
活动配置:
2019-12-10 ~ 2019-12-12
场次配置:
00:00:00 ~ 23:59:59

奖品配置:

双十二答题闯关奖品
第一关优惠券2元
第二关优惠券5元
第三关优惠券10元
第四关优惠券20元
第五关优惠券50元
第六关优惠券100元

注意与思考:同理活动&场次配置完全复用,同幸运大转盘配置(不需要支持多场次)。

最后,第三类: 按数额范围区间抽奖的需求配置:

类型活动名称特点
按数额范围区间抽奖支付刮刮乐、积分刮刮乐数额区间维度

支付刮刮乐的需求配置示例:

活动特征:不同的订单金额,一般金额越大中大奖的几率越大。

活动、场次配置:

双十二答题闯关
活动配置:
2019-12-10 ~ 2019-12-12
场次配置:
00:00:00 ~ 23:59:59

奖品配置:

订单金额奖品1奖品2---奖品N
0~100优惠券2元空奖---
100~200优惠券5元空奖---
200~1000优惠券10元优惠券20元---空奖
1000以上优惠券50元笔记本电脑---空奖

注意与思考:同理活动&场次配置完全复用,同幸运大转盘配置(不需要支持多场次)。

总结: 通过上面的分析我们得到了抽奖工具的两个要素活动场次

常见奖品类型

抽奖抽什么?
常见奖品类型
优惠券
积分
实物
空奖
总结: 我们得到了抽奖工具的另一个要素奖品

抽奖五要素

通过上面的分析我们已经得到了抽奖的三要素

  • 活动
  • 场次
  • 奖品
那还有什么要素我们还没聊到呢?接下来来看。

第四要素:中奖概率

抽奖自然离不开奖品的中奖概率的设置。关于中奖概率我们支持如下灵活的配置:

  1. 手动设置奖品中奖概率
  2. 自动概率,根据当前奖品的数量、奖品的权重得到中奖概率

比如我们某次大促活动红包雨的配置如下:

活动配置描述
活动时间2019-12-10~2019-12-12
活动名称2019双十二大促整点红包雨
活动描述2019双十二大促全端整点红包雨活动
手动设置奖品概率
场次奖品类型具体奖品奖品数量中奖概率
10:00:00 ~ 10:01:00优惠券2元优惠券200050%
-优惠券5元优惠券100020%
-空奖-500030%
12:00:00 ~ 12:01:00优惠券2元优惠券200050%
-优惠券5元优惠券100020%
-空奖-500030%
18:00:00 ~ 18:01:00优惠券2元优惠券200050%
-优惠券5元优惠券100020%
-空奖-500030%

备注:每轮场次中奖概率之和必须为100%,否则剩余部分默认添加为空奖的中奖概率。

第五要素:均匀投奖

如何均匀的抽走奖品?

答案: 均匀投奖。

具体方式为拆分总奖品数量,到各个细致具体的时间段。以双十二幸运大转盘为例:

场次奖品类型具体奖品奖品数量中奖概率投奖时间(默认提前5分钟投奖)投奖数量
00:00:00 ~ 23:59:59优惠券2元优惠券200050%--
-----00:00:002000
-----06:00:002000
-----12:00:002000
-----18:00:002000

这里我们就得到了抽奖的第五个要素:均匀投奖

结语

通过上面的分析,我们得到抽奖五要素如下:

抽奖五要素要素名称
第一要素活动
第二要素场次
第三要素奖品
第四要素中奖概率
第五要素均匀投奖

同时我们通过抽奖五要素也得到了通用抽奖工具配置一场抽奖活动的5个基本步骤:

  1. 活动配置
  2. 场次配置
  3. 奖品配置
  4. 奖品中奖概率配置
  5. 奖品投奖配置

最后,接着一篇文章,我们将来介绍通用抽奖工具的DB设计和配置后台设计。


[Skr Shop] 项目地址长按进入:https://github.com/skr-shop/m...


Skr Shop系列更多文章:

查看原文

赞 8 收藏 4 评论 0

TIGERB 分享了头条 · 2019-12-24

还不更新❓❓❓来啦~ Skr Shop年底第二弹《营销体系》第二篇《通用抽奖工具之需求分析》~

赞 1 收藏 1 评论 0

TIGERB 分享了头条 · 2019-12-23

还不更新❓❓❓来啦~ Skr Shop年底第二弹《营销体系》开更了,第一篇《营销体系开篇》~

赞 0 收藏 0 评论 0

TIGERB 发布了文章 · 2019-11-18

面向对象的设计过程

前言

我一直认为分享的目的不是炫技。

  • 一是,自我学习的总结。
  • 二是,降低他人的学习成本。
  • 三是,别人对自己学习结果的审核。

同时,本次分享有下面四个要素:

观点本次分享的观点是一个软件工程中的思维方法,不限于编程语言
探讨我可能理解错的,或者大家没理解的,欢迎大家积极评论,尽可能多互动,目的增加理解
理解真的希望大家能理解
运用最重要的,如果你觉着有帮助,一定要去在实际业务中实战

背景

工作中,几乎大家经常抱怨别人写的代码:

  • 没法改
  • 耦合高
  • 无法扩展
今天就来探讨如何克服上面的问题~

场景

首先问个问题:

平常工作中来了一个业务需求,我们是如何开始写代码的?

我推测大多数人可能:

  • 1、梳理业务
  • 2、设计数据库、接口、缓存
  • 3、评审
  • 4、于是就开始了 怎么怎么样...如果怎么怎么样...怎么怎么样...愉快的码代码的过程
此处有人觉着有啥问题么?
备注:说出来问题的,本次分享就可以略过了~

一个简单的业务场景

比如产品提了个需求:
描述“我一个同事”一天的生活,简单来看看他一天干些啥:

1.0 饿了吃饭
1.1 到点吃饭

2.0 渴了喝水
2.1 到点喝水

3.0 困了睡觉
3.1 到点睡觉
3.2 有可能一个人睡觉,也有可能... 是吧?复杂

刚开始,一个业务逻辑从头写到尾
https://user-gold-cdn.xitu.io/2019/11/7/16e45c864c8ab271?w=670&h=698&f=png&s=81774

一个业务逻辑(拆成多个函数)从头写到尾:
https://user-gold-cdn.xitu.io/2019/11/7/16e45c8655660dca?w=862&h=1382&f=png&s=145481

一个业务逻辑(引入类)从头写到尾:
https://user-gold-cdn.xitu.io/2019/11/7/16e45c865ab15489?w=858&h=842&f=png&s=100933

一个业务逻辑(拆成多个类方法)从头写到尾,也许、可能、貌似、猜测大多数人停留到了这个阶段。
问题:某一天多了社交的能力,咋办?
https://user-gold-cdn.xitu.io/2019/11/7/16e45c8650866cd5?w=1054&h=1562&f=png&s=232631

一个业务逻辑(拆成多类)从头写到尾:
https://user-gold-cdn.xitu.io/2019/11/7/16e45c865ff77738?w=914&h=1922&f=png&s=256693

一个业务逻辑(拆成类、抽象类、接口)从头写到尾:
https://user-gold-cdn.xitu.io/2019/11/7/16e45c8656622ead?w=1302&h=2606&f=png&s=427919

思考🤔:上面的代码就没啥问题了吗?

上面就是面向对象设计的代码结果。

所以,如何设计出完全面向对象的代码?

代码建模

什么是代码建模?

把业务抽象成事物(类class、抽象类abstact class)和行为(接口interface)的过程。

实栗🌰分析

又来看一个实际的业务场景:

最近“我一个同事”开始创业了,刚创立了一家电商公司,B2C,自营书籍《3分钟学会交际》。最近开始写提交订单的代码。

⚠️注意场景 1.刚创业 2.简单的单体应用 3.此处不探讨架构

一般来说,我们根据业务需求一顿分析,开始定义接口API、设计数据库、缓存、技术评审等就开始码代码了。

接口参数:
uid
address_id
coupon_id
.etc

业务逻辑:
参数校验->
地址校验->
其他校验->
写订单表->
写订单商品信息表->
写日志->
扣减商品库存->
清理购物车->
扣减各种促销优惠活动的库存->
使用优惠券->
其他营销逻辑等等->
发送消息->
等等...

就开始写代码了怎么怎么样...如果怎么怎么样...怎么怎么样...一蹴而就、思路清晰、逻辑清楚、很快搞定完代码,很优秀是不是,值得鼓励。

但是,上面的结果就是大概所有人都见过的连续上千行的代码等等。上面的流程没啥问题啊,那正确的做法是什么了?就是接着要说的代码建模

我们根据上面的场景,开始建模。

业务分析少不了

同样,首先,我们看看提交订单这个业务场景要做的事情:

换个角度看业务其实很简单:根据用户相关信息生成一个订单。
  1. 梳理得到业务逻辑
参数校验->
地址校验->
其他校验->
写订单表->
写订单商品信息表->
写日志->
扣减商品库存->
清理购物车->
扣减各种促销优惠活动的库存->
使用优惠券->
其他营销逻辑等等->
发送消息->
等等...
  1. 梳理业务逻辑依赖信息
用户信息
商品信息
地址信息
优惠券信息
等等...

再次回归概念

什么是代码建模?把业务抽象成事物(类class、抽象类abstact class)和行为(接口interface)的过程。

获取事物

比如我们把订单生成的过程可以想象成机器人,一个生成订单的订单生成机器人,或者订单生成机器啥的,这样我们就得到了代码建模过程中的一个事物。

从而我们就可以把这个事物转化成一个类(或结构体),或者抽象类。

https://user-gold-cdn.xitu.io/2019/11/7/16e45c86cacb4152?w=1208&h=407&f=jpeg&s=63758

获取行为

这些操作就是上面机器人要做的事情。

事物有了:订单生成机器人
行为呢?毫无疑问就是上面各种业务逻辑。把具体的行为抽象成一个订单创建行为接口:

https://user-gold-cdn.xitu.io/2019/11/7/16e45c86f90e2839?w=934&h=170&f=jpeg&s=32819

得到UML

https://user-gold-cdn.xitu.io/2019/11/7/16e45c8700e8b8c0?w=1577&h=886&f=png&s=82942>

设计代码

  1. 定义一个类

https://user-gold-cdn.xitu.io/2019/11/7/16e45c87038a0fc1?w=1186&h=1670&f=png&s=279028

  1. 定义一个订单创建行为的接口

https://user-gold-cdn.xitu.io/2019/11/7/16e45c8700f7443e?w=1166&h=518&f=png&s=82926

  1. 定义具体的不同订单创建行为类
参数校验->
地址校验->
其他校验->
写订单表->
写订单商品信息表->
写日志->
扣减商品库存->
清理购物车->
扣减各种促销优惠活动的库存->
使用优惠券->
其他营销逻辑等等->
发送消息->
等等...

https://user-gold-cdn.xitu.io/2019/11/7/16e45c870943c870?w=1302&h=2138&f=png&s=394128

  1. 创建订单

这里的代码该怎么写,这样?
https://user-gold-cdn.xitu.io/2019/11/7/16e45c871f2fd3fd?w=1034&h=1130&f=png&s=200044

还可以继续优化吗?
https://user-gold-cdn.xitu.io/2019/11/7/16e45c8730ebc469?w=1454&h=1022&f=png&s=188592

使用闭包。
https://user-gold-cdn.xitu.io/2019/11/7/16e45c8730f93eca?w=1454&h=1058&f=png&s=191695

PHP版完整代码

https://user-gold-cdn.xitu.io/2019/11/7/16e45c8754ee6abb?w=1346&h=5702&f=png&s=1056295

Go版完整代码

https://user-gold-cdn.xitu.io/2019/11/7/16e45c8750dfe08c?w=2048&h=4874&f=png&s=1117461

上面的代码有什么好处?

假如“我一个同事”又要新开发一个新的应用,新的应用创建订单的时候又有新的逻辑,比如没有优惠逻辑、新增了增加用户积分的逻辑等等,复用上面的代码,是不是就很简单了。

https://user-gold-cdn.xitu.io/2019/11/7/16e45c874d66fc62?w=1082&h=734&f=png&s=123089

所以现在,什么是面向对象?

概念

面向对象的设计原则

  • 对接口编程而不是对实现编程
  • 优先使用对象组合而不是继承
  • 抽象用于不同的事物,而接口用于事物的行为

针对上面的概念,我们再回头开我们上面的代码

对接口编程而不是对实现编程
结果:RobotOrderCreate依赖了BehaviorOrderCreateInterface抽象接口
优先使用对象组合而不是继承
结果:完全没有使用继承,多个行为不同场景组合使用
抽象用于不同的事物,而接口用于事物的行为
结果:
1. 抽象了一个创建订单的机器人 RobotOrderCreate
2. 机器人又有不同的创建行为
3. 机器人的创建行为最终依赖于BehaviorOrderCreateInterface接口

是不是完美契合,所以这就是“面向对象的设计过程”。

结论

代码建模过程就是“面向对象的设计过程”的具体实现方式.

预习

设计模式

最后,设计模式又是什么?

同样,我们下结合上面的场景和概念预习下设计模式。

设计模式的设计原则
开闭原则(Open Close Principle):对扩展开放,对修改封闭

看看上面的最终的代码是不是完美契合。

https://user-gold-cdn.xitu.io/2019/11/7/16e45c87616dc861?w=1504&h=914&f=png&s=188519

依赖倒转原则:对接口编程,依赖于抽象而不依赖于具体
结果:创建订单的逻辑从依赖具体的业务转变为依赖于抽象接口BehaviorOrderCreateInterface
接口隔离原则:使用多个接口,而不是对一个接口编程,去依赖降低耦合
结果:上面的场景,我们只简单定义了订单创建的接BehaviorOrderCreateInterface。由于订单创建过程可能出现异常回滚,我们就需要再定义一个订单创建回滚的接口
BehaviorOrderCreateRollBackInterface.
迪米特法则,又称最少知道原则:减少内部依赖,尽可能的独立
结果:还是上面那段代码,我们把RobotOrderCreate机器人依赖的行为通过外部注入的方式使用。
合成复用原则:多个独立的实体合成聚合,而不是使用继承
结果:RobotOrderCreate依赖了多个实际的订单创建行为类。
里氏代换:超类(父类)出现的地方,派生类(子类)都可以出现
结果:不好意思,我们完全没用继承。(备注:继承容易造成父类膨胀。)

下回预告

上面预习了设计模式的概念,下次我们进行《设计模式业务实战》。

查看原文

赞 33 收藏 21 评论 3

TIGERB 分享了头条 · 2019-11-04

工作中,几乎大家经常抱怨别人写的代码: 没法改 耦合高 无法扩展 今天就来探讨如何克服上面的问题~

赞 1 收藏 3 评论 3