问题 从控制器重构业务逻辑的良好,惯用的方法


我是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中,我写了两个类 XApiHandler 和 XBackend 并在它们之间使用干净的界面。

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!

3985
2018-03-05 18:02


起源



答案:


一些假设:

1)最后一行的HTTP调用阻塞

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

阻止调用应该移动到另一个线程,这样你就不会阻塞处理请求的线程。 Play文档对此非常具体。该 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 立即返回一个 Future[Try] (请参阅下面的业务方法的返回类型)。这意味着变量 response 有类型 Future[Try]。打电话给 map 第5行的方法不调用map块中的代码,而是将该代码(第6-10行)注册为回调。线程不会在第5行阻塞并返回 Future 到了 Async 块。该 Async 块返回一个 AsyncResult 播放并告诉Play在未来完成时注册自己的回调。

同时,其他一些线程会调用 BusinessService 从第3行开始,一旦你对后端系统发出的HTTP调用返回, response 第3行的变量是“已完成”,意味着第6-10行的回调被调用。 result 有类型 Try 这是抽象的,只有两个子类: Success 和 Failure。如果 result 是成功,然后是 map 方法调用第7行并将其包装成新的 Success。如果 result 是失败,然后map方法返回失败。该 recover 第8行的方法则相反。如果map方法的结果是成功的,那么它返回成功,否则它调用第9行并将其包装在a中 Success (不是 Failure!)。打电话给 get 第10行的方法将重定向或错误取出 Success 并且该值用于完成 AsyncResult Play正在坚持下去。播放然后获得回复,响应已准备好并可以呈现和发送。

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

以下是Business Service对象的代码(几乎复制了您的代码):

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-10 和 http://docs.scala-lang.org/overviews/core/futures.html 了解更多信息。


7
2018-03-09 21:25



你会如何测试? deleteNode 行动? - EECOLOR
好问题!我猜“BusinessService”不应该是一个对象,然后它可以被模拟,你可以测试积极和消极的结果。看到 playframework.com/documentation/2.1.0/ScalaTest 更多细节。或者你的意思是不同的部分在不同的线程中运行? - Ant Kutschera
此外,Akka.future依赖于Play应用程序的一个实例,可以像这样对单元测试进行存根:隐式val application = Application(新文件(“。”),this.getClass.getClassloader,None,Play.Mode。开发) - Ant Kutschera
你会如何将一个模拟实例注入到 deleteNode 行动? - EECOLOR
FakeApplication 用来嘲笑它有点容易 Application - EECOLOR


答案:


一些假设:

1)最后一行的HTTP调用阻塞

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

阻止调用应该移动到另一个线程,这样你就不会阻塞处理请求的线程。 Play文档对此非常具体。该 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 立即返回一个 Future[Try] (请参阅下面的业务方法的返回类型)。这意味着变量 response 有类型 Future[Try]。打电话给 map 第5行的方法不调用map块中的代码,而是将该代码(第6-10行)注册为回调。线程不会在第5行阻塞并返回 Future 到了 Async 块。该 Async 块返回一个 AsyncResult 播放并告诉Play在未来完成时注册自己的回调。

同时,其他一些线程会调用 BusinessService 从第3行开始,一旦你对后端系统发出的HTTP调用返回, response 第3行的变量是“已完成”,意味着第6-10行的回调被调用。 result 有类型 Try 这是抽象的,只有两个子类: Success 和 Failure。如果 result 是成功,然后是 map 方法调用第7行并将其包装成新的 Success。如果 result 是失败,然后map方法返回失败。该 recover 第8行的方法则相反。如果map方法的结果是成功的,那么它返回成功,否则它调用第9行并将其包装在a中 Success (不是 Failure!)。打电话给 get 第10行的方法将重定向或错误取出 Success 并且该值用于完成 AsyncResult Play正在坚持下去。播放然后获得回复,响应已准备好并可以呈现和发送。

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

以下是Business Service对象的代码(几乎复制了您的代码):

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-10 和 http://docs.scala-lang.org/overviews/core/futures.html 了解更多信息。


7
2018-03-09 21:25



你会如何测试? deleteNode 行动? - EECOLOR
好问题!我猜“BusinessService”不应该是一个对象,然后它可以被模拟,你可以测试积极和消极的结果。看到 playframework.com/documentation/2.1.0/ScalaTest 更多细节。或者你的意思是不同的部分在不同的线程中运行? - Ant Kutschera
此外,Akka.future依赖于Play应用程序的一个实例,可以像这样对单元测试进行存根:隐式val application = Application(新文件(“。”),this.getClass.getClassloader,None,Play.Mode。开发) - Ant Kutschera
你会如何将一个模拟实例注入到 deleteNode 行动? - EECOLOR
FakeApplication 用来嘲笑它有点容易 Application - EECOLOR


我可能会这样做

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))
  }
}

我定义的地方 Error 和 Success 喜欢这个

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

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

如果你需要有不同类型的 NodeService 绑定到您可能转换的同一个控制器 NodeRenderer 到一个类并使用构造函数传递它。 这个例子 告诉你如何做到这一点。


4
2018-03-07 22:32



我只添加了一些东西以使其有用。我将静态部分移动到服务中,以便可以通过其他方法重用它们。我添加了一些额外的代码,为他的实现提供了更多选项。我也习惯于在更多的线条上传播东西以使事物更具可读性。 - EECOLOR


我不是专家,但我很高兴将相干逻辑块分解为混合特性。

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

1
2018-03-07 17:10