使用数据驱动思想简化业务逻辑

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

前言

数据驱动编程的核心出发点是相对于程序逻辑,人类更擅长于处理数据。数据比程序逻辑更容易驾驭,所以我们应该尽可能的将设计的复杂度从程序代码转移至数据。

我们在看实现业务需求的时候,常常会震惊于需求的复杂性,prd写的晦涩难懂,一言难尽。照着prd写需求写的云里雾里的,也不知道到底对不对。这时候,可以试试数据驱动的方法来分析需求,简化业务逻辑。

本文以笔者工作中遇到的实际例子,示例数据驱动编程的思想是怎么简化业务逻辑思考量的。

需求简介

做的是一个离线状态展示的逻辑,在用户B离线时,符合条件的用户A可以看到B上次的在线时间。是否可以看到,是通过A、B的last seen(上次见到时间)可见性配置以及两者的关系算出来的。

prd逻辑

每个用户可以配置三个级别的可见性配置:

  1. nobody:谁都不能看见我的上次在线时间
  2. contact:我的通讯录可见
  3. 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

每个用户看另一个用户的通讯录关系:

  1. follow:关注,其实就是在自己的通讯录中
  2. delete:已删除
  3. unrelated:没关系

另外还有个拉黑关系:

  1. nonblock:未拉黑(默认)
  2. block:拉黑

需求分析

不知道你有没看傻眼,反正我看着prd的逻辑是混乱了。

其实我是在优化之前的代码,原代码逻辑是基本顺着prd逻辑来的,结果原逻辑就成了差不多这样:

  1. 任意一方拉黑,无权限
  2. 检查设置&好友关系
    1. 有一方设置为nobody,无权限
    2. 两方都为everybody,无权限
    3. 双向关注,可以看
    4. A->B 并且B为everybody,有权限
    5. B->A 并且A为everybody,有权限
    6. 其他,无权限
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可知:

  1. 此配置的设计贯穿着“权利对等”的思想,也就是说,我能看到你的lastseen,你就能看到我的lastseen,反之也成立。
  2. A、B的状态对调后,得出的权限结果是相等的。也就是说和谁在看谁没有关系。也就是说只用判断单向的组合就够了,判断了A->B的各种排列组合,也就相当于判断了A<-B的那些镜像组合。因此也就不需要区分是在判断A看B还是B看A。

降维

A、B的关系可以直接降成:

  • anyBlock(最高优先级)
  • noFollow
  • A2B
  • B2A
  • bidirFollow
A对B \ B对A拉黑非关注关注
拉黑anyBlockanyBlockanyBlock
非关注anyBlocknoFollowB2A
关注anyBlockA2BbidirFollow

A、B的设置也可以降维成

  • anyNobody(最高优先级)
  • contacts
  • AcBe
  • AeBc
  • everybodys
A \ Bnobodycontacteverybody
nobodyanyNobodyanyNobodyanyNobody
contactanyNobodycontactsAcBe
everybodyanyNobodyAeBceverybodys

lastseen可见性表

经过这么一通操作,最终可能性就被压缩到了这么一个二维表中

lastseen可见性anyBlocknoFollowA2BB2AbidirFollow
anyNobodyxxxxx
contactsxxxxo
AcBexxoxo
AeBcxxxoo
everybodysxoooo

实际编程中,可以根据情况把单值的行或列单独提出来判断,以降低表的大小。
比如在上表中,可以先对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代码没法直接运行,是为了方便阅读,去除了命名空间之类的多余信息的,但几乎就是最终代码了。

什么?常量命名方式不统一? 累了,不想改了,凑合就这么吧。

希望能对各位有一点启发。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值