Подтвердить что ты не робот

Хороший, идиоматический способ реорганизации бизнес-логики с контроллеров

Я новичок в 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 я пишу два класса 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!
4b9b3361

Ответ 1

Несколько предположений:

1) HTTP-запрос на второй второй строке

2) Вы не говорите, должно ли перенаправление ждать ответа от Http-вызова, но я предполагаю, что он это делает.

Блокирующий вызов должен быть перенесен в другой поток, чтобы вы не блокировали потоки, обрабатывающие запросы. Play docs довольно конкретны. Функция 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 не вызывает код внутри блока карты, скорее он регистрирует этот код (строки 6-10) в качестве обратного вызова. Нить не блокируется в строке 5 и возвращает блок Future в блок Async. Блок Async возвращает AsyncResult для воспроизведения, и он сообщает Play регистрировать себя для обратного вызова, когда будущее будет завершено.

В то же время, какой-то другой поток выполнит вызов BusinessService из строки 3, и как только HTTP-вызов, который вы делаете в обратную систему, вернется, переменная response в строке 3 будет "завершена", что означает, что вызывающий вызов на линиях 6-10 вызывается. result имеет тип Try, который является абстрактным и имеет только два подкласса: Success и Failure. Если result является sucess, тогда метод map вызывает строку 7 и переносит ее в новый Success. Если result является сбой, то метод карты возвращает сбой. Метод recover в строке 8 делает обратное. Если результат метода карты является успешным, он возвращает успех, иначе он вызывает строку 9 и обертывает ее в Success (а не Failure!). Вызов метода get в строке 10 принимает перенаправление или ошибку из Success, и это значение используется для завершения 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.

Ответ 2

Я, вероятно, сделаю это так

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

Файл службы node будет выглядеть примерно так:

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 в класс и передать его с помощью конструктора. В этом примере показано, как это сделать.

Ответ 3

Я не эксперт, но я очень доволен факторингом логических блоков когерентности в черты смешения.

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