到目前为止,我们已经讨论了许多纯函数的特性,这些特性使它们具有可组合性,并允许您为领域设计漂亮的抽象。如果事实如此简单,并且您为您的领域模型编写的所有函数都是如此的纯粹,那么整个软件开发行业就不会看到这么多失败的项目了。
考虑一个简单的函数,square,它将一个整数作为输入并输出平方值。但是与前一个不同,除了输出整数的平方之外,它还必须在文件系统上的特定文件中写入结果。游戏规则现在已经改变了。如果你在尝试向特定文件中写入输出时出现一个I/O异常,会发生什么情况?也许磁盘已经满了。或者在你试图创建输出文件的文件夹中没有写权限。
所有这些都是有效的关注点,您的代码需要处理所有这些异常,这意味着您的函数现在必须处理外部实体(例如文件系统),而不是它所接收到的显式输入的一部分。函数不再是纯的。它必须处理属于外部世界的外部影响,也就是所谓的side effects(副作用)。
问1.6:在本章中,您已经看到了副作用的例子。你能指出它在哪吗??
并不是说副作用是邪恶的;在设计一个重要的领域模型时,它们是您需要管理的基本组件之一。如果你仍然不相信,让我们考虑一个来自我们领域的例子并讨论你如何处理这方面的问题。打开Account(账户)的步骤之一是验证客户的身份,并进行必要的背景检查。在这个操作中,您必须与外部系统(你的银行模型之外)进行交互以获得背景检查的验证结果。接下来的列表使我们建模这一行为的第一次尝试。
openCheckingAccount所做的第一件事就是调用verifyRecord来验证客户的身份。这是一个与外部世界交互的调用,可能通过外部网关进行web service调用来进行验证。不过,这个调用可能会失败,您的代码需要处理与外部系统失败相关的异常。而这与给客户一个有效的、新打开的支票帐户(我们可以看方法名,openCheckingAccount,意思就是打开支票账户)的核心领域逻辑没有任何关系。
没有办法避免这种情况,而且当您探索领域模型实现的其他用例时候,将看到许多这样的情况。处理这个问题的最佳方法是尽可能地将副作用与纯领域逻辑分离。但是在研究如何解耦之前,让我们再次回顾一下在同一个函数中混合副作用(即不纯的函数)和纯域逻辑的缺点。表1.3列出了所有这些。
表1.3
混合副作用和领域逻辑 | 为什么这是坏味道? |
---|---|
领域逻辑和副作用牵连在一起 | 违反了关注点分离。领域逻辑和副作用相互正交(数学里正交的意思)---这种相互纠缠违反了基本的软件工程原则 |
困难的单元测试。 | 如果域逻辑与副作用纠缠在一起,那么单元测试就变得困难了。您需要使用mock,这会导致您的源代码中出现其他的复杂问题。 |
很难对域逻辑进行推理。 | 你不能推理出与副作用纠缠在一起的领域逻辑。 |
副作用不会构成和阻碍代码的模块化。 | 您的纠缠代码仍然是一个无法与其他函数进行组合的孤岛 |
您需要重构以避免这种代码纠缠,并且您需要确保副作用与领域逻辑是分离的。下一个示例将两个操作分割为单独的函数,并确保领域逻辑仍然是一个纯函数,您可以单独进行单元测试。这正是清单1.10所做的。它引入了一个通过与外部系统交互来进行验证的新函数。完成后,它将成功的Customer实例传递给openCheckingAccount函数,该函数执行打开帐户的纯逻辑。
管理副作用是一个可以在组合领域模型和非组合模型之间产生巨大差异的区域。一个不是组装式的模型,通常会受到粘合代码和baggage of boilerplates(实在找不到合适的意思)的影响。这就变成了一个维护噩梦---难以管理和扩展。但是,在您从纯逻辑中分离出副作用之后,您可以编写提供更好的组合性的代码。在我们的样例中,openCheckingAccount再一次成为纯逻辑。下面的清单展示了如何将verifyCustomer和openCheckingAccount结合在一起,并处理 在处理副作用的代码部分中可能出现的failures故障。
答1.6:在清单1.4中我们早已见到了副作用,即用于更新共享的可变状态的领域行为debit和credit。注意,副作用是依赖于外部系统的任何东西,无论是处理文件系统还是管理全局的可变状态。