2013-03-05 8 views
11

Jestem nowy w Scala and Play; i napisałem kontroler "do wszystkiego", który zawiera zarówno logikę biznesową, jak i prezentacyjną. Chcę odjąć logikę biznesową od kontrolera.Dobry, idiomatyczny sposób na refaktoryzację logiki biznesowej ze sterowników

Oto, jak wygląda moja Scala/Play. Jaki jest dobry/idiomatyczny sposób na wyprowadzenie logiki biznesowej z tego kontrolera za pomocą czystego interfejsu?

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

W Python/Django piszę dwie klasy XApiHandler i XBackend i użyć przejrzysty interfejs pomiędzy nimi.

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! 

Odpowiedz

7

Kilka założenia:

1) Połączenie HTTP na swoim przedostatnim bloków liniowych

2) Nie mów, czy przekierowanie musi czekać na odpowiedź z wywołania HTTP, ale załóżmy, że tak.

Zablokowanie połączenia powinno zostać przeniesione do innego wątku, aby nie blokować wątków obsługujących żądania. Zagraj w dokumentach dość szczegółowo o tym. Pomocna jest funkcja Akka.future w połączeniu z Async.

kod Kontroler:

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} 

To nieco więcej niż PHP, ale jest wielowątkowy.

Kod przekazywany do Akka.future na linii 3 zostanie w przyszłości wywołany przy użyciu innego wątku. Jednak wywołanie Akka.future zwraca natychmiast z numerem Future[Try] (zobacz poniżej typ zwracanej metody biznesowej).Oznacza to, że zmienna response ma typ . Wywołanie metody map w linii 5 nie wywołuje kodu wewnątrz bloku mapy, raczej rejestruje ten kod (linie 6-10) jako wywołanie zwrotne. Wątek nie blokuje się w linii 5 i zwraca Future do bloku Async. Blok Async zwraca graczowi AsyncResult, który nakazuje Play zarejestrować się w celu oddzwonienia po zakończeniu przyszłości.

W międzyczasie, inny wątek wywoła numer BusinessService z linii 3, a po powrocie wywołania HTTP do systemu zaplecza zmienna response na linii 3 jest "ukończona", co oznacza, że wywołanie zwrotne w liniach 6-10 zostaje wywołane. result ma typ Try, który jest abstrakcyjny i ma tylko dwie podklasy: Success i Failure. Jeśli result jest sukcesem, to metoda map wywołuje linię 7 i zawija ją w nowym Success. Jeśli result jest niepowodzeniem, metoda map zwraca błąd. Metoda recover na linii 8 działa odwrotnie. Jeśli wynikiem metody mapy jest sukces, to zwraca sukces, w przeciwnym razie wywołuje linię 9 i owija ją w Success (a nie Failure!). Wywołanie metody get w linii 10 powoduje przekierowanie lub błąd z Success i ta wartość jest używana do wypełnienia AsyncResult, na którą gra się trzyma. Następnie gra otrzymuje wywołanie zwrotne, że odpowiedź jest gotowa i może zostać wyrenderowana i wysłana.

Za pomocą tego rozwiązania nie są zgłaszane żadne wątki, które obsługują przychodzące żądania. Jest to ważne, ponieważ na przykład na 4 rdzeniowym komputerze Play ma tylko 8 wątków zdolnych do obsługi żądań przychodzących. Nie będzie odradzać żadnych nowych, przynajmniej nie przy użyciu domyślnej konfiguracji.

Oto kod z obiektu Serwis biznesowy (całkiem dużo skopiowany kod):

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 
} 

Logika prezentacji i logiki biznesowej są teraz całkowicie oddzielone.

Aby uzyskać więcej informacji, zobacz https://speakerdeck.com/heathermiller/futures-and-promises-in-scala-2-dot-10 i http://docs.scala-lang.org/overviews/core/futures.html.

+0

Jak można przetestować działanie 'deleteNode'? – EECOLOR

+0

Dobre pytanie! Przypuszczam, że "BusinessService" nie powinien być obiektem, to może być wyśmiewany i można zrobić test na pozytywny i negatywny wynik. Więcej informacji można znaleźć na stronie http://www.playframework.com/documentation/2.1.0/ScalaTest. Czy masz na myśli konkretnie, że różne części działają w różnych wątkach? –

+0

Ponadto, Akka.future opiera się na instancji aplikacji Play, która może zostać poddana skrótowi do testowania jednostkowego w następujący sposób: niejawna aplikacja val = Aplikacja (nowy plik ("."), This.getClass.getClassloader, Brak, Play.Mode .Dev) –

1

Nie jestem ekspertem, ale jestem bardzo zadowolony z faktoringu spójnych bloków logicznych do mix-in cech.

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

drukuje

starting ... 
Logic Here 
Hello World 
4

to pewnie zrobić jak tego pliku usług

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

Węzeł będzie wyglądać następująco

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

Gdzie określonym Error i Success jak ten

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

ten oddziela swój udział HTTP i logikę biznesową, co pozwala tworzyć inne rodzaje przednich końcach za tę samą usługę. Jednocześnie pozwala przetestować obsługę protokołu HTTP podczas dostarczania makiety modelu NodeService.

Jeśli musisz mieć różne typy NodeService powiązanych z tym samym kontrolerem, możesz przekonwertować klasę NodeRenderer i przekazać ją za pomocą konstruktora. This example pokazuje, jak to zrobić.

+0

Dodałem tylko kilka rzeczy, aby były przydatne. Przeniosłem części statyczne do serwisu, aby można je było ponownie wykorzystać za pomocą innych metod. Dodałem dodatkowy kod, aby dać OP więcej opcji do jego realizacji. Mam też zwyczaj rozprzestrzeniania rzeczy na więcej linii, aby uczynić je bardziej czytelnymi. – EECOLOR

Powiązane problemy