函数式编程思维下的Go语言重构之路

超级欧派课程 2024-05-21 07:41:42

作为一名编程语言的爱好者,我特别偏好函数式编程的风格。对我来说,将算法抽象为一系列数据及其转换就仿佛是在脑海中构造一幅画作,这与面向对象编程中侧重于包含行为的对象的理念有着本质的不同。从我的角度出发,当代码中出现反复的模式时,我首先考虑的是如何利用高阶函数将这一模式进行抽象化处理,以提升代码的复用性和简洁性。

话虽如此,我的日常职责却是使用Go语言进行编程。我对Go有着复杂的情绪,既有喜爱也伴有批评,因此我并不希望开启关于Go的无休止争辩。不过,相信对于Go并不是专为函数式编程设计的这一观点,是不会引发太多争议的。

模式的探索

在我的工作中,我开发出了一种新的加速算法,用于加强我们系统的安全计算机制。尽管具体的计算细节并不是讨论的重点,但如果我的新算法相较于旧逻辑产生了任何差异,这将可能给我们的客户带来严重的后果。为了验证新算法的正确性和稳健性,我的策略是同时运用新旧算法进行计算,对比两者的输出结果,正常情况下会返回旧算法的结果,并在发现差异时记录错误信息。我在这一过程中引入了两个功能标志:UseNewAlgorithmDark 负责启用新计算并进行比较与日志记录,而 UseNewAlgorithmOnly​ 则令系统停止使用旧算法,只采用新算法提供的结果。

实际工作中,代码的多个不同位置需要应用这一新的逻辑,并且它们执行的计算任务也有所不同。因此,在多个位置的代码中,我们可以看到类似以下的模式:

值得指出的是,并非所有接入这一功能特性的控制器函数都接收相同类型的参数。这里还有一个几乎一致的场景,但函数签名看起来更为复杂:

抽象的力量

为了减少代码的重复并增强其易用性,我希望将我所说的“暗启动模式”具体化,以方便我团队中的其他成员也能够方便地使用它。出于这个目的,我从下面这样的构造开始我的工作:

它的使用方式如下所示:

虽然这种方法可行,但它并不美观。其中一个原因是,每次调用这段逻辑的唯一方法就是在参数中新建一个闭包,而你不能简单地在某个地方定义一个DarkLaunch​函数并重复使用。你还需要传递相关的机制(如日志记录器和功能标志管理器)。最后,通用的Execute​方法无法访问NewWay​和OldWay​函数的参数,从而无法在发生错误时进行记录;我们可能不得不在闭包内部插入额外的日志记录来确保日志的准确。

面对Go语言的挑战,以及从其他语言中得到的启发

当面临的问题是,DarkLaunch​函数不能泛化处理NewWay​和OldWay​函数的参数时,在像Clojure这样更为动态的语言中,我们可以将一个函数应用于任意集合的参数,如下例:

而在拥有复杂类型系统的语言中,同样的想法可以表现得更为严格。我相信在Haskell中是可以实现类似功能的 [1],但我更熟悉TypeScript,因此我采用了[变参元组类型]这一特性:

TypeScript编译器将保证oldWay​与newWay​有相同数量和类型的参数,这正是我们所期望的。这为我们在Go中解决同样问题提供了线索。

Go语言的解决之道

尽管在Go语言中我们没有类似于变参元组类型 [2] 的特性,但我们依然可以表达一系列类型捆绑在一起的概念:也就是一个结构体。解决方案可能需要我们稍微重写oldWay​ / newWay​函数,使其参数以结构体形式呈现,这虽然需要一些改动,但并非难以克服的问题。

进一步的改善是我将结构体中的“辅助”成员(例如日志器与特性管理器,这些只是实际执行代码时会用到的配套设施)封装在一个Executor​接口之中。Controller​已经实现了这个Executor​接口,因此我们现在可以将整个过程进行打包,以方便在自己的代码库中使用。以下是经过简化后的代码结构:

通过这样的理顺,代码变得格外清晰与简洁。确实,Go的类型系统的表达能力还不足以支持抽象出能够作为函数参数、代表任意长度有限类型列表的类型参数;无论如何,我们已经通过使用一种简明的产品类型(结构体)成功地陈述了这个有限列表的概念。

掌握Golang精髓,释放编程潜能!关注我的《Golang实用技巧》专栏,它将为你揭秘生产环境最佳实践,带你探索高并发编程的实用教程。从分享实用的Golang小技巧到深入剖析实际应用场景,让你成为真正的Golang大师。无论你是初学者还是经验丰富的开发者,这里都有你所需要的灵感和知识。让我们一同探索Golang的无限可能!

0 阅读:5

超级欧派课程

简介:感谢大家的关注