读者进行环境注入

好吧,想象一下那些为孩子们准备的贴纸书 ,其中有一些孩子可以放在不同背景下的贴纸。 例如,他可以拿牛贴纸,并贴在田野中,山峦和河流在流淌。 或者,他可以将牛放在实验实验室,空间或马戏团中……这本书提供了许多不同的景观供您选择。

结果是一个场景,其中母牛在田野,马戏团或太空中嚼嚼嚼的东西,尽管那里可能没有很多草可以咀嚼……

孩子首先从书中的贴纸页面上拿走贴纸,然后决定将其放置在哪里(大多数情况下遵循完全随机的逻辑),最后将其放置在特定的环境中

然后他可以再贴上一张贴纸,一头母牛在做别的事情(跳跃,睡觉等等),或者另一头人物在做什么。 你明白了。

注入上下文

好吧,回到成年人的世界,那里不再有贴纸玩(除非有孩子)。 假设您正在构建一个应用程序,并且在某个时候您从存储库中获取了一些数据。 它可以来自任何来源,网络或本地,都没有关系。

假设您正在构建社交应用程序,并且需要为特定的电子邮件吸引用户。 您将拥有一个带有以下签名的方法:

  func getUser(email:String)->用户 

在此示例中,按照我们的不干胶标签书类似, 不干胶标签是电子邮件, 结果User对象。 但是实际情况在哪里?

景观是发生所有这些事情的背景,在贴纸书中是马戏团或田野。 在我们的小示例中,它可能是我们正在访问的特定存储库以获取数据: Web服务 ,成熟的持久层或测试模拟(其中可能没有任何东西可嚼)。

对于我们的应用程序,我们称其为服务

那么,我们如何让上一个函数知道要使用哪个服务,即应该从哪里获取数据呢? 换句话说,我们如何注入服务? 直接的解决方案可能只是将另一个参数传递给函数。

  func getUser(email:String,service:Service)->用户{ 
返回service.findUser(withEmail:email)
}

现在,该服务访问存储库,以返回用户以传递我们传递的电子邮件。 目前,我们正在忽略各种错误。 这非常好,因为现在出于测试目的,我们可以用其他符合相同协议的东西来代替服务。

尽管这种方法没有错,但是这意味着现在这种类型的每个函数都必须包含服务参数。 向服务参数添加默认值可能看起来更简单,您可以再次使用以前的函数签名,该服务将成为默认服务。 但是,随后您使对服务的依赖关系变得隐含了 ,API用户很难看到它。

还有另一个问题,可能很难看到。 使用这种语法,函数的调用者将必须传递userId和服务,换句话说,在进行调用时必须知道该信息。 乍看之下,两条信息是完全不相关的:用户的ID和获取该用户的服务。 对于此函数的调用者,我们正在混合两种完全不同的职责 :业务逻辑和持久性或网络代码。

替代。 读者

您是一名优秀的程序员,因此您希望将职责分开。 您相信SOLID中的“ S”,因此一类必须只有一个改变的理由 。 我们需要改进该代码。

如果还有另一种方式怎么办? 如果我们可以从函数中删除service参数,并获得允许部分应用的结果值,该怎么办? 在某种程度上,我们首先获取贴纸,然后,只要我们在贴纸书的适当页面中并且准备就绪,就将其放置在选定的景观中。

我们要做的是让函数返回某种结构,该结构以后可以接受环境来计算操作的实际结果。 那是一个阅读器 ,新的函数签名将是:

  func getUser(email:String)-> Reader  

Reader具有两种通用类型:环境和我们想要获得的实际结果。 而且,由于Reader只是彼此之间的一个功能 ,因此将实现如下方式:

如您所见,Reader只是一个结构,其中包含从一种泛型类型(环境)到另一种泛型类型(结果)的功能。 我们只是添加了一个方法运行来实际运行该函数

一切准备就绪,用法非常简单

使用这种方法,我们不会直接从服务返回结果,而是将函数的ReaderService返回给User 。 因此,现在getUser给我们提供了一个函数(由Reader封装), 我们可以通过将Service传递给它来随时运行

为了在本示例中使用读者,我们可以创建一个别名以简化语法。 我们可以将Reader服务到任何事物都视为我们要在Service中运行的动作。

再一步:单子

一切都很好,但是为了传递依赖关系,我可能看起来有些矫kill过正。 到现在为止,一切都可以通过使用咖喱函数来实现。 但是有了Reader,我们可以走得更远。 Reader实际上是Monad ,因此可以实现mapflatMap

您可以看一下实现,但是您所需要知道的是它们的工作原理与Swift中OptionalArray (也是monad)的版本非常相似。

附带说明一下: Reader Monad之所以这样称呼,是因为它是从共享环境中“读取”的,有时也称为Environmental Monad ,对我来说,它是一个更清晰的名称,尽管不那么酷,也不太常见。

地图的用法很简单

平面图将使我们可以轻松地链接读者。 例如,要获取与具有特定电子邮件的用户成为好友的User对象。

现在,让我们尝试更多的工作,并通过特定的电子邮件了解用户的所有朋友的年龄。 使用flatMap应该很容易,对吧?

那里发生了什么事? 简单:我们从getUserFriends获取了一系列用户,而getUserAge需要一个User 。 我们可以做什么? 我们可以手动编写flatMap闭包,然后返回一个读取器,该读取器在用户数组上映射功能以询问年龄。

这是可行的,但现在它使我们的代码看起来很丑陋,因为我们的目标是仅在一个flatMap行中就我们的新动作迈出第一步。 我们可以概括flatMap所做的工作,并将实现提取到函数中。 如果您考虑一下,我们实际上所做的只是将功能从User转换为Reader ,将功能从[User]转换Reader 。 我们可以编写一个纯函数来执行此操作。

结论

这有点长了,所以我们将其留在这里。 但是基本思想是,读者(成为Monad)可以很容易地扩展以与其他读者组成。 如果我们要使用带有咖喱函数的类似解决方案或其他方法来代替读者,那将不是一件容易的事。

接下来,我将讨论使所有这些异步操作以及在过程中将发现的特定挑战,我们将在Monad Transformers的帮助下解决这些挑战。