实现“第 24”种设计模式

传统方案

if-else 在我们编程时出现的频率,无需我多赘述。当逻辑复杂时,我们会写出很多 if-else 语句,于是网络上充斥着大量的相关文章,教我们如何去除if-else,大多大同小异。

归结下来,无非是策略模式、责任链模式等套路,它们目的都是一样的,都希望把逻辑打散在一个个类中,再通过一个工厂或者什么别的玩意去调度它们。

这些策略可以很好的处理以下语句:

if (cond) {
    do
} else if (cond){
    do
} else if (cond){
    do
} else if (cond){
    do
} else {
    do
}

由于 go 本身是支持函数作为一等公民,所以不怎么需要面向对象语言的设计模式,Java 虽然需要通过一些设计模式的支持,但总归也不是很复杂,这里以 go 为例:

package main

type Cond func(param interface{}) bool
type Do func()

var cond1 = func(param interface{}) bool {
   return false
}

var do1 = func() {}

var condMap = map[Cond]Do{
   cond1: do1,
}

func main(){
    for k, v := range condMap {
       if k(param) {
          v()
       }
    }
}

我们把条件和动作设计成两个函数:Cond \ Do ,就可以用 Map 去存储和扩展它。
Cond 和 Do 可以无限的新增下去。

更多的思考🤔

以上传统方案做法上没什么问题,但在设计上,却存在两个问题:

一、存在过度设计的嫌疑

首先, 如果真的只是例子中这种扁平的(指一层)if-else,即使它加到 100 个,也不会很复杂,本来也就很符合人类的思维习惯,一个个往下看就是了(人类很喜欢顺序和规律)。

过度设计是指为不属于当前业务需求(或短期可预见的需求)进行设计,说白了就是臆想一些场景,去兼容这些场景做设计。

如果可以预见的,这些扁平的if-else不会有大量的新增、删除操作,那就没必要使用这些设计模式;如果可以预见这些 if-else 会大量新增、删除,或者会被其他人维护,那使用这些模式进行改造倒是可行的。

二、无法解决if-else嵌套问题

对于多层的if-else,示例代码如下:

if (cond){
    if (cond){
        do
    } else if (cond){
        if (cond) {
            do
        } else if (cond){
            if (cond) {
                do
            } else if (cond){
                do
            } else if (cond){
                if (cond) {
                    do
                } else if (cond){
                    do
                } else {
                    do
                }
            }
        } else {
            do
        }
    }
} else {
    do
}

相信这种嵌套的if-else才是日常开发经常碰到的情况,可能没有示例这么夸张,但我们可以看出,这种不太好用策略或者责任链去解决。

策略套策略?也层层嵌套下去?

这种代码会更加的复杂,还不如不重构。

新方案:分支树

思考一下,这种嵌套的 if-else 是不是构成了一棵分支树,树的叶子节点就是某种操作,树的非叶子节点就是某个判断。

进入公园分支树.png

我们以一个日常生活场景:“入园政策”为例,这个场景就构成了一棵分支树。那么既然是树结构,我们就应该用树结构去表示它。

在机器学习中,也有相似的概念,称之为决策树。但是,机器学习的目标主要是怎么通过大量的数据,识别和构造出一个决策树,也就是图中这些条件本来是不存在的,需要通过学习去自动生成。而我们的场景是已存在 if-else 构成的分支树,我们要通过一种树结构去优化表示它。所以目的是不同的,我也用了新的名字:分支树。

要构造这样一棵分支树,我们先规定好树节点,分为两类:决策节点和叶子节点。决策节点负责判断数据是否符合条件,是则进入它的子节点,否则访问它的下个兄弟节点;叶子节点包含执行逻辑,一个个决策节点走下来,最终一定要走到一个叶子节点,不然这棵树就没有意义。

未命名绘图.drawio.png

节点接口定义如上,DecisionRule 用来决策,ChildrenList 用于构造树。抽象了一个 Node,用于标识节点。

这里我原本想试试把接口方法中的 interface{} 换成泛型参数[T any],但实际上做不到。似乎是 Go 不支持这种泛型方法,各位可以试试。

有了节点,我们接下来要实现这棵分支树:

未命名绘图.drawio (1).png

如同我们实现一棵二叉树一样,我们一般都会定义一个根节点,它起到一个引领作用,没有根节点,我们无法拿到这个树。

我们还定义了一个节点 Map,保存了这棵树的所有节点,这与一般的树实现不同,这是为什么呢?

这是为了松耦合,我们在前面的接口定义中,将ChildrenList的返回值规定为[]string,也就是不需要返回 Node 结构,只需要返回 ID 标识。这样,我们就无需一定要先定义出叶子节点,只需要规定好叶子节点的 ID 标识。

这样的话,我们不需要从下到上的顺序去实现树的节点,而是可以以任意顺序,只要 ID 能对的上即可。所以我们需要一个节点 Map,能够根据 ID 标识去找到这个节点。

AddNode() 可以添加一个节点到树中,AddNode 会自动识别,把节点放在它在树中的正确位置。

当树的所有节点都添加好了,调用 Decision() ,遍历这个分支树,遍历到某个叶子节点,就会执行它的 Do 函数。这个遍历我们应该选用 BFS 遍历,不能使用 DFS 遍历,因为我们要一层一层的处理。

我们用一个实际的分支树来做个例子:

//                                           ┌──────┐
//                                           │ Root │
//                                           └──────┘
//                                               ▲
//                  ┌────────────────────────────┼──────────────────────┐
//                  │                            │                      │
//           ┌────────────┐          ┌───────────────────────┐   ┌─────────────┐
//           │  age < 18  │          │ age >= 18 & age < 60  │   │  age >= 60  │
//           └────────────┘          └───────────────────────┘   └─────────────┘
//                  ▲                            ▲                      ▲
//          ╱──────╱ ╲──────╲                    │                      │
//         ╱                 ╲            ╔════════════╗       ╔═════════════════╗
// ┌───────────────┐ ┌───────────────┐    ║ buy ticket ║       ║ discount ticket ║
// │ height < 1.2  │ │ height >= 1.2 │    ╚════════════╝       ╚═════════════════╝
// └───────────────┘ └───────────────┘
//         ▲                 ▲
//         │                 │
//    ╔═════════╗     ╔════════════╗
//    ║  enter  ║     ║ buy ticket ║
//    ╚═════════╝     ╚════════════╝

使用步骤如下代码所示:


func main() {
   tree := branch_tree.DecisionTree{}

   // 1. 定义叶子节点
   // 如上所说,我们例子里虽然先定义了叶子节点,但实际上这个顺序是可以打乱的
   enterNode := &EnterPark{}
   buyTicketNode := &BuyTicket{}
   discountTicketNode := &DiscountTicket{}

   // 2. 定义决策节点
   lessHeight := &HeightDecisionNode{
      Id: "H<120cm",
      DecisionFunc: func(param interface{}) bool {
         if p, ok := param.(*People); ok {
            return p.Height < 120
         }
         return false
      },
      Childs: []string{enterNode.ID()},
   }
   geHeight := &HeightDecisionNode{
      Id: "H>=120cm",
      DecisionFunc: func(param interface{}) bool {
         if p, ok := param.(*People); ok {
            return p.Height >= 120
         }
         return false
      },
      Childs: []string{buyTicketNode.ID()},
   }

   ageLess18 := NewAgeDecisionNode(0, 18)
   ageIn18And60 := NewAgeDecisionNode(18, 60)
   ageG60 := NewAgeDecisionNode(60, math.MaxInt)

   ageLess18.SetChilds([]string{lessHeight.ID(), geHeight.ID()})
   ageIn18And60.SetChilds([]string{buyTicketNode.ID()})
   ageG60.SetChilds([]string{discountTicketNode.ID()})
   

   // 3. 把以上节点加到树中
   err := tree.AddNode(enterNode)
   err = tree.AddNode(buyTicketNode)
   err = tree.AddNode(discountTicketNode)
   err = tree.AddNode(lessHeight)
   err = tree.AddNode(geHeight)
   err = tree.AddNode(ageLess18)
   err = tree.AddNode(ageIn18And60)
   err = tree.AddNode(ageG60)
   if err != nil {
      panic(err)
   }

   // 4. 接下来把数据输入到树中决策。
   err = tree.Decision(&People{
      Age:    16,
      Height: 150,
   })
   if err != nil {
      panic(err)
   }
   // 应该执行:Buy ticket

   err = tree.Decision(&People{
      Age:    8,
      Height: 110,
   })
   if err != nil {
      panic(err)
   }
   // 应该执行:Enter

   err = tree.Decision(&People{
      Age:    61,
      Height: 160,
   })
   if err != nil {
      panic(err)
   }
   // 应该执行:Discount

   err = tree.Decision(&People{
      Age:    45,
      Height: 160,
   })
   if err != nil {
      panic(err)
   }
   // 应该执行:Buy ticket

   // Run result:
   // I will buy ticket.
   // I will enter park.
   // I will discount.
   // I will buy ticket.
}

所有代码均在 Github 开源,欢迎 Star。

感谢观看~ 🎶

求职(远程、Golang)启事
本人非 985、211,普通本科大学,无大厂经历😂。
有一颗对技术充满热情的心和喜欢独立思考的大脑,单兵作战能力强。
SAIL作者,设计开发过多款业务、技术系统,也经历过高并发的洗礼。
如果您愿意让我成为您的同事,可私聊我或者通过邮箱(690174435@qq.com)与我联系。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值