我是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!
答案 0 :(得分: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
方法的调用不会调用map块中的代码,而是将该代码(第6-10行)注册为回调。线程不会在第5行阻塞,并将Future
返回到Async
块。 Async
块将AsyncResult
返回给Play,并告诉Play在未来完成时注册自己的回调。
同时,其他一些线程将从第3行调用BusinessService
,一旦你对后端系统发出的HTTP调用返回,第3行的response
变量“完成”意味着调用第6-10行的回调。 result
的类型Try
是抽象的,只有两个子类:Success
和Failure
。如果result
是成功的,那么map
方法会调用第7行并将其包装在新的Success
中。如果result
失败,则map方法返回失败。第8行的recover
方法恰恰相反。如果map方法的结果是成功的,那么它返回成功,否则它调用第9行并将其包装在Success
(而不是Failure
!)中。第10行对get
方法的调用会将重定向或错误从Success
中删除,该值用于完成Play所持有的AsyncResult
。然后播放获得回复,表示响应已准备就绪,可以呈现并发送。
使用此解决方案,不会阻止为传入请求提供服务的线程。这很重要,因为例如在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。
答案 1 :(得分: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))
}
}
我定义了Error
和Success
这样的
case class Error(message: String)
trait Success
case object Success extends Success
这将您的http部分和业务逻辑分开,允许您为同一服务创建其他类型的前端。同时,它允许您在提供NodeService
的模拟时测试您的http处理。
如果您需要将不同类型的NodeService
绑定到同一个控制器,您可以将NodeRenderer
转换为类并使用构造函数传递它。 This example向您展示了如何做到这一点。
答案 2 :(得分: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