前言
数据驱动编程的核心出发点是相对于程序逻辑,人类更擅长于处理数据。数据比程序逻辑更容易驾驭,所以我们应该尽可能的将设计的复杂度从程序代码转移至数据。
我们在看实现业务需求的时候,常常会震惊于需求的复杂性,prd写的晦涩难懂,一言难尽。照着prd写需求写的云里雾里的,也不知道到底对不对。这时候,可以试试数据驱动的方法来分析需求,简化业务逻辑。
本文以笔者工作中遇到的实际例子,示例数据驱动编程的思想是怎么简化业务逻辑思考量的。
需求简介
做的是一个离线状态展示的逻辑,在用户B离线时,符合条件的用户A可以看到B上次的在线时间。是否可以看到,是通过A、B的last seen(上次见到时间)可见性配置以及两者的关系算出来的。
prd逻辑
每个用户可以配置三个级别的可见性配置:
- nobody:谁都不能看见我的上次在线时间
- contact:我的通讯录可见
- everybody:谁都可见(默认)
lastseen显示逻辑:
- 两人有任意拉黑关系,不展示last seen
- 判断A和B的lastseen配置,是否存在 nobody
- 若存在,则不展示last seen
- 若不存在,则判断A和B是否互为通讯录联系人
- 若是,则双方均展示last seen
- 若不是,且A和B都不是对方的通讯录联系人,则判断A和B的last seen配置是否存在contact
- 若存在,则均不展示last seen
- 若不存在,则均展示last seen
- 若不是,且A和B有一方是对方的通讯录联系人,则根据通讯录联系人中不包含对方的用户的last seen显示范围是否为contact
- 若是,则均不展示last seen
- 若不是,则均展示last seen
数据
lastseen配置数据就是按上面的三个,以及未设置时默认为everybody
每个用户看另一个用户的通讯录关系:
- follow:关注,其实就是在自己的通讯录中
- delete:已删除
- unrelated:没关系
另外还有个拉黑关系:
- nonblock:未拉黑(默认)
- block:拉黑
需求分析
不知道你有没看傻眼,反正我看着prd的逻辑是混乱了。
其实我是在优化之前的代码,原代码逻辑是基本顺着prd逻辑来的,结果原逻辑就成了差不多这样:
- 任意一方拉黑,无权限
- 检查设置&好友关系
- 有一方设置为nobody,无权限
- 两方都为everybody,无权限
- 双向关注,可以看
- A->B 并且B为everybody,有权限
- B->A 并且A为everybody,有权限
- 其他,无权限
relation, err := rpc.GetRelation(ctx, t.subscriber, t.subscribed)
if err != nil {
return err
}
if relation != nil {
if relation.BlockStatus == BLOCK || relation.RevBlockStatus == BLOCK {
return NoPermission
}
}
if t.subscriberLastSeen == NoBody || t.subscribedLastSeen == NoBody {
return NoPermission
} else if t.subscriberLastSeen == EveryBody && t.subscribedLastSeen == NoBody {
return HasPermission
} else {
if relation != nil {
if relation.Relation == FOLLOW && relation.RevRelation == FOLLOW {
return HasPermission
} else if relation.Relation == FOLLOW && t.subscribedLastSeen != Contacts {
return HasPermission
} else if relation.RevRelation == FOLLOW && t.subscriberLastSeen != Contacts {
return HasPermission
} else {
return NoPermission
}
}
return NoPermission
}
}
读了好多遍才理顺,特别是最后那边 !=Contacts 得按 ==everybody来看。别看这个代码好像还算清晰,我是已经为了方便各位看,优化调整过的,原代码乱多了,并且不是这样直接return常量的,是去通知状态的。
嵌套层数一多,思考难度就几何上升。写面条代码实在痛苦,于是逼出了数据驱动来搞定。
状态分析
直接来看,A和B这个系统的状态有6维:LastseenA,LastseenB,A->B,B->A,A block B,B block A
这样,状态的排列组合就有 3 * 3 * 3 * 3 * 2 * 2 种
当然,可以通过枚举列出所有的排列组合下A是否可以看见B的lastseen来得到结果,但是心智负担过大。因此必须通过方法降维度。
观察prd可知:
- 此配置的设计贯穿着“权利对等”的思想,也就是说,我能看到你的lastseen,你就能看到我的lastseen,反之也成立。
- A、B的状态对调后,得出的权限结果是相等的。也就是说和谁在看谁没有关系。也就是说只用判断单向的组合就够了,判断了A->B的各种排列组合,也就相当于判断了A<-B的那些镜像组合。因此也就不需要区分是在判断A看B还是B看A。
降维
A、B的关系可以直接降成:
- anyBlock(最高优先级)
- noFollow
- A2B
- B2A
- bidirFollow
| A对B \ B对A | 拉黑 | 非关注 | 关注 |
|---|---|---|---|
| 拉黑 | anyBlock | anyBlock | anyBlock |
| 非关注 | anyBlock | noFollow | B2A |
| 关注 | anyBlock | A2B | bidirFollow |
A、B的设置也可以降维成
- anyNobody(最高优先级)
- contacts
- AcBe
- AeBc
- everybodys
| A \ B | nobody | contact | everybody |
|---|---|---|---|
| nobody | anyNobody | anyNobody | anyNobody |
| contact | anyNobody | contacts | AcBe |
| everybody | anyNobody | AeBc | everybodys |
lastseen可见性表
经过这么一通操作,最终可能性就被压缩到了这么一个二维表中
| lastseen可见性 | anyBlock | noFollow | A2B | B2A | bidirFollow |
|---|---|---|---|---|---|
| anyNobody | x | x | x | x | x |
| contacts | x | x | x | x | o |
| AcBe | x | x | o | x | o |
| AeBc | x | x | x | o | o |
| everybodys | x | o | o | o | o |
实际编程中,可以根据情况把单值的行或列单独提出来判断,以降低表的大小。
比如在上表中,可以先对anyblock和nobody判断,有任意就直接返回false,状态组合就被压缩到了4*4种可能性。
示例代码
ABLastSeen
type ABLastSeenStatus int
const (
anyNobody ABLastSeenStatus = iota
Contacts // A contact && B contact
AcBe // A contact && B everybody
AeBc // A everybody && B contact
Everybodys // A everybody && B everybody
)
func GetLastSeenStatus(lastseenOfA, lastseenOfB string) ABLastSeenStatus {
if lastseenOfA == NoBody || lastseenOfB == NoBody {
return anyNobody
}
if lastseenOfA == Contact && lastseenOfB == Contact {
return Contacts
}
if lastseenOfA == Contact {
return AcBe
}
if lastseenOfB == Contact {
return AeBc
}
return Everybodys
}
ABRelationStatus
type ABRelationStatus int
const (
NoFollow ABRelationStatus = iota
AFollowB
BFollowA
BidirFollow
)
func GetABRelationStatus(A2B Relation, B2A Relation) ABRelationStatus {
if A2B == FOLLOW && B2A == FOLLOW {
return BidirFollow
}
if A2B == FOLLOW {
return AFollowB
}
if B2A == FOLLOW {
return BFollowA
}
return NoFollow
}
核心逻辑
func CanSeeLastSeen(lastSeenOfFrom, lastSeenOfTo string, relation Relation) bool {
relationFrom2To := UNRELATED
relationTo2From := UNRELATED
if relation != nil {
if relation.BlockStatus == BLOCK || relation.RevBlockStatus == BLOCK {
return false
}
relationFrom2To = relation.RelationStatus
relationTo2From = relation.RevRelationStatus
}
return CanSeeLastSeen(lastSeenOfFrom, lastSeenOfTo, relationFrom2To, relationTo2From)
}
func canSeeLastSeen(lastSeenOfA, lastSeenOfB string, relationA2B, relationB2A Relation) bool {
lastSeenStatus := GetLastSeenStatus(lastSeenOfA, lastSeenOfB)
relation := GetABRelationStatus(relationA2B, relationB2A)
return CanSeeLastSeenByStatus(lastSeenStatus, relation)
}
type statusCombine struct {
ABLastSeenStatus
ABRelationStatus
}
// NoFollow AFollowB BFollowA BidirFollow
// AnyNobody x x x x
// Contacts x x x o
// AcBe x o x o
// AeBc x x o o
// Everybodys o o o o
var canSeeLastSeenMap = map[statusCombine]bool{
{Contacts, BidirFollow}: true,
{AcBe, BidirFollow}: true,
{AeBc, BidirFollow}: true,
{Everybodys, BidirFollow}: true,
{Everybodys, NoFollow}: true,
{Everybodys, AFollowB}: true,
{Everybodys, BFollowA}: true,
{AcBe, AFollowB}: true,
{AeBc, BFollowA}: true,
}
func CanSeeLastSeenByStatus(lastSeenStatus ABLastSeenStatus, relationStatus ABRelationStatus) bool {
s := statusCombine{
ABLastSeenStatus: lastSeenStatus,
ABRelationStatus: relationStatus,
}
return canSeeLastSeenMap[s]
}
可以看到,最后的驱动表实际我是用一个map来实现的,由于为true的情况相对少,而且false是兜底值,这样可以在同时兼顾处理速度和空间占用。
当然,驱动表也可以用数组扫描之类的方式来实现。
结语
本文示例了下我是怎么实践数据驱动编程思想的。其他业务中可能不是简单的true/false判断,那么可以把返回值变成各状态下的处理函数handler,即可实现复杂逻辑。
文中的Go代码没法直接运行,是为了方便阅读,去除了命名空间之类的多余信息的,但几乎就是最终代码了。
什么?常量命名方式不统一? 累了,不想改了,凑合就这么吧。
希望能对各位有一点启发。

本文通过实例展示了如何使用数据驱动编程思想来简化一个离线状态展示的需求分析和实现。原始需求涉及多个条件判断,包括用户之间的可见性配置、通讯录关系和拉黑状态。通过状态分析和降维处理,将6维状态压缩到二维表格,最后用映射表实现核心逻辑,降低了代码复杂性和理解难度,体现了数据驱动编程的优势。

649

被折叠的 条评论
为什么被折叠?



