2013-03-05 14 views
11

Sono nuovo di Scala e Play; e ho scritto un controller "fai tutto" che contiene sia la logica di business che di presentazione. Voglio rifattorizzare la logica aziendale dal controller.Buono, modo idiomatico per rifattorizzare la logica aziendale dai controllori

Ecco come si presenta la mia Scala/Play. Qual è un modo buono/idiomatico per rifattorizzare la logica aziendale da questo controller, con un'interfaccia pulita?

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

In Python/Django scrivo due classi XApiHandler e XBackend e utilizzare un'interfaccia pulita tra di loro.

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! 

risposta

7

Qualche ipotesi:

1) La chiamata HTTP sul secondo blocchi ultima riga

2) Non si dice se il reindirizzamento ha bisogno di attendere la risposta dalla chiamata HTTP, ma io supponiamo che lo faccia.

La chiamata di blocco deve essere spostata su un altro thread in modo da non bloccare i thread che gestiscono le richieste. I documenti di Play sono piuttosto specifici su questo. La funzione Akka.future combinata con Async aiuta.

Controller cod:

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} 

Questo è un po 'più del vostro PHP, ma è multi-threaded.

Il codice passato a Akka.future sulla riga 3 verrà chiamato in un secondo momento in futuro utilizzando un thread diverso. Ma la chiamata a Akka.future restituisce immediatamente con un Future[Try] (vedere sotto per il tipo di ritorno del metodo di business).Ciò significa che la variabile response ha il tipo Future[Try]. La chiamata al metodo map alla riga 5 non chiama il codice all'interno del blocco della mappa, ma registra quel codice (righe 6-10) come un callback. Il thread non si blocca sulla riga 5 e restituisce Future al blocco Async. Il blocco Async restituisce un AsyncResult da riprodurre e indica a Play di registrarsi per una richiamata al termine del futuro.

Nel frattempo, qualche altro thread farà la chiamata al BusinessService dalla linea 3 e una volta che la chiamata HTTP che si fanno alla parte posteriore sistema restituisce fine, la variabile response sulla linea 3 è "completato" il che significa che il viene richiamato il callback sulle linee 6-10. result ha il tipo Try che è astratto e ha solo due sottoclassi: Success e Failure. Se result è un successo, il metodo map chiama la linea 7 e lo avvolge in un nuovo Success. Se result è un errore, il metodo map restituisce l'errore. Il metodo recover alla riga 8 fa l'opposto. Se il risultato del metodo mappa è un successo, restituisce il successo, altrimenti chiama la riga 9 e lo avvolge in un Success (non uno Failure!). La chiamata al metodo get alla riga 10 rimuove il reindirizzamento o l'errore dallo Success e tale valore viene utilizzato per completare lo AsyncResult su cui è in attesa. Play quindi riceve una richiamata che la risposta è pronta e può essere visualizzata e inviata.

Utilizzando questa soluzione, nessun thread che gestisce le richieste in arrivo viene bloccato. Questo è importante perché, ad esempio, su una macchina a 4 core, Play ha solo 8 thread in grado di gestire le richieste in entrata. Non genererà alcun nuovo, almeno non quando si utilizza la configurazione predefinita.

ecco il codice dall'oggetto Business Service (praticamente copiato il codice):

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 
} 

La logica di presentazione e la logica di business sono ora completamente disaccoppiati.

Vedere https://speakerdeck.com/heathermiller/futures-and-promises-in-scala-2-dot-10 e http://docs.scala-lang.org/overviews/core/futures.html per ulteriori informazioni.

+0

Come testare l'azione 'deleteNode'? – EECOLOR

+0

Buona domanda! Immagino che "BusinessService" non dovrebbe essere un oggetto, quindi può essere deriso e puoi fare un test per un risultato positivo e negativo. Vedi http://www.playframework.com/documentation/2.1.0/ScalaTest per maggiori dettagli. O intendi nello specifico che parti diverse vengono eseguite in thread diversi? –

+0

Inoltre, Akka.future si basa su un'istanza dell'applicazione Play, che può essere sottoposta a test di unità in questo modo: implicit val application = Applicazione (nuovo file ("."), This.getClass.getClassloader, None, Play.Mode .Dev) –

1

io non sono un esperto, ma sono abbastanza contento di factoring fuori blocchi logici coerenti in mix-in tratti.

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

stampe

starting ... 
Logic Here 
Hello World 
4

probabilmente sarebbe fare in questo servizio di file

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

Il nodo sarebbe simile a questa

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

Dove ho definito Error e Success come questo

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

Questo separa la vostra parte http e la logica di business, consentendo di creare altri tipi di front-end per lo stesso servizio. Allo stesso tempo, consente di testare la gestione http fornendo una simulazione di NodeService.

Se è necessario associare diversi tipi di NodeService allo stesso controller, è possibile convertire lo NodeRenderer in una classe e passarlo utilizzando il costruttore. This example mostra come farlo.

+0

Ho aggiunto solo alcune cose per renderlo utile. Ho spostato le parti statiche nel servizio in modo che possano essere riutilizzate con altri metodi. Ho aggiunto del codice extra per dare all'OP più opzioni per la sua implementazione. Ho anche l'abitudine di diffondere le cose su più linee per rendere le cose più leggibili. – EECOLOR

Problemi correlati