有没有办法 "trickle down" 从顶级应用程序隐含到其他导入的模块?

Is there a way to "trickle down" implicits from top level applications to other imported modules?

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

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

这是一个一般性问题,因为我倾向于 运行 对这个问题进行合理的探讨。

我有两个目标:

  1. 尽量减少我创建的 ActorSystems 的数量以简化对它们的跟踪(目标是每个顶级应用程序一个)
  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 的模块之间添加了更多的中间库)?

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

在您的示例中,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 技术堆栈的现有代码库,因此在这种特殊情况下这不太可能成为选择的方法。