11

我是 Scala 和 Play 的新手;我编写了一个包含业务和表示逻辑的“全部”控制器。我想从控制器中重构业务逻辑。

这是我的 Scala/Play 的样子。用干净的界面重构出这个控制器的业务逻辑的好/惯用的方法是什么?

object NodeRender extends Controller {
...
def deleteNode(nodeId: Long) = Action { request =>
    //business logic
    val commitDocument = Json.toJson(
    Map(
        "delete" -> Seq( Map( "id" -> toJson( nodeId)))  
    ))
    val commitSend   = Json.stringify( commitDocument)
    val commitParams = Map( "commit" -> "true", "wt" -> "json")
    val headers = Map( "Content-type" -> "application/json")

    val sol = host( "127.0.0.1", 8080)
    val updateReq  = sol / "solr-store" / "collection1" / "update" / "json" <<?
        commitParams <:< headers << commitSend

    val commitResponse = Http( updateReq)()

    //presentation logic
    Redirect( routes.NodeRender.listNodes)
}

在 Python/Django 中,我编写了两个类XApiHandlerXBackend并在它们之间使用了一个干净的接口。

xb = XBackend( user).do_stuff()
if not xb:
  return a_404_error
else:
  return the_right_stuff( xb.content) #please dont assume its a view!
4

3 回答 3

7

几个假设:

1)你的倒数第二行块上的 HTTP 调用

2)您没有说重定向是否需要等待来自 Http 调用的响应,但我认为确实如此。

阻塞调用应该移到另一个线程,这样你就不会阻塞处理请求的线程。播放文档对此非常具体。该Akka.future功能与Async帮助相结合。

控制器代码:

1 def deleteNode(nodeId: Long) = Action { request =>
2     Async{
3         val response = Akka.future( BusinessService.businessLogic(nodeId) )
4 
5         response.map { result =>
6             result map {
7                 Redirect( routes.NodeRender.listNodes)
8             } recover {
9                 InternalServerError("Failed due to ...")
10            } get 
11        }
12    }
13}

这比你的 PHP 多一点,但它是多线程的。

传递到Akka.future第 3 行的代码将在未来某个时间使用不同的线程调用。但是调用会Akka.future立即返回 a Future[Try](业务方法的返回类型见下文)。这意味着变量response具有类型Future[Try]。第 5 行对map方法的调用不会调用 map 块内的代码,而是将该代码(第 6-10 行)注册为回调。该线程不会在第 5 行阻塞并返回Future到该Async块。该AsyncAsyncResult向 Play 返回 a ,并告诉 Play 在未来完成时为回调注册自己。

同时,其他一些线程将调用BusinessService第 3 行,一旦您对后端系统进行的 HTTP 调用返回,第response3 行的变量“已完成”,这意味着第 6-10 行的回调被调用。 result具有抽象类型Try,并且只有两个子类:SuccessFailure. 如果result成功,则该map方法调用第 7 行并将其包装在一个新的Success. 如果result是失败,则 map 方法返回失败。第recover8 行的方法正好相反。如果 map 方法的结果是成功,则返回成功,否则调用第 9 行并将其包装在 a Success(不是 a Failure!)中。对get第 10 行的方法将重定向或错误排除在外,Success并且该值用于完成AsyncResultPlay 所保留的。Play 然后会收到一个回调,表明响应已准备好并且可以呈现和发送。

使用此解决方案,服务传入请求的线程不会被阻塞。这很重要,因为例如在 4 核机器上,Play 只有 8 个线程能够处理传入请求。它不会产生任何新的,至少在使用默认配置时不会。

这是来自业务服务对象的代码(几乎复制了您的代码):

def businessLogic(nodeId: Long): Future[Try] {

    val commitDocument = Json.toJson(
    Map(
       "delete" -> Seq( Map( "id" -> toJson( nodeId)))  
    ))
    val commitSend   = Json.stringify( commitDocument)
    val commitParams = Map( "commit" -> "true", "wt" -> "json")
    val headers = Map( "Content-type" -> "application/json")

    val sol = host( "127.0.0.1", 8080)
    val updateReq  = sol / "solr-store" / "collection1" / "update" / "json" <<?
        commitParams <:< headers << commitSend

    val commitResponse = Http( updateReq)()

    Success(commitResponse) //return the response or null, doesnt really matter so long as its wrapped in a successful Try 
}

表示逻辑和业务逻辑现在完全解耦了。

有关更多信息,请参阅https://speakerdeck.com/heathermiller/futures-and-promises-in-scala-2-dot-10http://docs.scala-lang.org/overviews/core/futures.html

于 2013-03-09T21:25:39.327 回答
4

我可能会这样做

object NodeRenderer extends Controller {

  def listNodes = Action { request =>
    Ok("list")
  }

  def deleteNode(nodeId: Long)(
    implicit nodeService: NodeService = NodeService) = Action { request =>

    Async {
      Future {
        val response = nodeService.deleteNode(nodeId)

        response.apply.fold(
          error => BadRequest(error.message),
          success => Redirect(routes.NodeRenderer.listNodes))
      }
    }
  }
}

节点服务文件看起来像这样

trait NodeService {
  def deleteNode(nodeId: Long): Promise[Either[Error, Success]]
}

object NodeService extends NodeService {

  val deleteDocument =
    (__ \ "delete").write(
      Writes.seq(
        (__ \ "id").write[Long]))

  val commitParams = Map("commit" -> "true", "wt" -> "json")
  val headers = Map("Content-type" -> "application/json")

  def sol = host("127.0.0.1", 8080)
  def baseReq = sol / "solr-store" / "collection1" / "update" / "json" <<?
    commitParams <:< headers

  def deleteNode(nodeId: Long): Promise[Either[Error, Success]] = {

    //business logic
    val commitDocument =
      deleteDocument
        .writes(Seq(nodeId))
        .toString

    val updateReq = baseReq << commitDocument

    Http(updateReq).either.map(
      _.left.map(e => Error(e.getMessage))
        .right.map(r => Success))
  }
}

我在哪里定义ErrorSuccess喜欢这个

case class Error(message: String)
trait Success
case object Success extends Success

这将您的 http 部分和业务逻辑分开,允许您为同一服务创建其他类型的前端。同时,它允许您测试您的 http 处理,同时提供NodeService.

如果您需要将不同类型的NodeService绑定到同一个控制器,您可以将其转换NodeRenderer为类并使用构造函数将其传递。这个例子向你展示了如何做到这一点。

于 2013-03-07T22:32:24.103 回答
1

我不是专家,但我很高兴将连贯的逻辑块分解为混合特征。

abstract class CommonBase {
    def deleteNode(): Unit
}


trait Logic extends CommonBase{
  this: NodeRender =>

  override def deleteNode(): Unit = {
    println("Logic Here")
    println(CoolString)
    }
}

class NodeRender extends CommonBase
    with Logic
{
    val CoolString = "Hello World"

}



object test {
    def main(args: Array[String]) {
      println("starting ...")
      (new NodeRender()).deleteNode()
    }
}

印刷

starting ...
Logic Here
Hello World
于 2013-03-07T17:10:02.657 回答