scala - 当我使用 "Reader monad"进行依赖注入(inject)时如何注入(inject)多个依赖项?

标签 scala dependency-injection monads

我正在尝试使用 Reader monad用于依赖注入(inject),但是当方法需要不同的依赖时会出现问题:

class PageFetcher {
  def fetch(url: String) = Reader((dep1: Dep1) => Try {
    ...
  })
}

class ImageExtractor {
  def extractImages(html: String) = Reader((deps: (Dep2, Dep3)) => {
    ...
  })
}


object MyImageFinder {
  def find(url: String) = Reader((deps: (PageFetcher, ImageExtractor)) => {
    val (pageFetcher, imageExtractor) = deps
    for {
      htmlTry <- pageFetcher.fetch(url)
      html <- htmlTry
      images <- imageExtractor.extractImages(html)
    } yield images
  })
}

// I add these 3 useless dependencies here just for demo
class Dep1

class Dep2

class Dep3

你可以看到PageFetcher.fetchImageExtractor.extractImagesMyImageFinder.find都有不同的依赖关系。

我不确定我使用 Reader 的方式是否正确正确,很快当我将它们组合在一起并想要传递依赖项时,我不知道该怎么做:
val pageFetcher = new PageFetcher
val imageExtractor = new ImageExtractor
val dep1 = new Dep1
val dep2 = new Dep2
val dep3 = new Dep3

def main(args: Array[String]) {
  args.headOption match {
    case Some(url) =>
      MyImageFinder.find(url)(???) match {
        case Success(images) => images.foreach(println)
        case Failure(err) => println(err.toString)
      }
    case _ => println("Please input an url")
  }
}

注意代码 MyImageFinder.find(url)(???) ,我想传递像 pageFetcher/imageExtractor/dep1/dep2/dep3 这样的依赖项,无论我如何尝试,它都无法编译。

是我的使用方式Reader正确的?如何轻松传递依赖项?

最佳答案

更新:删除了自定义 flatMap 以支持 scalaz 的阅读器

正如 Travis 已经指出的,要使用 Reader 模式,您需要单参数函数。因此,为了将它用于多个依赖项,您需要以某种方式将所有依赖项放入一个参数中。在这里它变得有趣。 Travis 展示的方法是最简单的方法,但您还必须使用 .local 手动切换环境。调用,如果您需要计算子树的多个依赖项,则需要手动构建本地环境。

另一种方法是让 Scala 的子类型自动地解决它。只要您的依赖项可以混合,组合具有不同或多个依赖项的东西就可以工作(如果您实际使用 scalaz 的 Reader,而不是像某些 Reader 示例那样在 Function1 上使用 flatMap)。

选项1:杯子蛋糕图案

允许您的依赖项能够混合的一种方法是精简蛋糕模式。我会称它为纸杯蛋糕图案,如果我必须给它起个名字,Dick Wall 称它为 Parfait(见 https://parleys.com/play/53a7d2cde4b0543940d9e55f/chapter28/about)。这个想法不是将所有内容都放入蛋糕中,而是将依赖项放入蛋糕中并将其作为上下文对象传递,您可以使用阅读器对其进行抽象。让我们将其应用于您的示例:

// business logic
class PageFetcher {
  def fetch(url: String) = Reader((deps: Dep1Component) => Try {
    ...
  })
}

class ImageExtractor {
  def extractImages(html: String) = Reader((deps: (Dep2Component with Dep3Component)) => {
    ...
  })
}


object MyImageFinder {
  def find(url: String) = 
    for {
      pageFetcher <- Reader((deps: PageFetcherComponent) => dep.pageFetcher)
      imageExtractor <- Reader((deps: ImageExtractorComponent) => dep.imageExtractor)
      htmlTry <- pageFetcher.fetch(url)
      html <- htmlTry
      images <- imageExtractor.extractImages(html)
    } yield images
}

// I add these 3 useless dependencies here just for demo
class Dep1

class Dep2

class Dep3

// cupcake modules
trait PageFetcherComponent{
  def pageFetcher: PageFetcher
}
trait ImageExtractorComponent{
  def imageExtractor: ImageExtractor
}
trait Dep1Component{
  def dep1: Dep1
}
trait Dep2Component {
  def dep2: Dep2
}
trait Dep3Component{
  def dep3: Dep3
}

object Dependencies extends PageFetcherComponent with ImageExtractorComponent with Dep1Component with Dep2Component with Dep3Component{
  val pageFetcher = new PageFetcher
  val imageExtractor = new ImageExtractor
  val dep1 = new Dep1
  val dep2 = new Dep2
  val dep3 = new Dep3
}

def main(args: Array[String]) {
  args.headOption match {
    case Some(url) =>
      MyImageFinder.find(url)(Dependencies) match {
        case Success(images) => images.foreach(println)
        case Failure(err) => println(err.toString)
      }
    case _ => println("Please input an url")
  }
}

如果您有多个相同依赖项的实例(多个记录器、多个数据库等)并且有一些您希望能够有选择地在其中一个或另一个上使用的代码,那么纸杯蛋糕模式就会变得棘手。

选项 2:类型索引 map

我最近想出了另一种方法来使用我称为类型索引映射的特殊数据结构。它保存了所有纸杯蛋糕样板,并且使使用相同类型依赖项的多个实例变得更加容易(即,只需将它们包装在单个成员类中以区分它们)。
/** gets stuff out of a TMap */
def Implicit[V:TTKey] = Reader((c: TMap[V]) => c[V])

// business logic
class PageFetcher {
  def fetch(url: String) = Implicit[Dep1].map{ dep1 => Try {
    ...
  }}
}

class ImageExtractor {
  def extractImages(html: String) = for{
    dep2 <- Implicit[Dep1]
    dep3 <- Implicit[Dep3]
  } yield {
    ...
  }
}


object MyImageFinder {
  def find(url: String) = 
    for {
      pageFetcher <- Implicit[PageFetcherComponent]
      imageExtractor <- Implicit[ImageExtractorComponent]
      htmlTry <- pageFetcher.fetch(url)
      html <- htmlTry
      images <- imageExtractor.extractImages(html)
    } yield images
}

// I add these 3 useless dependencies here just for demo
class Dep1

class Dep2

class Dep3

val Dependencies =
  TMap(new PageFetcher) ++
  TMap(new ImageExtractor) ++
  TMap(new Dep1) ++
  TMap(new Dep2) ++
  TMap(new Dep3)

def main(args: Array[String]) {
  args.headOption match {
    case Some(url) =>
      MyImageFinder.find(url)(Dependencies) match {
        case Success(images) => images.foreach(println)
        case Failure(err) => println(err.toString)
      }
    case _ => println("Please input an url")
  }
}

我在这里发表 https://github.com/cvogt/slick-action/ .对应的测试用例在这里:https://github.com/cvogt/slick-action/blob/master/src/test/scala/org/cvogt/di/TMapTest.scala#L213它在 maven 上,但使用时要小心,因为代码在不断变化,并且当前实现在 2.10 中不是线程安全的,仅在 2.11 中,因为它依赖于 TypeTags。我可能会在某个时候发布适用于 2.10 和 2.11 的版本。

附录
虽然这解决了 reader monad 的多依赖注入(inject),但您仍然会收到 htmlTry 的类型错误,因为您将 Reader/Function1-composition 与 Try-composition 混合在一起。解决方案是创建一个包装 Monad,它在内部包装 Function1[TMap[...],Try[...]] 并允许组合它们。这确实需要您将所有内容都填充到这种类型的 monad 中,即使某些东西不需要 Try。

关于scala - 当我使用 "Reader monad"进行依赖注入(inject)时如何注入(inject)多个依赖项?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/25947783/

相关文章:

java - 带有 setter 注入(inject)的 Spring @Qualifier 奇怪行为

dependency-injection - 在Windsor中注册组件不指定接口(interface)可以吗?

c# - 使用简单注入(inject)器在运行时注入(inject)数据解析模型

用于过滤任一输入列表的 Scala 多态函数

haskell - 非确定性如何用 List monad 建模?

scala - "value class space is flat"是什么意思?

scala - 使用spark scala远程连接hbase

mongodb - Mongo-Scala-Driver:CodecConfigurationException:找不到类 immutable.Document 的编解码器

scala - 为什么 from_json 失败并显示 "not found : value from_json"?