Scala 从控制器重构业务逻辑的好方法

Scala 从控制器重构业务逻辑的好方法,scala,playframework,playframework-2.1,Scala,Playframework,Playframework 2.1,我不熟悉Scala和游戏;我还编写了一个“DoAll”控制器,它包含业务逻辑和表示逻辑。我想从控制器中重构业务逻辑 下面是我的Scala/Play的样子。用一个干净的接口从这个控制器中重构出业务逻辑的好方法/惯用方法是什么 object NodeRender extends Controller { ... def deleteNode(nodeId: Long) = Action { request => //business logic val commitDocume

我不熟悉Scala和游戏;我还编写了一个“DoAll”控制器,它包含业务逻辑和表示逻辑。我想从控制器中重构业务逻辑

下面是我的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)
}

我不是专家,但我很乐意将连贯的逻辑块分解成混合特征

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

我可能会这样做

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))
  }
}
case class Error(message: String)
trait Success
case object Success extends Success
节点服务文件看起来像这样

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))
  }
}
case class Error(message: String)
trait Success
case object Success extends Success
这将分离http部分和业务逻辑,允许您为同一服务创建其他类型的前端。同时,它允许您在提供
节点服务的模拟时测试http处理

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

一些假设:

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稍多一些,但它是多线程的

第3行上传递给Akka.future的代码将在将来某个时候使用不同的线程调用。但是对
Akka.future
的调用会立即返回
future[Try]
(有关业务方法的返回类型,请参见下文)。这意味着变量
response
的类型为
Future[Try]
。第5行对
map
方法的调用不会调用map块内的代码,而是将该代码(第6-10行)注册为回调。线程不会在第5行阻塞,而是将
Future
返回到
Async
块。
Async
块返回一个要播放的
AsyncResult
,告诉播放在将来完成时注册自己以进行回调

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

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

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

def businessLogic(nodeId:Long):未来[尝试]{
val commitDocument=Json.toJson(
地图(
“删除”->Seq(映射(“id”->toJson(nodeId)))
))
val commitSend=Json.stringify(commitDocument)
val commitParams=Map(“commit”->“true”,“wt”->“json”)
val headers=Map(“内容类型”->“应用程序/json”)
val sol=主机(“127.0.0.1”,8080)

val updateReq=sol/“solr store”/“collection1”/“update”/“json”如何测试
deleteNode
操作?好问题!我猜是“BusinessService”不应该是一个对象,那么它可以被模拟,你可以测试一个肯定的和否定的结果。有关更多详细信息,请参阅。或者你的具体意思是不同的部分在不同的线程中运行?另外,Akka.future依赖于Play应用程序的一个实例,它可以在如下的单元测试中存根:implicit val application=Ap应用程序(新文件(“.”),this.getClass.getClassloader,None,Play.Mode.Dev)如何将模拟实例注入到
deleteNode
操作中?
FakeApplication
更易于用于模拟
应用程序
我只添加了一些有用的东西。我将静态部分移动到服务中,以便其他方法可以重用它们。我添加了一些额外的代码,为OP提供了更多的h选项我还有一个习惯,那就是把东西放在更多的行上,让它更具可读性。