拥抱Ruby中的函数式编程(2017)

2021-02-21 07:40:00

在古斯托(Gusto),我们一直在深思熟虑运行工资单的系统。

进行工资核算需要采取几种不同的输入方法,例如员工应该得到多少薪水,他们在哪里工作,他们工作了多少,他们应该缴纳多少税,今年他们缴纳了多少税等等。在。

作为一家提供薪资服务的公司,将系统的这一部分保持在顶尖状态对于企业来说很重要。客户喜欢Gusto,因为它在运行工资单方面既简单又快速。

多年来,该系统已超出其最初的授权范围。现在,它不仅为一个州提供薪资,还为所有50个州和哥伦比亚特区提供薪水。尽管客户喜欢我们的工资单,但是内部新工程师很难理解代码并安全地进行更改。该系统需要调整,因此我们着手进行大规模的重构。

因为计算薪水所需的过程是一个很大的公式,所以我们设定了使该系统像功能编程那样“更具功能性”的目标。我们希望采用计算工资单的过程,并使之成为一项大型无状态操作。

Gusto的服务器端代码是用Ruby编写的,该语言通常以面向对象和元编程的根源而闻名。尽管如此,我们还是希望将更多功能性概念集成到我们的代码中,以期提高系统的安全性和清晰度。结果是可维护的代码,使代码更容易推理,更安全。

Ruby是一种富有表现力的语言,但是它并不适合某些常见的功能实践。尽管Ruby允许通过Procs进行闭包和一流的功能,但是在惯用的Ruby中并没有看到许多Procs作为对象传递。

在我们的整个工作中,我们发现您可以通过同时拥抱Ruby的OO和功能方面来创建具有干净内部结构的表达接口。

为此,我们使用了一种称为“纯函数作为对象”(PFaaO)的模式。本质上,您可以像对待纯函数一样设计对象,但是将它们打扮成Ruby类。

纯函数是没有明显副作用的函数,对于给定的一组输入,该函数始终返回相同的值。这意味着不与数据库对话,不修改其他对象的状态,不访问系统时钟等。当我们用Ruby编写PFaaO时,我们希望构建一个没有副作用的对象。

PayrollCalculator def self类。计算(工资)新(工资)。计算结束def初始化(工资)@payroll =结束工资private_class_method:new def计算PayrollResult。新的(payroll:payroll,paystubs:paystubs,tax:tax,debits:debits)end def paystubs#... end def tax#... end def debits#... end end

这里有很多事情,所以让我们一点一点地分解。

首先,我们的类只有一个有效的公共接口:PayrollCalculator.calculate。由于我们已使用private_class_method:new将构造函数声明为私有,因此实例方法#calculate实际上是私有的。 1个

这意味着,即使此类中没有显式的私有块,我们声明的所有其他实例方法也是隐式私有的。由于无法更新实例,因此没有向量可以调用任何实例方法。

我们的方法只有一个公共接口,其设计的操作实际上是无状态的,因此我们只需要在测试中使用一个接口即可。放入一些数据,断言数据出来就是我们所期望的。

在上面的示例中,假设从时间角度来看,计算税收的过程非常昂贵。 2因此,我们希望在时间/空间上进行权衡以消耗更多的内存,以最大程度地减少计算税金的次数。在我们的示例中,同时计算#paystubs和#debits将需要#taxes的结果。

现在,由于每个私有方法都是纯函数,因此我们具有引用透明性。这意味着我们可以将方法及其参数替换为其返回值。像代数一样思考它:给定函数f(x)= x + 5,您可以安全地用值7替换任何出现的f(2)。

备忘是一种缓存形式,如果备忘的值实际上并非来自纯函数,则可能会遇到很多问题。但是,因为我们使PFaaO中的所有内容都是纯净的,所以我们可以安全地记住此方法调用。

这很有趣,因为看起来此类不再是无状态的:它现在分配局部值。但是,唯一的接口是单个.calculate类方法,我们的PFaaO的每个实例都是一次性的。任何中间状态都不能从外部访问。由于无法从外部观察此缓存状态,因此我们的功能在技术上仍然是纯净的。

开发人员可以通过许多抽象方式来抽象同步和异步行为,而您可以在功能纯净的情况下进行相同的操作。任何本地状态更改与PFaaO的生命周期无关。这些局部状态的变化无法从外部观察到。

随着我职业的发展,我对软件的编写方式和维护方式的兴趣已减弱。软件维护是任何成功项目的祝福和诅咒:恭喜!您的企业具有持久的价值。我们的哀悼!您现在必须为所有错误付费。但是,始终首选拥有技术债务的企业,而不是拥有原始代码库的破产公司。

Ruby中的PFaaO很棒,因为它们易于维护。它们不仅易于测试,而且易于健康成长。

让我们再次以#taxes方法为例。在Gusto的历史早期(当时仍称为ZenPayroll),我们仅在加利福尼亚提供薪资服务。因此,我们只需要担心加利福尼亚的工资税。

按照宏伟的计划,在工资税方面,加利福尼亚是一个简单的州。我们的税收方法可能看起来仅是以下内容:

现在说我们扩展到一个新的州,纽约。现在我们的方法有了一点增长:

随着我们扩展到每种状态,3这种方法将变得非常大!此外,这些方法中的每一个都增加了PayrollCalculator类的长度。没有持续的园艺,班级将变得难以理解。

但是由于PFaaO中的每个方法本身都是纯函数,因此我们能够按自己认为合适的方式提取类,并将每个类都设为新的PFaaO。我们可以用新的PFaaO安全地替换我们的增长方法:

当我们梳理这些不同的PFaaO时,我们也对这些服务类的输入要求有了更好的了解。我们的@payroll是一个大参数对象,每个提取的PFaaO可能只需要其数据的一个子集。

在这里,我们假设Payroll#only_pay_and_location_data作为新的Value Object返回实例中总数据的一部分。此值对象仅表示计算运行工资单的税款部分所需的数据。

可伸缩PFaaO的另一个重要因素是要求所有数据默认情况下都是不变的。与大多数人传统上编写Ruby的方式相比,这是一个巨大的改变。

每次达到=时,都需要用#set或#put替换它。您将习惯于返回具有新值的新副本,而不是在适当位置修改对象。 (仓鼠提供了很好的不可变数据结构,可以帮助您不必手动设置FP功能。)

这对Rails意味着什么?这通常意味着创建带有ActiveRecord对象的函数或类,并将它们转换为不可变的值对象。对于我们来说,我们将这些值对象切入正在执行的操作的名称空间中。例如,以下是我们系统中薪资的两种表示形式:

工资单的ActiveRecord版本表示存在于数据库中的数据。它是实际运行工资单所需数据的超集。尽管它们具有相同的名称,但它们不具有相同的属性。例如,ActiveRecord版本的薪资将具有处理的_at属性,而位于计算域中的薪资则没有。

用域驱动设计的话来说,这里的每个命名空间都是一个不同的绑定上下文。我们实现适配器以获取ActiveRecord工资单并将其转换为PayrollCalculator工资单,反之亦然。

它的好处与您在具有定义良好的抽象的任何其他大型系统中可能看到的一样。模型更改不会跨域。在我们的示例中,我们可以更改数据库中薪资的结构,而无需更改计算代码。我们只需要更改适配器。此外,此上下文与Rails的阴谋完全分开。我们可以轻松安全地将其放入其自己的gem或完全独立的服务中。

如果将ActiveRecord对象作为计算器的参数,则从ActiveRecord对象中添加或删除列可能会导致一系列级联,痛苦而危险的更改。

对于年轻的Rails应用而言,这种间接级别是过高的。随着应用程序的增长以及多个团队开始为同一个应用程序做贡献,像这样的绑定上下文是必要的。

我们一直在慢慢地将薪资计算器重构为该模型,并使用它来安全地每月处理10亿美元以上的费用。

结果非常了不起:添加或更改薪资代码现在更安全了。因为每个更改都更加孤立,所以开发人员只需要关心本地实现。

尽管这篇文章没有涵盖它,但是使用不可变数据测试PFaaO轻而易举。我们发现自己为每个方法和类执行的设置较少。我们的测试保持快速,因为它们没有访问数据库。

不过,并非所有的阳光和彩虹。这种方法确实会导致大量的代码。我的粗略估计将使代码量增加1.5倍至2倍。一些开发人员不喜欢由此产生的许多PFaaO的庞大性质。尽管代码总行数会增加,但是您应该对每个有界上下文的数据要求有更好的了解。换句话说,您不需要传递整个ActiveRecord对象,而只需传递它们的属性的一小束。

在完全接受之前,请与您的团队讨论以制定一些基本规则。我们通常每节课拍摄约100条线,但是您的团队可能会决定采取其他不同的方法。确保进入同一页面,并同意您的应用处于可以从这种思维方式中受益的大小。

对于某些团队而言,ActiveRecord和对数据执行有趣的操作之间多余的抽象层似乎过大了。在许多情况下,情况将会如此。同样,我鼓励您与团队进行健康的讨论,以确定这种方法的好处是否大于缺点。

对于我们来说,我们会在适当的地方使用它。试一下这个模式,让我知道它的进展!

特别感谢Justin Duke,Eddie Kim,BoSørensen,Matt Lewis和Julia Lee提供了有关此职位的初稿的反馈。

如果您有兴趣定期收到这样的博客文章,请加入数百名开发人员并订阅我的新闻通讯。

敏锐的作者会知道,Ruby中没有什么是真正私有的。总有#send。 ↩

在Gusto,计算税收非常昂贵!您知道美国境内有6,000多种工资税吗?根据工资单本身的不同参数,每个人可能不需要申请给定工资单。 ↩

如今,Gusto在包括华盛顿州在内的每个州都提供薪资服务,并且出错率是业内最低的。 ↩