从控制器中重构业务逻辑的好方法是什么?

11

我是Scala和Play的新手,并编写了一个包含业务和展示逻辑的“do all”控制器。我想将业务逻辑重构到控制器之外。

这是我的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!
3个回答

7
一些假设:
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方法不会调用地图块内的代码,而是将该代码(第6-10行)注册为回调。线程不会阻塞第5行,并将Future返回给Async块。 Async块返回一个AsyncResult给Play,这告诉Play在完成未来时注册自己的回调。

与此同时,另一些线程将从第3行调用BusinessService,并且一旦您对后端系统进行的HTTP调用返回,则第3行的response变量“已完成”,这意味着在第6-10行上调用回调。 result具有抽象类型Try,只有两个子类:SuccessFailure。如果result是成功的,则map方法调用第7行并将其包装在新的Success中。如果result是失败的,则映射方法返回该失败。第8行上的recover方法做相反的操作。如果映射方法的结果是成功的,则返回成功,否则调用第9行并将其包装为Success(而不是Failure!)。在线10上对get方法的调用将重定向或错误从Success中去掉,并使用该值完成Play所持有的AsyncResult。然后,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


你会如何测试 deleteNode 操作? - EECOLOR
好问题!我猜“BusinessService”不应该是一个对象,这样它就可以被模拟,你可以进行正面和负面结果的测试。请参阅http://www.playframework.com/documentation/2.1.0/ScalaTest以获取更多详细信息。或者你是指不同的部分在不同的线程中运行? - Ant Kutschera
此外,Akka.future 依赖于 Play 应用程序的一个实例,可以像这样为单元测试创建存根:implicit val application = Application(new File("."), this.getClass.getClassloader, None, Play.Mode.Dev)。 - Ant Kutschera
这样怎么样:def deleteNode(nodeId: Long)(implicit nodeService: NodeService = NodeService)... - Ant Kutschera
@AntKutschera,您的回答超出了我的要求!例如,“没有服务于传入请求的线程被阻塞” - Jesvin Jose
显示剩余2条评论

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部分和业务逻辑分离,允许您为同一服务创建其他类型的前端。同时,这也允许您在提供NodeService模拟时测试http处理。
如果您需要将不同类型的NodeService绑定到同一控制器,则可以将NodeRenderer转换为类,并通过构造函数传递它。 这个例子向您展示了如何实现。

我只是添加了一些东西使其更有用。我将静态部分移到服务中,以便其他方法可以重复使用它们。我添加了一些额外的代码,为OP提供了更多实现选项。我还有一个习惯,就是将事物分散到更多行中,以使事物更易读。 - EECOLOR

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接