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 收藏了文章 · 4月15日

消息通知系统模型设计

简介

几乎每个站点都有消息通知系统,可见通知系统的重要性不言而喻。通知系统看似简单,实际上比较复杂。那么本篇主要讲解常见的消息通知系统的设计和具体实现,包括数据库设计、逻辑关系分析等。

常见的站内通知类别:

  • 公告 Announcement
  • 提醒 Remind

    • 资源订阅提醒「我关注的资源有更新、评论等事件时通知我」
    • 资源发布提醒「我发布的资源有评论、收藏等事件时通知我」
    • 系统提醒「平台会根据一些算法、规则等可能会对你的资源做一些事情,这时你会收到系统通知」
  • 私信 Mailbox

以上三种消息有各自特点,实现也各不相同,其中「提醒」类通知是最复杂的,下面会详细讲。

设计与实现

公告

公告是指平台发送一条含有具体内容的消息,站内所有用户都能收到这条消息。

方案一:【适合活跃用户在5万左右】

公告表「notify_announce」
表结构如下:

id: {type: 'integer', primaryKey: true, autoIncrement:true} //公告编号;
senderID: {type: 'string', required: true} //发送者编号,通常为系统管理员;
title: {type: 'string', required: true} //公告标题;
content: {type: ’text', required: true} //公告内容;
createdAt: {type: 'timestamp', required: true} //发送时间;

用户公告表「notify_announce_user」
表结构如下:

id: {type: 'integer', primaryKey: true, autoIncrement:true} //用户公告编号;
announceID: {type: 'integer'} //公告编号;
recipientID: {type: 'string', required: true} //接收用户编号;
createdAt:{type: 'timestamp', required: true} //拉取公告时间;
state: {type: 'integer', required: true} //状态,已读|未读;
readAt:{type: 'timestamp', required: true} //阅读时间;

平台发布一则公告之后,当用户登录的时候去拉取站内公告并插入notify_announce_user表,这样那些很久都没登陆的用户就没必要插入了。「首次拉取,根据用户的注册时间;否则根据notify_announce_user.createdAt即上一次拉取的时间节点获取公告」

方案二:【适合活跃用户在百万-千万左右】

和方案一雷同,只是需要把notify_announce_user表进行哈希分表,需事先生成表:notify_announce_<hash(uid)>。

用户公告表「notify_announce_<hash(uid)>」
表结构如下:

id: {type: 'integer', primaryKey: true, autoIncrement:true} //用户公告编号;
announceID: {type: 'integer'} //公告编号;
recipientID: {type: 'string', required: true} //接收用户编号;
createdAt:{type: 'timestamp', required: true} //拉取公告时间;
state: {type: 'integer', required: true} //状态,已读|未读;
readAt:{type: 'timestamp', required: true} //阅读时间;

通知提醒

提醒是指「我的资源」或「我关注的资源」有新的动态产生时通知我。提醒的内容无非就是:
「someone do something in someone's something」
「谁对一样属于谁的事物做了什么操作」

常见的提醒消息例子,如:

XXX 关注了你  - 「这则属于资源发布提醒」   
XXX 喜欢了你的文章 《消息通知系统模型设计》  - 「这则属于资源发布提醒」   
你喜欢的文章《消息通知系统模型设计》有新的评论  - 「这则属于资源订阅提醒」   
你的文章《消息通知系统模型设计》已被加入专题 《系统设计》 - 「这则属于系统提醒」  
小明赞同了你的回答 XXXXXXXXX  -「这则属于资源发布提醒」   
最后一个例子中包含了消息的生产者(小明),消息记录的行为(赞同),行为的对象(你的回答内容)

分析提醒类消息的句子结构:

someone = 动作发起者,标记为「sender」  
do something = 对资源的操作,如:评论、喜欢、关注都属于一个动作,标记为「action」  
something = 被作用对象,如:一篇文章,文章的评论等,标记为「object」  
someone's = 动作的目标对象或目标资源的所有者,标记为「objectOwner」  
sender 和 objectOwner 就是网站的用户,object 就是网站资源,可能是一篇文章,一条文章的评论等等。action 就是动作,可以是赞、评论、收藏、关注、捐款等等。

为了能够更加通俗易懂一点,接下来我会按照通知事件发送的时间顺序开始讲解。

关键点

  • 通知事件
  • 通知设置
  • 通知提醒
  • 消息推送.方式

    • 一对一推送
    • 一对多推送
  • 通知推送.渠道

    • 站内
    • 站外

      • 安卓系统应用
      • IOS系统应用
      • 邮箱
      • 短信
      • WhatsAPP
      • 其它
  • 通知推送.技术

    • WebSocket
    • 第三方SDK开发或接入
  • 通知接收.渠道

    • 站内
    • 站外

      • 安卓系统应用
      • IOS系统应用
      • 邮箱
      • 短信
      • WhatsAPP
      • 其它
  • 消息聚合

通知事件

什么是通知事件?

通知事件就是当用户在网站或应用上产生了支付行为之后,如果你想给用户一个通知,告诉她系统已收到她的付款,那么你就要把这个「支付行为」定义为一个通知事件,并且保存这个通知事件到「通知事件表」里,以便通知系统作异步处理。通知系统会不断的处理「通知事件表」里的数据,分析每一个事件应该通知和不通知哪些人。

通知事件表「notify_event」

记录每一个用户行为产生的通知事件信息

表结构如下:

id: {type: 'integer', primaryKey: true, autoIncrement:true} 
userID: {type: 'string', required: true} //用户ID
action: {type: 'string', required: true} //动作,如:捐款/更新/评论/收藏
objectID: {type: 'string', required: true}, //对象ID,如:文章ID;
objectType: {type: 'string', required: true} //对象所属类型,如:人、文章、活动、视频等;
createdAt:{type: 'timestamp', required: true} //创建时间;
用户行为定义

「action」即用户行为,如:赞了、评论了、喜欢了、捐款了、收藏了;一般来讲,我们把一个用户行为定义为一个通知类型,那么用户行为必须是需要提前定义好的。

由消息系统内部定义,为后台提供接口,用于通知设置。如下:

notify_action_type := ["donated","conllected","commented","updated"]
对象类型定义

「objectType」即用户行为作用的对象的所属类型,简单的说就是资源类型,如:项目、文章、评论、商品、视频、图片、用户。

由消息系统内部定义,为后台提供接口,用于通知设置。如下:

notify_object_type := ["project","comment"]

通知设置

什么是通知设置?

通知设置是指用户在通知设置页面上可以对某类通知进行屏蔽的设置选项,如:

通知提醒

  • [x] {我关注的}{项目}有动态{更新}时通知我
  • [x] 有人{收藏}{我发布的}{项目}时通知我

通知设置页面上的通知选项信息来源于「后台通知设置管理」,后台需要将通知设置选项的内容描述,转化为逻辑关系保存在「通知设置配置表」。"{}"括号内部就是需要提取的变量,保存到对应的字段中。

通知设置配置表「notify_setting_config」

表结构如下:

id: {type: 'integer', primaryKey: true, autoIncrement:true} //通知设置编号;
objectType: {type: 'string', required: true} //资源对象类型,如:项目、文章、评论、商品、视频、图片、用户;
action: {type: 'string', required: true} //动作,也即通知类型,如:捐款、更新、评论、收藏
objectRelationship: {type: 'string', required: true} //用户与资源的关系,如:用户发布的published,用户关注的followed;
messageTemplate: {type: 'string', required: true} //为某个通知类型设置对应的消息模版
notifyChannel: {type: 'string', required: true} //为某个通知类型设置一个或多个推送渠道
description: {type: 'string', required: true}  //设置选项的内容描述
settingType: {type: 'string', required: true} //remind、privateLetters
消息模版

每一个通知类型都有一个特定的消息模版,由消息系统内部定义,为后台提供接口,用于通知设置。如下:

  • XXX 喜欢了你的文章 《消息通知系统模型设计》
  • 你喜欢的文章《消息通知系统模型设计》有新的评论
message_templates := ["donated","conllected","updated"]

message.SetString(language.AmericanEnglish, "donated", "XXXXXXXXXXXXXXXXXXXXXXXXXXXXX.")
message.SetString(language.AmericanEnglish, "conllected", "XXXXXXXXXXXXXXXXXXXXXXXXXXXXX.")
message.SetString(language.AmericanEnglish, "updated", "XXXXXXXXXXXXXXXXXXXXXXXXXXXXX.")
通知渠道

什么是通知渠道?
通知渠道是指消息以什么途径推送给用户。每一条特定通知类型的通知可以被发送到一个或多个渠道,每一个通知类型对应几个通知渠道需要在「后台通知设置管理」中配置好。

通知渠道由消息系统内部定义,为后台提供接口,用于通知设置。如下:
站内,短信,邮件,设备,WhatsAPP

notify_channel := ["inside","device","sms","email","whatsapp"]
用户通知设置

用户的通知设置都保存在这张表里。 当无用户的任何记录时,即用户没有设置时,默认代表用户是开启了接收此类通知的;否则如果有用户的设置记录,则代表用户关闭了此类通知的接收。

系统在给用户推送消息的时候必须查询「用户通知设置」,以获取某一通知事件的提醒消息应该推送到哪些用户。也就是说「事件」和「用户」之间有一个订阅关系。

让我们分析下「订阅」有哪些关键元素:

比如我发布了一篇文章,那么我会订阅文章《XXX》的评论动作,所以文章《XXX》每被人评论了,就需要发送一则提醒告知我。

分析得出以下关键元素:

  • 订阅者「subscriber」
  • 订阅的对象「object」
  • 订阅的动作「action」
  • 订阅对象和订阅者的关系「objectRelationship」

什么是订阅的目标关系呢?

拿知乎来说,比如我喜欢了一篇文章,我希望我订阅这篇文章的更新、评论动作。那这篇文章和我什么关系?不是所属关系,只是喜欢。

  • objectRelationship = 我发布的,对应着 actions = [评论,收藏]
  • objectRelationship = 我喜欢的,对应着 actions = [更新,评论]

一般系统都默认用户设置/订阅了所有通知的。

通知设置表「notify_setting」

表结构如下:

id: {type: 'integer', primaryKey: true, autoIncrement:true} //用户通知设置ID;
userId: {type: 'string', required: true},//用户ID,对应 notify_remind 中的 recipientId;
settingId: {type: 'string', required: true},//通知设置表ID;
createdAt:{type: 'timestamp', required: true} //创建时间;
通知设置接口

用户行为、对象类型、消息模版、通知渠道 这些都在消息系统内部定义,为后台提供接口,用于通知设置。

type NotifySetting struct {
    NotifyActionType []string
    NotifyObjectType []string
    MessageTemplates []string
    NotifyChannel []string
}

用户提醒

每一个通知事件产生的通知消息将被保存在这里,通过提供接口最终展示在页面上。

通知提醒表「notify_remind」

表结构如下:

id: {type: 'integer', primaryKey: true, autoIncrement:true} //主键;
remindID: {type: 'string', required: true} //通知提醒编号;
senderID: {type: 'string', required: true} //操作者的ID,三个0代表是系统发送的;
senderName: {type: 'string’, required: true} //操作者用户名;
senderAction: {type: 'string', required: true} //操作者的动作,如:捐款、更新、评论、收藏;
objectID: {type: 'string', required: true}, //目标对象ID;
object: {type: 'string', required: false}, //目标对象内容或简介,比如:文章标题;
objectType: {type: 'string', required: true} //被操作对象类型,如:人、文章、活动、视频等;
recipientID: {type: 'string’} //消息接收者;可能是对象的所有者或订阅者;
message: {type: 'text', required: true} //消息内容,由提醒模版生成,需要提前定义;
createdAt:{type: 'timestamp', required: true} //创建时间;
status:{type: 'integer', required: false} //是否阅读,默认未读;
readAt:{type: 'timestamp', required: false} //阅读时间;

消息聚合

假如我在抖音上发布了一个短视频,在我不在线的时候,被评论了1000遍,当我一上线的时候,应该是收到一千条消息,类似于:「* 评论了你的文章《XXX》」? 还是应该收到一条信息:「有1000个人评论了你的文章《XXX》」?

当然是后者更好些,要尽可能少的骚扰用户。

消息推送

是不是感觉有点晕了,还是先上一张消息通知的推送流程图吧:
clipboard.png

订阅表一共有两张噢,一张是「通知订阅表」、另一张是用户对资源的「对象订阅表」。
具体实现就不多讲了,配合这张图,理解上面讲的应该不会有问题了。

私信

通常私信有这么几种需求:

  • 点到点:用户发给用户的站内信,系统发给用户的站内信。「1:1」
  • 点到多:系统发给多个用户的站内信,接收对象较少,而且接收对象无特殊共性。「1:N」
  • 点到面:系统发给用户组的站内信,接收对象同属于某用户组之类的共同属性。「1:N」
  • 点到全部:系统发给全站用户的站内信,接收对象为全部用户,通常为系统通知。「1:N」

这里主要讲「点到点」的站内信。

私信表「notify_mailbox」
表结构如下:

id: {type: 'integer', primaryKey: true, autoIncrement:true} //编号;
dialogueID: {type: 'string', required: true} //对话编号; 
senderID: {type: 'string', required: true} //发送者编号;
recipientID: {type: 'string', required: true} //接收者编号;
messageID: {type: 'integer', required: true} //私信内容ID;
createdAt:{type: 'timestamp', required: true} //发送时间;
state: {type: 'integer', required: true} //状态,已读|未读;
readAt:{type: 'timestamp', required: true} //阅读时间;

Inbox

私信列表
select * from notify_inbox where recipientID="uid" order by createdAt desc

对话列表
select * from notify_inbox where dialogueID=“XXXXXXXXXXXX” and (recipientID=“uid” or senderID="uid") order by createdAt asc

私信回复时,回复的是dialogueID

Outbox

私信列表
select * from notify_inbox where senderID="uid" order by createdAt desc

对话列表
select * from notify_inbox where dialogueID=“XXXXXXXXXXXX” and (senderID=“uid” or recipientID="uid") order by createdAt asc

私信内容表「notify_inbox_message」
表结构如下:

id: {type: 'integer', primaryKey: true, autoIncrement:true} //编号;
senderID: {type: 'string', required: true} //发送者编号;
content: {type: 'string', required: true} //私信内容; 
createdAt:{type: 'timestamp', required: true}

参考

消息系统设计与实现
通知系统设计

查看原文

TIGERB 赞了头条 · 3月8日

赞 1 收藏 4 评论 0

TIGERB 分享了头条 · 3月8日

为什么我们需要学习设计模式?

赞 1 收藏 4 评论 0

TIGERB 分享了头条 · 3月1日

Go语言里每次遍历Map输出元素的顺序并不一致,但是在PHP里却是稳定的。今天我们就来看看这个现象的原因。

赞 0 收藏 1 评论 2

TIGERB 分享了头条 · 2月1日

我们想搞懂Go语言的内存分配原理前,必须先了解TCMalloc内存分配器,以便于我们更好的理解Go语言的内存分配原理。

赞 0 收藏 1 评论 0

TIGERB 发布了文章 · 1月27日

64位平台下,指针自身的大小为什么是8字节?

系列导读

本系列基于64位平台、1Page=8KB

今天我们开始拉开《Go语言轻松系列》第二章「内存与垃圾回收」的序幕。

关于「内存与垃圾回收」章节,大体从如下三大部分展开:

  • 知识预备:为后续的内容做一些知识储备,知识预备包括

    • 指针的大小
    • Tcmalloc内存分配原理
  • Go内存设计与实现
  • Go的垃圾回收原理

本篇前言

第一部分知识预备的第一个知识点指针的大小

为什么指针的大小会作为一个知识点呢?

因为后续内存管理的内容会涉及一些数据结构,这些数据结构使用到了指针,同时存储指针的值是需要内存空间的,所以我们需要了解指针的大小,便于我们理解一些设计的意图;其次,这也是困扰我的一个问题,因为有看见64位平台下指针底层定义的类型为uint64

为了搞清楚这个问题,我们需要了解两个知识点:

  1. 存储单元
  2. CPU总线

什么是存储单元?

存储单元是存储器(本文指内存)的基本单位,每个存储单元是8bit,也就是1Byte,如下图所示:

同时从上图中我们可以看出,每个存储单元会被编号,这个编号又是什么呢?

  • 就是我们通常所谓的“内存的地址”
  • 也就是指针的值
结论:指针的值就是存储单元的编号。

接着,我们只需要知道这个「编号」的最大值是多少,就可以知道存储「指针」的值所需的大小。要找到这个最大值就需要了解CPU总线的知识了。

CPU总线的概念


CPU总线由系统总线、等等其他总线组成。

总线的组成
系统总线
等等其他总线...

系统总线由一系列总线组成。

系统总线的组成
地址总线
数据总线
信号总线

内存的地址(存储单元的编号)是通过地址总线传递的,地址总线里的“每一根线”传递二进制01,如下图所示(实际不是这么简单,图示为了便于大家理解)。

地址总线的宽度决定了一次能传递多少个01,由于64位CPU每次可处理64位数据,所以理论上地址总线的宽度可以支持到最大64,也就是2^64种组合,可代表的数字范围为0 ~ 2^64-1

结论:理论上64位CPU地址总线可传输的10进制数范围为0 ~ 2^64-1

上面知道64位CPU的地址总线可寻址范围 为 0 ~ 2^64-1,需要一个类型可以存储这个指针的值,毫无疑问就是uint64uint64又是多大呢?是不是8byte。所以:64位平台下,一个指针的大小是8字节

顺便扩充个问题:

为什么32位平台下,可寻址空间是4GB?
备注:64位太大,我们这里用32位来看这个问题

我们来分析一下:

  • 由于,32位平台可支持地址总线的最大宽度为32,及代表的存储单元编号的范围:0 ~ 2^32-1
  • 则,最多可以找到2^32个存储单元
  • 又有,存储单元的大小为8bit(1Byte)

所以我们可以得到,32位平台最多可以寻找到2^32个存储单元,再翻译下2^32个存储单元这句话:

2^32个存储单元 == 2^32个1Byte == 2^32Byte == 4GByte == 4GB

做个总结哈

我们回头再来看,本次内容可以get到如下知识点:

  • 存储器的基本单位是存储单元
  • 存储单元为8bit
  • 指针的值就是存储单元的编号
  • CPU地址总线的宽度决定了指针的值的最大范围

查看《Go语言轻松系列》更多内容

链接 http://tigerb.cn/go/#/kernal/

3911642037-d2bb08d8702e7c91_articlex.jpg

查看原文

赞 2 收藏 1 评论 0

TIGERB 发布了文章 · 1月27日

由浅到深,入门Go语言Map实现原理

导读

Go源码版本1.13.8

今天要分享的是主要内容是Go语言Map底层实现,目的让大家快速了解Go语言Map底层大致的实现原理。读完本篇文章你可以获得收益、以及我所期望你能获取的收益如下:

收益序号收益描述掌握程度
收益1大致对Go语言Map底层实现有一个了解必须掌握
收益2大致知道Go语言Map是如何读取数据的必须掌握
收益3熟悉Go语言Map底层核心结构体hmap可选
收益4熟悉Go语言Map底层核心结构体bmap可选
收益5熟悉Go语言Map底层里的溢出桶可选
收益6熟悉Go语言Map是如何读取数据的可选

收益1和收益2是看了本篇文章希望大家必须掌握的知识点,其他的为可选项,如果你对此感兴趣或者已经掌握了收益1、2可以继续阅读此处的内容。

对于本篇文章的结构主要按如下顺序开展:

  • 简单看看一般Map的实现思路
  • Go语言里Map的实现思路(入门程度:包含收益1、2)
  • Go语言里Map的实现思路(熟悉程度:包含收益3、4、5、6)

其次,本篇文章主要以Map的读来展开分析,因为读弄明白了,其他的写、更新、删除等基本操作基本都可以猜出来了,不是么😏。

简单看看一般Map的实现思路

直入主题,一般的Map会包含两个主要结构:

  • 数组:数组里的值指向一个链表
  • 链表:目的解决hash冲突的问题,并存放键值

大致结构如下:
http://cdn.tigerb.cn/20201216161128.png

读取一个key值的过程大致如下:

                  key
                   |
                   v                 
+------------------------------------+
|      key通过hash函数得到key的hash    |
+------------------+-----------------+
                   |
                   v
+------------------------------------+
|       key的hash通过取模或者位操作     |
|          得到key在数组上的索引        |
+------------------------------------+
                   |
                   v
+------------------------------------+
|         通过索引找到对应的链表         |
+------------------+-----------------+
                   |
                   v
+------------------------------------+
|       遍历链表对比key和目标key        |
+------------------+-----------------+
                   |
                   v
+------------------------------------+
|              相等则返回value         |
+------------------+-----------------+
                   |
                   v                
                 value 

接着我们来简单看看Go语言里Map的实现思路。

Go语言里Map的实现思路(入门程度)

包含收益1、2

Go语言解决hash冲突不是链表,实际主要用的数组(内存上的连续空间),如下图所示:

备注:后面我们会解释上面为啥用的“主要”两个字。

http://cdn.tigerb.cn/20201219202458.png

但是并不是只使用一个数组(连续内存空间)存放键和值,而是使用了两个数组分别存储键和值,图示如下:

http://cdn.tigerb.cn/20201217210507.png

上图中:

  • 分别对应的是两个核心的结构体hmapbmap
  • bmap里有两个数组分别存放key和value

把上面简化的关系转换一下,其实就是这样的一个大致关系,如下图所示:

http://cdn.tigerb.cn/20201217210752.png

我们通过一次读操作为例,看看读取某个key的值的一个大致过程

步骤编号描述
通过hash函数获取目标key的哈希,哈希和数组的长度通过位操作获取数组位置的索引(备注:获取索引值的方式一般有取模或位操作,位操作的性能好些)
遍历bmap里的键,和目标key对比获取key的索引(找不到则返回空值)
根据key的索引通过计算偏移量,获取到对应value

读过程图示如下:

http://cdn.tigerb.cn/20201217210816.png

这么看起来是不是“很简单”、很清晰,所以读到这里,你是不是已经入门了Go语言Map底层实现并且:

  • 大致对Go语言Map底层实现有一个了解(收益1)
  • 大致知道Go语言Map是如何读取数据的(收益2)

然而实际情况不止如此,我们再稍微深入的探索下,有兴趣的可以继续往下看,没兴趣可以不用继续往下看了(开玩笑=^_^=),反正已经达到目的了,哈哈😏。

Go语言里Map的实现思路(熟悉程度)

包含收益3、4、5、6

想要深入学习,首先得了解下上面提到了实现Map的两个核心结构体hmapbmap

核心结构体hmap

收益3: 熟悉Go语言Map底层核心结构体`hmap`

hmap的结构其实刚开始看起来其实还是比较复杂的,有不少的字段,具体字段如下图所示:

http://cdn.tigerb.cn/20201218132443.png

字段释义如下:

字段解释
count键值对的数量
B2^B=len(buckets)
hash0hash因子
buckets指向一个数组(连续内存空间),数组的类型为[]bmap,bmap类型就是存在键值对的结构下面会详细介绍,这个字段我们可以称之为正常桶。如下图所示
oldbuckets扩容时,存放之前的buckets(Map扩容相关字段)
extra溢出桶结构,正常桶里面某个bmap存满了,会使用这里面的内存空间存放键值对
noverflow溢出桶里bmap大致的数量
nevacuate分流次数,成倍扩容分流操作计数的字段(Map扩容相关字段)
flags状态标识,比如正在被写、buckets和oldbuckets在被遍历、等量扩容(Map扩容相关字段)
备注:本次内容不涉及Map的扩容逻辑。

重点看一些字段的含义和用处。

字段buckets

http://cdn.tigerb.cn/20201216202022.png

buckets指向了一个数组(连续的内存空间),数组的元素是bmap类型,这个字段我们称之为正常桶。

hmap的源码和地址如下:

// https://github.com/golang/go/blob/go1.13.8/src/runtime/map.go
type hmap struct {
    count     int 
    flags     uint8
    B         uint8 
    noverflow uint16 
    hash0     uint32
    buckets    unsafe.Pointer
    oldbuckets unsafe.Pointer
    nevacuate  uintptr 
    extra *mapextra
}

核心结构体bmap

收益4: Go语言Map底层核心结构体`bmap`

正常桶hmap.buckets的元素是一个bmap结构。bmap的具体字段如下图所示:

http://cdn.tigerb.cn/20201216202114.png

字段释义如下:

字段解释
topbits长度为8的数组,[]uint8,元素为:key获取的hash的高8位,遍历时对比使用,提高性能。如下图所示
keys长度为8的数组,[]keytype,元素为:具体的key值。如下图所示
elems长度为8的数组,[]elemtype,元素为:键值对的key对应的值。如下图所示
overflow指向的hmap.extra.overflow溢出桶里的bmap,上面的字段topbitskeyselems长度为8,最多存8组键值对,存满了就往指向的这个bmap里存
pad对齐内存使用的,不是每个bmap都有会这个字段,需要满足一定条件

http://cdn.tigerb.cn/20201216202224.png

推断出bmap结构字段的代码和位置如下:

// https://github.com/golang/go/blob/go1.13.8/src/cmd/compile/internal/gc/reflect.go
func bmap(t *types.Type) *types.Type {
  // 略...

  field := make([]*types.Field, 0, 5)

    field = append(field, makefield("topbits", arr))

  // 略...
  
    keys := makefield("keys", arr)
    field = append(field, keys)

  // 略...
  
    elems := makefield("elems", arr)
    field = append(field, elems)

  // 略...
  
    if int(elemtype.Align) > Widthptr || int(keytype.Align) > Widthptr {
        field = append(field, makefield("pad", types.Types[TUINTPTR]))
    }

  // 略...
  
    overflow := makefield("overflow", otyp)
    field = append(field, overflow)

  // 略...
}
结论:每个bmap结构最多存放8组键值对。

hmapbmap的基本结构合起来

分别了解了hmapbmap的基本结构后,我们把上面的内容合并起来,就得到如下的Map结构图:

http://cdn.tigerb.cn/20201216202349.png

溢出桶

收益5: 熟悉Go语言Map底层里的溢出桶

上面讲bmap的时候,我们不是得到了个结论么“每个bmap结构最多存放8组键值对。”,所以问题来了:

正常桶里的bmap存满了怎么办?

解决这个问题我们就要说到hmap.extra结构了,hmap.extra是个结构体,结构图示和字段释义如下:

http://cdn.tigerb.cn/20201216202608.png

字段解释
overflow称之为溢出桶。和hmap.buckets的类型一样也是数组[]bmap,当正常桶bmap存满了的时候就使用hmap.extra.overflowbmap。所以这里有个问题正常桶hmap.buckets里的bmap是怎么关联上溢出桶hmap.extra.overflowbmap呢?我们下面说。
oldoverflow扩容时存放之前的overflow(Map扩容相关字段)
nextoverflow指向溢出桶里下一个可以使用的bmap

源码和地址如下:

// https://github.com/golang/go/blob/go1.13.8/src/runtime/map.go
type mapextra struct {
    overflow    *[]*bmap
    oldoverflow *[]*bmap
    nextOverflow *bmap
}
问题:正常桶hmap.buckets里的bmap怎么关联上溢出桶hmap.extra.overflowbmap呢?

答:就是我们介绍bmap结构时里的bmap.overflow字段(如下图所示)。bmap.overflow是个指针类型,存放了对应使用的溢出桶hmap.extra.overflow里的bmap的地址。

http://cdn.tigerb.cn/20201221131007.png

问题又来了

问题:正常桶hmap.buckets里的bmap什么时候关联上溢出桶hmap.extra.overflowbmap呢?

答:Map写操作的时候。这里直接看关键代码:

// https://github.com/golang/go/blob/go1.13.8/src/runtime/map.go
func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
  // 略
again:
    // 略...
    var inserti *uint8
  // 略...
bucketloop:
    for {
        for i := uintptr(0); i < bucketCnt; i++ {
      // key的hash高8位不相等
            if b.tophash[i] != top {
        // 当前位置bmap.tophash的元素为空且还没有写入的记录(inserti已经写入的标记为)
                if isEmpty(b.tophash[i]) && inserti == nil {
          // inserti赋值为当前的hash高8位 标记写入成功
                    inserti = &b.tophash[i]
                    // 略...
                }
                // 略...
                continue
            }
            // 略...
            goto done
    }
    // 正常桶的bmap遍历完了 继续遍历溢出桶的bmap 如果有的话
        ovf := b.overflow(t)
        if ovf == nil {
            break
    }
        b = ovf
    }

  // 略...

  // 没写入成功(包含正常桶的bmap、溢出桶的bmap(如果有的话))
    if inserti == nil {
    // 分配新的bmap写
    newb := h.newoverflow(t, b)
    // 略...
    }

    // 略...
}

// 继续看h.newoverflow的代码
func (h *hmap) newoverflow(t *maptype, b *bmap) *bmap {
  var ovf *bmap
  // 如果hmap的存在溢出桶 且 溢出桶还没用完
    if h.extra != nil && h.extra.nextOverflow != nil {
    // 使用溢出桶的bmap
    ovf = h.extra.nextOverflow
    // 判断桶的bmap的overflow是不是空
    // 这里很巧妙。为啥?
    // 溢出桶初始化的时候会把最后一个bmap的overflow指向正常桶,值不为nil
    // 目的判断当前这个bmap是不是溢出桶里的最后一个
        if ovf.overflow(t) == nil {
      // 是nil
      // 说明不是最后一个
            h.extra.nextOverflow = (*bmap)(add(unsafe.Pointer(ovf), uintptr(t.bucketsize)))
        } else {
      // 不是nil
      // 则重置当前bmap的overflow为空
      ovf.setoverflow(t, nil)
      // 且 标记nextOverflow为nil 说明当前溢出桶用完了
            h.extra.nextOverflow = nil
        }
    } else {
    // 没有溢出桶 或者 溢出桶用完了
    // 内存空间重新分配一个bmap
        ovf = (*bmap)(newobject(t.bucket))
  }
  // 生成溢出桶bmap的计数器计数
    h.incrnoverflow()
  // 略...
  // 这行代码就是上面问题我们要的答案:
  // 正常桶`hmap.buckets`里的`bmap`在这里关联上溢出桶`hmap.extra.overflow`的`bmap`
    b.setoverflow(t, ovf)
    return ovf
}

// setoverflow函数的源码
func (b *bmap) setoverflow(t *maptype, ovf *bmap) {
  // 这行代码的意思:通过偏移量计算找到了bmap.overflow,并把ovf这个bmap的地址赋值给了bmap.overflow
    *(**bmap)(add(unsafe.Pointer(b), uintptr(t.bucketsize)-sys.PtrSize)) = ovf
}

下面代码这段代码解释了,上面的源码中为何如此判断预分配溢出桶的bmap是最后一个的原因。

// https://github.com/golang/go/blob/go1.13.8/src/runtime/map.go
// 创建hmap的正常桶
func makeBucketArray(t *maptype, b uint8, dirtyalloc unsafe.Pointer) (buckets unsafe.Pointer, nextOverflow *bmap) {
  // 略...
    if base != nbuckets {
    // 略...
    last := (*bmap)(add(buckets, (nbuckets-1)*uintptr(t.bucketsize)))
    // 把溢出桶里 最后一个 `bmap`的`overflow`指先正常桶的第一个`bmap`
    // 获取预分配的溢出桶里`bmap`时,可以通过判断overflow是不是为nil判断是不是最后一个
        last.setoverflow(t, (*bmap)(buckets))
  }
  // 略...
}

hmap存在溢出桶时,且当前溢出桶只被使用了一个bmap时,我们可以得到如下的关系图:

http://cdn.tigerb.cn/20201217165310.png

同时我们可以看出正常桶的bmap和溢出桶的bmap实际构成了链表关系,所以这也解释了开篇我们说到的“Go里面Map的实现主要用到了数组”,其次还用到了链表。

再次分析Map的读

收益6: 熟悉Go语言Map是如何读取数据的

通过上面的学习,我们再次通过一次读操作为例,看看读取某个key的值的一个大致过程:

http://cdn.tigerb.cn/20201217165551.png

结合代码分析下整个大体的过程:

func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
    // ...略
    
    // ①通过hash函数获取当前key的哈希
    hash := alg.hash(key, uintptr(h.hash0))
    m := bucketMask(h.B)
    // ②通过当前key的哈希获取到对应的bmap结构的b
    // 这里的b 我们称之为“正常桶的bmap”
    // “正常桶的bmap”可能会对应到溢出桶的bmap结构,我们称之为“溢出桶的bmap”
    b := (*bmap)(add(h.buckets, (hash&m)*uintptr(t.bucketsize)))
    
    // ...略
    
    // 获取当前key的哈希的高8位
    top := tophash(hash)
bucketloop:
    // 下面的for循环是个简写,完整如下。
    // for b = b; b != nil; b = b.overflow(t) {
    // 可以知道b的初始值为上面的“正常桶的bmap”,则:
    // 第一次遍历:遍历的是“正常桶的bmap”
    // 如果正常桶没找到,则
    // 绿色线条④ 继续遍历:如果当前“正常桶的bmap”中的overflow值不为nil(说明“正常桶的bmap”关联了“溢出桶的bmap”),则遍历当前指向的“溢出桶的bmap”继续 蓝色线条的③④⑤步骤
    for ; b != nil; b = b.overflow(t) {
        // 由于b的初始值为“正常桶的bmap”,第一次先遍历“正常桶的bmap”
        for i := uintptr(0); i < bucketCnt; i++ {
            // 蓝色线条③ 对比key哈希的高8位
            // 对比哈希的高8位目的是为了加速
            if b.tophash[i] != top {
                // emptyRest 标志位:表示当前位置已经是末尾了;删除操作会设置此标志位
                if b.tophash[i] == emptyRest {
                    break bucketloop
                }
                continue
            }
            // 找到了相同的hash高8位,则:找到对应索引位置i的key
            k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
            if t.indirectkey() {
                k = *((*unsafe.Pointer)(k))
            }
            // 蓝色线条④ 对比key是不是一致
            if alg.equal(key, k) {
                // 蓝色线条⑤ key是一致,则:获取对应索引位置的值
                e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
                if t.indirectelem() {
                    e = *((*unsafe.Pointer)(e))
                }
                // 返回找到的结果
                return e
            }
        }
    }
    // 正常桶、溢出桶都没找到则返回 “空值”
    return unsafe.Pointer(&zeroVal[0])
}
参考:
1.《Go语言设计与实现》https://draveness.me/golang/docs/part2-foundation/ch03-datastructure/golang-hashmap/
2. Go源码版本1.13.8 https://github.com/golang/go/tree/go1.13.8/src

3911642037-d2bb08d8702e7c91_articlex.jpg

查看《Go语言轻松系列》更多内容

链接 http://tigerb.cn/go/#/kernal/

查看原文

赞 3 收藏 2 评论 4

TIGERB 分享了头条 · 2020-12-21

今天要分享的是主要内容是Go语言Map底层实现,目的让大家快速了解Go语言Map底层大致的实现原理。

赞 1 收藏 3 评论 0

TIGERB 分享了头条 · 2020-11-05

嗯,Go设计模式实战系列,一个设计模式业务真实使用的golang系列。

赞 1 收藏 1 评论 0

TIGERB 分享了头条 · 2020-10-27

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

赞 2 收藏 0 评论 0

TIGERB 关注了专栏 · 2020-07-09

人云思云

码农一只

关注 208

TIGERB 发布了文章 · 2020-07-01

你想知道的优惠券业务,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名单,根据不同用户级别设置限制策略。等其他大数据分析手段。

阈值

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

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

优惠券不要支持虚拟商品

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


3911642037-d2bb08d8702e7c91_articlex.jpg

查看原文

赞 3 收藏 3 评论 0

TIGERB 发布了文章 · 2020-06-02

客户决策 | 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 发布了文章 · 2020-06-02

状态变换 | 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 分享了头条 · 2020-05-07

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

赞 0 收藏 3 评论 0

TIGERB 发布了文章 · 2020-05-07

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

前言

本文结构很简单:

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

查看原文

赞 59 收藏 39 评论 2

TIGERB 发布了文章 · 2020-04-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 发布了文章 · 2020-04-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