Написание приложений с участием Scala на практике II
Поскольку мой первый вопрос был таким долгим, я задаю это как отдельный вопрос. Это еще один вопрос об архитектуре актерского приложения.
Отслеживание путей сообщений через приложение
Возьмем кусок кода Java:
public void deleteTrades(User user, Date date) {
PermissionSet ps = permissionService.findPermissions(user)
if (ps.hasPermission("delete")) {
Set<Trade> ts = peristence.findTrades(date);
reportService.sendCancelReports(ts);
positionService.updateWithDeletedTrades(ts);
}
}
В этом коде у меня есть 4 отдельных компонента, и взаимодействие между ними, необходимое для процедуры deleteTrades
, хорошо определено. Он полностью содержится в методе deleteTrades
.
Моделируя это с помощью Actor
и заменяя мои 4 компонента четырьмя отдельными участниками, как мне отслеживать (на мой взгляд), что включает в себя процедура? В частности, если я избегу использовать оператор !?
, то скорее всего я отправлю сообщение ConditionalDelete
на мой PermissionActor
, который будет отправлять сообщение GetTradesAndDelete
на my PersistenceActor
, который затем отправит дальнейшие сообщения и т.д. Код для обработки удаления будет разбросан по моему приложению.
Это также означает, что почти каждому актеру нужен дескриптор для каждого другого актера (для пересылки сообщений).
Как и в моем предыдущем вопросе, как люди справляются с этим? Есть ли хороший инструмент моделирования, который позволяет вам отслеживать все это? Используют ли люди !?
Я превращаю слишком много компонентов в Actor
s?
Ответы
Ответ 1
Вы используете 5 компонентов, определенно. Есть актеры, которые занимаются конкретными задачами, а также оркестром.
Вопрос, который у вас должен быть, конечно, заключается в том, как вы связываете это асинхронно. Ну, это на самом деле несколько легко, но это может затмить код. В принципе, вы отправляете каждому компоненту ответ, который вы хотите.
react {
case DeleteTrades(user,dates) =>
PermissionService ! FindPermissions(user, DeleteTradesPermissions(dates) _)
case DeleteTradesPermissions(dates)(ps) =>
if (ps hasPermission "delete")
Persistence ! FindTrades(date, DeleteTradesTradeSet _)
case DeleteTradesTradeSet(ts) =>
ReportService ! SendCancelReports(ts)
PositionService ! UpdateWithDeletedTrades(ts)
}
Здесь мы используем currying для передачи "дат" в первом возвращаемом ответе. Если есть много параметров, связанных с взаимодействием, возможно, лучше сохранить информацию для всех текущих транзакций в локальном HashSet и просто передать токен, который вы будете использовать, чтобы найти эту информацию при получении ответа.
Обратите внимание, что этот одиночный актер может обрабатывать несколько одновременных действий. В этом конкретном случае просто удалите транзакции, но вы можете добавить любое количество различных действий для его обработки. Когда данные, необходимые для одного действия, готовы, это действие продолжается.
ИЗМЕНИТЬ
Здесь приведен рабочий пример того, как эти классы могут быть определены:
class Date
class User
class PermissionSet
abstract class Message
case class DeleteTradesPermission(date: Date)(ps: PermissionSet) extends Message
case class FindPermissions(u: User, r: (PermissionSet) => Message) extends Message
FindPermissions(new User, DeleteTradesPermission(new Date) _)
Несколько объяснений о карри и функциях. Класс DeleteTradesPermission
задан так, что вы можете передать Date
на нем, а другую функцию выполнить с помощью PermissionSet
. Это будет шаблон сообщений ответа.
Теперь класс FindPermissions
получает в качестве второго параметра функцию. Актер, получающий это сообщение, передаст возвращаемое значение этой функции и получит сообщение Message
, которое будет отправлено в качестве ответа. В этом примере сообщение будет иметь как Date
, которое посылает вызывающий актер, так и PermissionSet
, который предоставляет автоответчик.
Если ожидаемый ответ не ожидается, например, в случае DeleteTrades
, SendCancelReports
и UpdateWithDeletedTrades
для целей этого примера, вам не нужно передавать функцию возвращаемого сообщения.
Поскольку мы ожидаем функцию, которая возвращает параметр Message как параметр для сообщений, требующих ответа, мы могли бы определить такие черты:
trait MessageResponse1[-T1] extends Function1[T1, Message]
trait MessageResponse2[-T1, -T2] extends Function2[T1, T2, Message]
...
Ответ 2
Актеры не должны использоваться для замены традиционных компонентов обслуживания без соображений.
Большинство компонентов службы, которые мы пишем в настоящее время, путем обучения, являются апатридами. Компоненты службы без состояния легче управлять (без класса сообщений и т.д.), Чем с актерами. Одной из вещей, которые им не хватает, по сравнению с актерами, является асинхронное выполнение. Но когда клиенты ожидают, что результаты будут возвращаться синхронно большую часть времени, синхронные компоненты обслуживания без состояния не будут хорошими для работы.
Актер хорошо подходит, когда есть внутренние состояния для управления. Нет необходимости выполнять какую-либо синхронизацию внутри "act()" для доступа к внутренним состояниям и беспокоиться о состоянии гонки. Так долго как "!?" не используется внутри "act()" , необходимо также минимизировать блокировку.
Актеры должны с осторожностью относиться к любой обработке блокировки при обработке сообщений. Поскольку участники обрабатывают свои сообщения последовательно, в любое время они блокируются, ожидая ввода/вывода внутри "act()" , они не могут обрабатывать какие-либо другие сообщения в своих почтовых ящиках. Идиома Scala, чтобы использовать здесь, - это запустить другой ad-hoc-актер, который выполняет фактическую операцию блокировки. Этот анти-шаблон влияет на активацию (реагировать) на актера еще больше, потому что он блокирует поток, на котором основан на событиях актер.
Из того, что я могу собрать, все четыре ваших обращения к компонентам службы потенциально блокируются, поэтому следует обратить внимание на их масштабирование при преобразовании их в участников.