【问题标题】:Is there a way to "trickle down" implicits from top level applications to other imported modules?有没有办法从顶级应用程序“涓涓细流”隐含到其他导入的模块?
【发布时间】:2020-11-19 14:25:58
【问题描述】:

我正在尝试为使用 ActorSystem 作为 Http 调用的主干的程序重构一些代码。

我的具体目标是让我的代码更加模块化,这样我就可以编写函数库,这些函数库使用 ActorSystem 进行 http 调用,而 ActorSystem 稍后将由应用程序提供。

这是一个普遍的问题,因为我倾向于在合理的范围内遇到这个问题。

我有两个目标:

  1. 尽量减少我创建的 ActorSystem 的数量以简化对它们的跟踪(目标是每个顶级应用程序一个)
  2. 避免在需要的任何地方显式传递 ActorSystem 和上下文。

从概念上讲 - 下面的代码说明了我的想法(当然,这段代码不会编译)。

import akka.actor.ActorSystem
import intermediateModule._

import scala.concurrent.ExecutionContextExecutor

object MyApp extends App {
  // Create the actorsystem and place into scope
  implicit val system = ActorSystem()
  implicit val context = system.dispatcher

  intermediateFunc1(300)

}

// Elsewhere in the intermediate module
object intermediateModule {
  import expectsActorSystemModule._

  def intermediateFunc1(x: Int) = {

    // Relies on ActorSystem and Execution context, 
    // but won't compile because, of course the application ActorSystem and
    // ec is not in scope 
    usesActorSystem(x)
  }

}


// In this modiule, usesActorSystem needs an ActorSystem
object expectsActorSystemModule {

  def usesActorSystem(x: Int)
                     (implicit system: ActorSystem, context: ExecutionContextExecutor) = ???
  //... does some stuff like sending http requests with ActorSystem
}

有没有办法通过子模块“涓涓细流”隐式,以实现顶级应用程序提供所需隐式的目标?

这是否可以通过模块导入的“深度”无关紧要的方式来完成(例如,如果我在顶级应用程序和需要 ActorSystem 的模块之间添加了一些中间库)?

【问题讨论】:

  • 我建议只将隐式作为函数参数传递到层次结构中。这种方法在我工作的生产系统中效果很好
  • 谢谢@YikSanChan 我尝试采用这种方法并且我已经让它工作了,尽管我遇到了一个条件,如果我将隐式值作为隐式放入,例如implicit val myImplicit=... 我收到一个编译错误,因为隐含含糊不清。如果我在顶级范围内直接使用val myImplicit,一切正常 - 我需要通过一些案例来了解导致歧义的原因
  • 请阅读docs.scala-lang.org/tutorials/FAQ/finding-implicits.html 了解可以导入哪些隐式。一旦你发现额外的隐含,你可以删除它。另外,如果使用intellij,可以看到implicits,有助于理解流程:jetbrains.com/help/idea/edit-scala-code.html#scala_hints

标签: scala akka implicit


【解决方案1】:

这里的答案是依赖注入。每个依赖于其他对象的对象都应该将它们作为构造函数参数接收。这里重要的是,高层只得到它们自己的依赖,而不是它们依赖的依赖。

在您的示例中,IntermediateModule 不使用 ActorSystem 本身;它只需要将它传递给ExpectsActorSystemModule。这很糟糕,因为如果后者发生变化并需要另一个依赖项,那么您也需要更改前者——这太耦合了。你可以像这样重构它:

import akka.actor.ActorSystem
import scala.concurrent.ExecutionContextExecutor

object MyApp extends App {
  // Create the actorsystem and place into scope
  // wire everything together  
  implicit val system = ActorSystem()
  implicit val context = system.dispatcher
  val expectsActorSystemModule = new ExpectsActorSystemModule
  val intermediateModule = new IntermediateModule(expectsActorSystemModule)

  // run stuff
  intermediateModule.intermediateFunc1(300)

}

// Elsewhere in the intermediate module
class IntermediateModule(expectsActorSystemModule: ExpectsActorSystemModule) {
  def intermediateFunc1(x: Int) = {
    // Note: no ActorSystem or ExecutionContext is needed, because they were
    // injected into expectsActorSystemModule
    expectsActorSystemModule.usesActorSystem(x)
  }
}


// In this module, usesActorSystem needs an ActorSystem
class ExpectsActorSystemModule(
  implicit system: ActorSystem,
  context: ExecutionContextExecutor) {

  def usesActorSystem(x: Int) = ???
  //... does some stuff like sending http requests with ActorSystem
}

请注意,IntermediateModule 不再需要ActorSystemExecutionContext,因为它们直接提供给ExpectsActorSystemModule

有点烦人的部分是,有时您必须在应用程序中实例化所有这些对象并将它们连接在一起。在上面的示例中,MyApp 中只有 4 行代码,但对于更实质性的程序,它会变得更长。

有像 MacWire 或 Guice 这样的库可以帮助解决这个问题,但我建议不要使用它们。它们使正在发生的事情变得不那么透明,而且它们也没有保存那么多代码——在我看来,这是一个糟糕的权衡。而这两个具体有更多的缺点。 Guice 来自 Java 世界,基本上没有为您提供编译时保证,这意味着您的代码可能编译得很好,然后因为 Guice 而无法启动。 MacWire 在这方面更好(一切都在编译时完成),但它不是面向未来的,因为它是作为 Scala 2 宏实现的——它不适用于当前形式的 Scala 3。

在纯函数式编程社区中流行的另一种方法是使用 ZIO 的ZLayer。但由于您正在处理基于 Lightbend 技术堆栈的现有代码库,因此在这种特殊情况下,这不太可能是选择的方法。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2020-06-13
    • 1970-01-01
    • 1970-01-01
    • 2019-01-30
    • 2014-07-17
    • 1970-01-01
    • 1970-01-01
    • 2020-12-08
    相关资源
    最近更新 更多