可以将路由完成DSL卸载到akka-http中的actor吗?

时间:2017-04-02 20:19:20

标签: scala akka akka-http

我对akka-http实现感兴趣,但有一件事让我觉得它是一种反模式,就是为所有路由创建DSL,解析参数,错误处理等等。文件中给出的例子极其微不足道。然而,我在市场上看到了一个真实产品的路线,它是一个巨大的10k线文件,其中包含许多级别的深层次和路线中的大量业务逻辑。真实世界系统可以处理传递错误参数的用户,没有正确的权限等等,因此简单的DSL在现实生活中快速爆炸。对我来说,最佳解决方案是将路线完成交给演员,每个演员都有相同的api,然后他们将完成路线所需的工作。这将分散逻辑并启用可维护的代码,但在几小时后我无法管理它。使用低级API,我可以传递HttpRequest并以旧方式处理它,但这使我无法使用DSL中的大多数工具。那么有没有一种方法可以将一些东西传递给一个演员,使其能够在那时继续使用DSL,处理特定于路线的东西?即我在谈论这样的事情:

age = int(input())

当然,甚至不会编译。尽管我付出了最大的努力,但我还没有找到ContextOfSomeKind的类型,或者在我进入actor之后如何重新进入DSL。这可能是不可能的。如果不是我不认为我喜欢DSL,因为它鼓励我会考虑可怕的编程方法。然后,低级API的唯一问题是获得对实体marshallers的访问,但我宁愿这样做,然后在单个源文件中创建一个大型应用程序。

2 个答案:

答案 0 :(得分:2)

卸载路线完成

直接回答你的问题:Route只不过是一个功能。 The definition being

type Route = (RequestContext) => Future[RouteResult]

因此,您可以简单地编写一个能够满足您需求的功能,例如:将RequestContext发送给Actor并获取结果:

class MySlashHandler extends Actor {

  val routeHandler = (_ : RequestContext) => complete("Actor Complete")

  override def receive : Receive = {
    case requestContext : RequestContext => routeHandler(requestContext) pipeTo sender
  }
}

val actorRef : ActorRef = actorSystem actorOf (Props[MySlashHandler])

val route : Route = 
  (requestContext : RequestContext) => (actorRef ? requestContext).mapTo[RouteResult]

演员不要解决您的问题

您尝试处理的问题是现实世界的复杂性以及对代码复杂性的建模。我同意这是一个问题,但Actor不是您的解决方案。对于您寻求的设计解决方案,有几个原因可以避免Actors:

  1. compelling arguments against putting business logic in Actors
  2. Akka-http在引擎盖下使用akka-stream。 Akka流使用引擎盖下的演员。因此,您试图通过使用Actors来逃避基于可组合Actors的DSL。水通常不是溺水者的解决方案......
  3. akka-http DSL提供了大量的编译时间检查,一旦你恢复到Actor的非类型receive方法就会被冲走。您将获得更多运行时错误,例如死信,使用演员。
  4. 路线组织

    如前所述:Route是一个简单的函数,是scala的构建块。仅仅因为你看到一个草率开发者的例子,将所有逻辑保存在1个文件中并不意味着这是唯一的解决方案。

    我可以在main方法中编写所有应用程序代码,但这并不能使其成为良好的函数式编程设计。同样,我可以在一个for循环中编写所有的集合逻辑,但我通常使用filter / map / reduce。

    相同的组织原则适用于路线。从最基本的角度来看,您可以根据方法类型分解Route逻辑:

    //GetLogic.scala
    object GetLogic {
      val getRoute = get {
        complete("get received")
      }  
    }
    
    //PutLogic.scala
    object PutLogic {
      val putRoute = put {
        complete("put received")
      }
    }
    

    另一个常见的组织原则是将您的业务逻辑与您的Route逻辑分开:

    object BusinessLogic {
    
      type UserName = String
      type UserId = String     
    
      //isolated business logic 
      val dbLookup(userId : UserId) : UserName = ???
    
      val businessRoute = get {
        entity(as[String]) { userId => complete(dbLookup(userId)) } 
      }
    }
    

    然后可以将这些全部合并到您的主要方法中:

    val finalRoute : Route = 
      GetLogic.getRoute ~ PutLogic.putRoute ~ BusinessLogic.businessRoute
    

    路由DSL可能会产生误导,因为它有时看起来像魔术,但在它下面只是scala可以组织和隔离的普通功能...

答案 1 :(得分:1)

上周我也遇到过这样的问题。最终我最终在this blog并决定采用与那里描述相同的方式。

我创建了一个自定义指令,使我可以将请求上下文传递给Actors。

   def imperativelyComplete(inner: ImperativeRequestContext => Unit): Route = { ctx: RequestContext =>
       val p = Promise[RouteResult]()
       inner(new ImperativeRequestContext(ctx, p))
       p.future
   }

现在我可以在我的Routes文件中使用它,如下所示:

val route = 
    put {
        imperativelyComplete { ctx =>
            val actor = actorSystem.actorOf(Props(classOf[RequestHandler], ctx))
            actor ! HandleRequest
        }
    }

我的RequestHandler Actor如下所示:

class RequestHandler(ctx: ImperativeRequestContext) extends Actor {
    def receive: Receive = {
        case handleRequest: HandleRequest =>
            someActor ! DoSomething() // will return SomethingDone to the sender
        case somethingDone: SomethingDone =>
            ctx.complete("Done handling request")
            context.stop(self)
    }
}

我希望这能让您找到更好的解决方案。我不确定这个解决方案是否应该成为可行的方法,但到目前为止它对我来说非常好。