Этот вопрос беспокоит меня в течение некоторого времени (надеюсь, я не единственный). Я хочу взять типичное трехуровневое приложение Java EE и посмотреть, как он может выглядеть как реализованный с актерами. Я хотел бы узнать, действительно ли имеет смысл сделать такой переход и как я могу извлечь из этого выгоду, если это имеет смысл (возможно, производительность, улучшенная архитектура, расширяемость, ремонтопригодность и т.д.).
Вот типичный контроллер (презентация), служба (бизнес-логика), DAO (данные):
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 = { ... }
}
Во многих приложениях spring вы можете найти что-то подобное. Мы можем выполнить простую реализацию, которая не имеет никакого общего состояния, и потому, что у нее нет синхронизированных блоков... поэтому все состояние находится в базе данных, а приложение использует транзакции. Сервис, контроллер и дао имеют только один экземпляр. Поэтому для каждого запроса сервер приложений будет использовать отдельный поток, но потоки не будут блокировать друг друга (но будут заблокированы DB IO).
Предположим, мы пытаемся реализовать аналогичную функциональность с участниками. Это может выглядеть так:
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) => ...
}
Я думаю, что здесь не очень важно, как реализованы экстракторы Get() и Post(). Предположим, что я пишу структуру для ее реализации. Я могу отправить сообщение на контроллер следующим образом:
controller !! Get("/users")
То же самое будет делать контроллер и сервис. В этом случае весь рабочий процесс будет синхронным. Хуже того - я могу обработать только один запрос вовремя (в то время как все остальные запросы попали бы в контрольный почтовый ящик). Поэтому мне нужно сделать все асинхронным.
Есть ли элегантный способ выполнить каждый шаг обработки асинхронно в этой настройке?
Насколько я понимаю, каждый уровень должен каким-то образом сохранить контекст получаемого сообщения, а затем отправить сообщение на уровень ниже. Когда уровень ниже ответов с некоторым результатом сообщения, я должен иметь возможность восстановить исходный контекст и ответить с этим результатом исходному отправителю. Правильно ли это?
Кроме того, на данный момент у меня есть только один экземпляр актера для каждого уровня. Даже если они будут работать асинхронно, я все равно могу обрабатывать параллельно только один контроллер, сервис и сообщение dao. Это означает, что мне нужно больше актеров одного типа. Это приводит меня к LoadBalancer для каждого уровня. Это также означает, что если у меня есть UserService и ItemService, я должен LoadBalace их обоих отдельно.
У меня такое чувство, что я понимаю что-то не так. Вся необходимая конфигурация кажется слишком сложной. Что вы думаете об этом?
(PS: Было бы также очень интересно знать, как транзакции БД вписываются в это изображение, но я думаю, что это излишне для этого потока)