副作用是编程头号敌人!如何剥离它?- spin


随着时间的推移,我注意到一种设计启发式方法,它极大地帮助了我完成无数项目。
这种启发式的地方在于它在概念上易于理解和应用,但它自然会引导您更接近函数式编程。
事实上,这与 Haskell 处理 IO 的方式非常相似。它也是 React 等现代 Web/UI 框架的核心理念。
 
为了说明这一点,让我们看一个例子:
你正在处理一个涉及安排未来用户通知的事情:有一些输入需要考虑,比如用户喜欢的时间和时间的推移;你已经决定提前一周安排它们;你需要定期安排新的通知,但你也可能需要删除或重新安排现有的通知。
 

对于未来一周的每一天foreach
  是否没有安排通知?if
    现在就生成并安排(副作用!)。
  否则,应该重新安排吗?else
    删除现有的通知(副作用!)。
    生成并安排新的通知(副作用!)。

 
在这个ifelse例子中,我们在进行处理的同时也在修改世界。
副作用本质上并不坏:事实上,它们是编写软件的全部意义所在。如果你的代码没有效果,就不会有人使用它。

不过,还是要问问自己,你怎么能把这些副作用保存到最后?
归根结底,办法就是建立一些数据,描述需要采取的任何行动。

对于未来一周的每一天foreach
  是否没有安排通知?if
    在输出中添加一些数据,描述一个新的通知
  否则,是否应该重新安排?else
    在输出中添加一些数据描述如何删除该通知
    并添加更多关于如何安排其替换的数据


这段伪代码输出:

[{action: "remove", notificationId: 4},
 {action:
"create", time: "some ISO8601 time"}]

然后你会把这些数据交给另一个更简单的函数,由这个更简单的函数来执行这些副作用。
 
诚然,这种启发式方法将导致你做一些额外的工作,但也有很多好处。其中。

首先,它创造了一个自然边界,对测试非常有用。上面的伪代码中的逻辑(如 "否则,是否应该重新安排?")实际上可能相当复杂。

在系统中,决定实施什么变化的逻辑往往比实施上述变化的逻辑要复杂得多。通过划定这个边界,你可以获得编写更少的艰巨的测试的好处。要模拟的东西更少,而且很容易为数据写断言。

第二,调试变得更加容易。能够问一个系统要做什么,并在它开始执行之前收到其计划的完整快照,这真的很神奇。这对于更复杂的功能来说尤其如此,例如与外部系统的同步。

最后,这种方法使你能够轻松地进行分层转换。使用上面的例子,假设你的通知调度器应该避免某些假期。你可以写一个简单的函数,接收一个调度行为的列表,并过滤掉任何发生在特定日期的行为。