To pytanie dręczy mnie już od pewnego czasu (mam nadzieję, że nie jestem jedyny). Chcę wziąć typową trójwarstwową aplikację Java EE i zobaczyć, jak to możliwe, że może wyglądać jak zaimplementowana z aktorami. Chciałbym dowiedzieć się, czy rzeczywiście ma sens takie przejście i jak mogę z niego skorzystać, jeśli ma to sens (może wydajność, lepsza architektura, rozszerzalność, łatwość konserwacji itd.).Przenoszenie typowej architektury trójwarstwowej na aktorów.
Oto typowy kontroler (prezentacja), usługi (logika biznesowa), DAO (dane):
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 = { ... }
}
Można znaleźć coś takiego w wielu zastosowaniach sprężynowych. Możemy przyjąć prostą implementację, która nie ma żadnego współdzielonego stanu, a to dlatego, że nie ma zsynchronizowanych bloków ... więc cały stan jest w bazie danych, a aplikacja opiera się na transakcjach. Usługa, kontroler i dao mają tylko jedno wystąpienie. Tak więc dla każdego żądania serwer aplikacji użyje osobnego wątku, ale wątki nie będą się blokować (ale będą blokowane przez DB IO).
Załóżmy, że próbujemy wprowadzić podobną funkcjonalność z aktorami. To może wyglądać następująco:
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) => ...
}
myślę, że nie jest to bardzo ważne, tutaj jak Get() i Poczty (odciągi) są realizowane. Załóżmy, że piszę framework do wdrożenia tego. Mogę wysłać wiadomość do kontrolera w ten sposób:
controller !! Get("/users")
To samo zrobi kontroler i obsługa. W takim przypadku cały przepływ pracy byłby synchroniczny. Co gorsza - mogę przetworzyć tylko jedną prośbę o czasie (w międzyczasie wszystkie inne żądania trafią do skrzynki pocztowej kontrolera). Muszę więc zrobić to wszystko asynchronicznie.
Czy istnieje jakiś elegancki sposób wykonania asynchronicznego wykonywania każdego kroku przetwarzania w tej konfiguracji?
O ile rozumiem, każdy poziom powinien w jakiś sposób zapisać kontekst wiadomości, którą otrzymuje, a następnie wysłać wiadomość do warstwy poniżej. Kiedy poziom poniżej odpowiedzi za pomocą jakiegoś komunikatu wynikowego powinienem być w stanie przywrócić początkowy kontekst i odpowiedzieć tym wynikiem pierwotnemu nadawcy. Czy to jest poprawne?
Co więcej, w tej chwili mam tylko jedną instancję aktora dla każdej warstwy. Nawet jeśli będą działać asynchronicznie, nadal mogę przetwarzać równolegle tylko jeden kontroler, usługę i wiadomość dao. Oznacza to, że potrzebuję więcej aktorów tego samego typu. Co prowadzi mnie do LoadBalancer dla każdego poziomu. Oznacza to również, że jeśli mam usługę UserService i ItemService, powinienem załadować oba te ustawienia osobno.
Mam wrażenie, że rozumiem coś złego. Wszystkie potrzebne konfiguracje wydają się zbyt skomplikowane. Co o tym myślisz?
(PS: Byłoby bardzo interesujące wiedzieć, jak transakcje DB pasuje do tego obrazu, ale myślę, że to przesada dla tego wątku)
+1 - Ambitne rzeczy od ciebie, Easy Angel. – duffymo