实现“第 24”种设计模式

news2025/8/1 15:02:16

传统方案

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)与我联系。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/367773.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

凌恩生物资讯|抗性宏基因组又一力作|抗性基因+可移动元件研究新成果!

凌恩生物合作客户&#xff1a;合肥工业大学崔康平老师团队利用凌恩生物宏基因组抗性基因研究解决方案&#xff0c;对污水处理厂活性污泥中的钆&#xff08;Gd&#xff08;III&#xff09;&#xff09;和抗生素磺胺甲噁唑&#xff08;SMX&#xff09;的联合污染情况进行了调查&a…

华为OD机试题,用 Java 解【滑动窗口最大和】问题

最近更新的博客 华为OD机试 - 猴子爬山 | 机试题算法思路 【2023】华为OD机试 - 分糖果(Java) | 机试题算法思路 【2023】华为OD机试 - 非严格递增连续数字序列 | 机试题算法思路 【2023】华为OD机试 - 消消乐游戏(Java) | 机试题算法思路 【2023】华为OD机试 - 组成最大数…

前端无障碍适配

无障碍简介&#xff1a; 帮助一些视障群体使用手机&#xff0c;点击的热区会增加配合文字识别增加一些语音播报的功能&#xff0c;手机一般可以通过&#xff1a;设置—》辅助功能—》无障碍功能菜单 体验无障碍功能 IOS&#xff1a;设置–》辅助功能----》旁白 需求背景 会有…

Allegro如何显示层叠Options和Find操作界面

Allegro如何显示层叠Options和Find操作界面 Allegro常规有三大操作界面,层叠,Options和Find,如下图 软件第一次启动的时候,三大界面是关闭的,下面介绍如何把它们打开,具体操作步骤如下 点击菜单上的View点击Windows

JavaScript 进阶(面试必备)--charater4

文章目录前言一、深浅拷贝:one: 浅拷贝:two:深拷贝二、异常处理:one: throw 抛异常:two: try /catch 捕获异常:three:debugger三、处理thisthis指向 :one:普通函数this指向this指向 :two: 箭头函数this指向3.2 改变this:one: call():two: apply():three: bind()四、性能优化:on…

优化算法(寻优问题)

前言 群智能算法&#xff08;全局最优&#xff09;&#xff1a;模拟退火算法&#xff08;Simulated annealing&#xff0c;SA&#xff09;&#xff0c;遗传算法&#xff08;Genetic Algorithm, GA&#xff09;&#xff0c;粒子群算法&#xff08;Particle Swarm Optimization&…

前端:你不知道的async await

1.先抛出一个场景&#xff1a;你是否在日常开发中经常使用类似代码&#xff1f;async function getXXList () {const result await this.getArrListApi({page:1,id:2})this.arr result.data.listconsole.log(结果是…, this.arr)……………………其他逻辑代码 }1.1 问题那你是…

OM | 顶刊论文解读:一种求解最大边权团问题的精确算法

解读人&#xff1a;曲晨辉&#xff0c;陈盈鑫&#xff0c;孙楚天&#xff0c;杨李平&#xff0c;张云天 编者按 本次解读的文章是于2020年发表在INFORMS Journal on Computing的“A Lagrangian Bound on the Clique Number and an Exact Algorithm for the Maximum Edge Weigh…

双指针、字符串、哈希表、链表、数组总结

目录总结1、交换元素swap2、链表设置哑结点3、while(cur -> next ! nullptr)代表运行到倒数第二个元素&#xff0c;也就是cur此时为倒数第一个元素4、在cur初始指向哑结点时&#xff0c;下面执行cur指向index的前一个节点5、关于链表&#xff0c;什么移动删除元素等&#xf…

关于用windows开发遇到的各种乌龙事件之node版本管理---nvm install node之后 npm 找不到的问题

友情提醒&#xff0c;开发最好用nvm控制node版本 nrm 控制镜像源&#xff0c;能少掉很多头发开发过程中技术迭代更新的时候最要老命的就是 历史项目的node版本没有记录&#xff0c;导致开启旧项目的时候就会报错。尤其是npm 升级到8.x.x以后&#xff0c;各种版本不兼容。 真…

如何5分钟跑起来一个完整项目?

今天熊哥和大家聊聊&#xff0c;我怎么在5分钟之内生成一个完整的项目。 效果 看看这个面板&#xff0c;这居然是我花了5分钟成功跑起来的项目。 竟然具备超过三十项功能。还可以直接在页面上生成代码。 它是什么&#xff1f;它是 go-gin-api 它支持哪些功能&#xff1f; 可…

【OpenAI】基于 Gym-CarRacing 的自动驾驶练习项目 | 路径训练功能的实现 | GYM-Box2D CarRacing

限时开放&#xff0c;猛戳订阅&#xff01; &#x1f449; 《一起玩蛇》&#x1f40d; &#x1f4ad; 写在前面&#xff1a; 本篇是关于多伦多大学自动驾驶专业项目的博客。GYM-Box2D CarRacing 是一种在 OpenAI Gym 平台上开发和比较强化学习算法的模拟环境。它是流行的 Box2…

RocketMQ实现延迟队列精确到秒级实现

前言篇&#xff1a;为了节约成本&#xff0c;决定通过自研来改造rocketmq&#xff0c;添加任意时间延迟的延时队列&#xff0c;开源版本的rocketmq只有支持18个等级的延迟时间&#xff0c;其实对于大部分的功能是够用了的&#xff0c;但是以前的项目&#xff0c;全部都是使用了…

剑指 Offer 12. 矩阵中的路径

⭐简单说两句⭐ CSDN个人主页&#xff1a;后端小知识 &#x1f50e;GZH&#xff1a;后端小知识 &#x1f389;欢迎关注&#x1f50e;点赞&#x1f44d;收藏⭐️留言&#x1f4dd; Hello吖&#xff0c;各位小伙伴大家好呀&#xff0c;今天我采用了一种特别的方式&#x1f60e;来…

《关于我找了好久的bug,却没找出来的,又不小心解决了的事》

个人简介 作者简介&#xff1a;大家好&#xff01;我是yukki。个人主页&#xff1a;yukki. 喜欢&#xff1a;&#x1f308;点赞&#x1f308;收藏&#x1f308;一键三连&#xff01;共勉问题&#xff1a; 这是一个SpringBoot问题 刚开始很正常可以启动&#xff0c;但是加了r…

键盘布局持久化技术

**01 **键盘布局简介 键盘布局是按键在键盘上的分布模式&#xff0c;决定了键位顺序。键盘布局在发展过程中&#xff0c;由于使用习惯的不同&#xff0c;各国间使用的键盘布局存在细微差别&#xff0c;因此在Windows系统上以国家为单位区分不同的键盘布局方案。我们最熟悉的布…

后端接收格式为x-www-form-urlencoded的数据

1.x-www-form-urlencoded是什么&#xff1f; x-www-form-urlencoded纸面翻译即所谓url格式的编码&#xff0c;是post的默认Content-Type&#xff0c;其实就是一种编码格式&#xff0c;类似json也是一种编码传输格式。form表单中使用 form的enctype属性为编码方式&#xff0…

【MySQL】5.7版本解压安装配置

前言 之所以使用解压版本&#xff0c;而不使用exe安装&#xff0c;因为exe的安装方式删除过于麻烦&#xff01;&#xff01;&#xff01; 如果安装MySQL过程中&#xff0c;出错了或者想重新在来一把&#xff0c;删除mysql服务即可 sc delete mysql # 删除已经安装好的Mysql&a…

ifconfig不显示ipv4地址,ifconfig eth0 192.168.5.9失败

ifconfig eth0 192.168.5.9设置ip地址后&#xff0c;通过ifconfig仍然没有ipv4地址&#xff1a; 一、 执行ifup eth0启动eth0: ifconfig、ifup、ifdown &#xff1a;这三个命令的用途都是启动网络接口&#xff0c;不过&#xff0c;ifup 与 ifdown 仅就 /etc/sysconfig/network-…

【数据结构】红黑树

红黑树一、红黑树的概念二、红黑树的接口2.1 插入三、验证四、源码一、红黑树的概念 红黑树也是一个二叉搜索树&#xff0c;他是通过对任何一条从根到叶子的路径上各个结点着色方式的限制&#xff0c;最长路径长度不超过最短路径长度的 2 倍保持近似平衡。他在每个节点添加了一…