Questa domanda mi infastidisce da tempo (spero di non essere l'unico). Voglio prendere una tipica applicazione Java EE a 3 livelli e vedere come può essere implementata con gli attori. Mi piacerebbe scoprire se ha davvero senso fare una tale transizione e come posso trarne profitto se ha senso (magari prestazioni, migliore architettura, estensibilità, manutenibilità, ecc ...).Trasferimento di un'architettura tipica a 3 livelli agli attori
Ecco tipico Controller (presentazione), servizi (logica di business), DAO (dati):
trait UserDao {
def getUsers(): List[User]
def getUser(id: Int): User
def addUser(user: User)
}
trait UserService {
def getUsers(): List[User]
def getUser(id: Int): User
def addUser(user: User): Unit
@Transactional
def makeSomethingWithUsers(): Unit
}
@Controller
class UserController {
@Get
def getUsers(): NodeSeq = ...
@Get
def getUser(id: Int): NodeSeq = ...
@Post
def addUser(user: User): Unit = { ... }
}
si può trovare qualcosa di simile in molte applicazioni di primavera. Possiamo prendere un'implementazione semplice che non ha nessuno stato condiviso e questo perché non ha blocchi sincronizzati ... quindi tutto lo stato è nel database e l'applicazione si basa sulle transazioni. Servizio, controller e dao hanno solo un'istanza. Quindi per ogni richiesta il server applicativo utilizzerà thread separati, ma i thread non si bloccheranno a vicenda (ma saranno bloccati da DB IO).
Supponiamo di provare a implementare funzionalità simili con gli attori. Può apparire così:
sealed trait UserActions
case class GetUsers extends UserActions
case class GetUser(id: Int) extends UserActions
case class AddUser(user: User) extends UserActions
case class MakeSomethingWithUsers extends UserActions
val dao = actor {
case GetUsers() => ...
case GetUser(userId) => ...
case AddUser(user) => ...
}
val service = actor {
case GetUsers() => ...
case GetUser(userId) => ...
case AddUser(user) => ...
case MakeSomethingWithUsers() => ...
}
val controller = actor {
case Get("/users") => ...
case Get("/user", userId) => ...
case Post("/add-user", user) => ...
}
penso che non è molto importante qui come get() e post() estrattori sono implementati. Supponiamo che io scriva un quadro per implementarlo. Posso inviare un messaggio al controller in questo modo:
controller !! Get("/users")
La stessa cosa dovrebbe essere fatta dal controller e dal servizio. In questo caso l'intero flusso di lavoro sarebbe sincrono. Ancor peggio: posso elaborare una sola richiesta alla volta (nel frattempo tutte le altre richieste arrivano nella casella di posta del controllore). Quindi ho bisogno di renderlo tutto asincrono.
C'è un modo elegante per eseguire ogni passo di elaborazione in modo asincrono in questa configurazione?
Per quanto ne so, ogni livello dovrebbe in qualche modo salvare il contesto del messaggio che riceve e quindi inviare un messaggio al livello sottostante. Quando il livello sotto risponde con un messaggio di risultato, dovrei essere in grado di ripristinare il contesto iniziale e rispondere con questo risultato al mittente originale. È corretto?
Inoltre, al momento ho solo un'istanza di attore per ogni livello. Anche se funzionassero in modo asincrono, posso ancora elaborare in parallelo un solo controller, servizio e messaggio dao. Ciò significa che ho bisogno di più attori dello stesso tipo. Il che mi porta a LoadBalancer per ogni livello. Questo significa anche che se avessi UserService e ItemService dovrei caricarli entrambi separatamente.
Ho sentito, che ho capito qualcosa di sbagliato. Tutta la configurazione necessaria sembra essere complicata. Cosa ne pensi di questo?
(PS: Sarebbe inoltre molto interessante sapere come le transazioni DB rientrano in questa immagine, ma penso che sia eccessivo per questa discussione)
+1 - Roba ambiziosa da parte tua, Easy Angel. – duffymo