Acteurs utilisant Scala et Akka – Partie 2: AskPattern et SpawnProtocol
Un guide rapide pour commencer avec les acteurs Akka avec Scala pour les débutants
Il s’agit du deuxième article d’une série d ‘«Acteurs utilisant Scala et Akka». Dans Partie 1, nous avons vu comment
- Créer un nouveau projet scala et l’exécuter
- Créer un nouveau système d’acteurs
- Créer un nouveau comportement pour gérer les messages et modifier l’état
- Générer un nouvel acteur système en utilisant un comportement donné
- Envoyer des messages à l’acteur
Dans cet article, nous verrons
- AskPattern (comment obtenir une réponse des acteurs)
- SpawnProtocol (génération d’acteurs utilisateurs au lieu d’acteurs système)
Avant de passer au modèle de demande, il faut avoir une compréhension de base de certains concepts de la scala
- Futures
- Contexte d’exécution
- Paramètres implicites
Si vous êtes déjà familier avec scala, n’hésitez pas à passer à la section «Demander un modèle».
Futures
Les futurs en scala sont similaires à Task
en C #, un Promise
en Javascript ou un CompletableFuture
en Java. Leur objectif est d’aider les développeurs à écrire du code asynchrone. UNE Future
représente une valeur qui sera prête à l’avenir lorsque le futur sera achevé. Un avenir peut se terminer avec une valeur réussie ou échouer avec une exception. Les futurs sont idéalement retournés à partir de méthodes qui effectuent une opération asynchrone telles que les entrées-sorties réseau, les entrées-sorties de fichiers, etc. C’est une conception évoluée par rapport aux rappels qui étaient difficiles à composer. Étant donné que les contrats à terme sont des «valeurs de retour» au lieu de paramètres de méthode, il est facile de chaîner plusieurs contrats à terme ensemble (composer) et également de les répartir dans l’application.
Il est important de connaître Future
parce que lorsque vous envoyez un message à un acteur et attendez une réponse, même si l’acteur envoie une réponse de type T
, ce que vous obtiendrez sera toujours Future[T]
. En effet, la communication basée sur les messages est asynchrone et non bloquante.
Contexte d’exécution
Les rappels sont un moyen de dire au programme ce que vous voulez faire après une opération asynchrone. Lorsque vous utilisez une API basée sur le rappel, vous perdez le contrôle sur thread
ou thread-pool
le rappel sera exécuté.
Lorsque vous utilisez des futures, vous avez ce contrôle. map
ing & flatMap
sur un Future
c’est comme attacher un rappel à un futur et cela nécessite de fournir un ExecutionContext
. Ce contexte d’exécution peut être alimenté par un seul thread ou l’un des différents types de pools de threads. En d’autres termes, tout ce que vous écrivez dans le map
ou flatMap
sera exécuté par le contexte d’exécution fourni par vous.
Scala fournit un ExecutionContext prêt à l’emploi sur scala.concurrent.ExecutionContext.Implicits.global
et il fait un bon travail pour faire un travail asynchrone ainsi que pour bloquer le code lorsqu’il est utilisé de manière appropriée. Il est recommandé d’utiliser différents pools de threads (et contextes d’exécution) pour des domaines d’application logiquement différents. Vous pouvez lire sur le contexte d’exécution global dans le documentation officielle.
Paramètres implicites
Scala a cette fonctionnalité où vous pouvez définir une liste de paramètres comme «implicite» et elle sera transmise automatiquement par le compilateur à la fonction, si une variable implicite du type requis est présente dans la portée de l’appelant.
Voyons un exemple
sendData est une méthode qui accepte un paramètre data
, de type Any
. Any
est un « super type » de tous les types en scala afin que vous puissiez passer n’importe quoi à sendData
. Cette méthode a une autre liste de paramètres qui commence par implicit
et nécessite un paramètre de type Serializer
. En regardant le corps de la méthode, nous pouvons voir que le corps de la méthode peut accéder serializer
argument de la même manière qu’il peut accéder data
argument. Lorsque nous appelons cette méthode sans serializer
, il ne parvient pas à compiler comme le montre la capture d’écran ci-dessus.
Nous pouvons ouvrir une autre paire d’accolades et passer le sérialiseur comme n’importe quel autre paramètre et cela se compilerait et fonctionnerait très bien.
Mais comme il est marqué comme implicit
, il est conçu et destiné à être transmis automatiquement – implicitement.
Ici, j’ai déclaré une variable et l’ai marquée comme implicit
et maintenant je n’ai plus besoin de le passer. Puisqu’il a le type requis et est de portée actuelle, le compilateur fait le travail pour moi. Si j’active la vue magique dans IntelliJ en appuyant sur option
+ ctrl
+ shift
+ +
+ +
, Je peux voir le paramètre implicite passé!
J’ai parlé de paramètres implicites parce que .map
et .flatMap
nécessite de passer un ExecutionContext
implicitement. Il y a beaucoup plus à implicit
s que cela à Scala et vous pouvez en savoir plus à leur sujet dans le documentation officielle.
Le modèle de demande est utilisé lorsque vous envoyez un message à un acteur et attendez également une réponse. Comme il s’agit d’une communication basée sur les messages et asynchrone, nous récupérons un Future
de réponse. L’avenir se termine lorsque les acteurs décident d’envoyer une réponse et que la réponse atteint la destination.
Vous souvenez-vous du code de l’acteur du compte bancaire où nous avons terminé partie 1?
Ici, pour connaître le solde du compte, on fait imprimer l’acteur sur console. Mais que se passe-t-il si nous voulons obtenir l’équilibre comme réponse? Faisons en sorte que cela se produise.
La première chose que nous devrons faire est de – changer le protocole. Voici notre protocole existant
Nous devons ajouter un paramètre « replyTo » (le nom du paramètre n’a pas d’importance) au message qui nécessite une réponse. Il doit être de type ActorRef[T]
où T
est le type de réponse que vous souhaitez. Notez que nous avons changé le nom de PrintBalance
à GetBalance
et aussi changé de case object
à case class
car il a maintenant des paramètres et ne peut plus être singleton.
Voyons comment les comportements doivent changer.
C’était assez simple (j’espère!). Lors du traitement du message dans le comportement, nous saisissons l’adresse «replyTo» et envoyons simplement (!
) l’équilibre.
Voyons maintenant comment nous pouvons recevoir la réponse en dehors du monde des acteurs. C’est là que le modèle de demande vient à la rescousse.
Nous devons d’abord ajouter une instruction d’importation pour activer certaines méthodes d’extension
Cela fournit un ask
méthode d’extension terminée account1
acteurRef. Observons cette signature.
Il nécessite un paramètre explicite, c’est-à-dire une fonction de ActorRef[Int]
à BankAccountMessage
. C’est facile à construire.
Nous n’avons pas à nous soucier de la façon dont nous obtenons cet acteurRef, nous devons juste nous préoccuper de créer un nouveau BankAccountMessage
. Dans ce cas, il est GetBalance
. GetBalance
nécessite un ActorRef[Int]
qui nous est fourni par ask
méthode. La méthode Ask génère en interne un acteur temporaire et nous fournit sa référence d’acteur. Mais nous ne devons pas trop nous en préoccuper, c’est juste bon à savoir.
Nous pouvons réduire ce code. ask
prend une fonction et GetBalance
Le constructeur est également une fonction qui prend l’acteur réf. Nous pouvons passer cela directement et laisser le compilateur le développer au moment de la compilation.
ask
nécessite deux autres paramètres qui sont marqués comme implicites. Un délai d’attente et un planificateur. Le délai est nécessaire pour dire ask
, combien de temps faut-il attendre avant d’échouer dans le futur avec une exception de timeout. Un ordonnanceur est un utilitaire qui, comme son nom l’indique, est utilisé pour planifier les exécutions de fonctions. Dans ce cas, ask
nécessite que le planificateur déclenche une exception de délai d’expiration au cas où l’acteur n’envoie pas de réponse ou s’il n’atteint pas à temps Pour transmettre ces valeurs implicites, il suffit de créer leurs instances dans le cadre de ask
et marquez-les comme implicit
comme indiqué ci-dessous.
Nous n’avons pas besoin de créer une nouvelle instance de planificateur, nous pouvons simplement utiliser le planificateur de notre système d’acteur. Pour définir le délai d’expiration, vous devez passer une instance de FiniteDuration
et il peut être créé en utilisant des méthodes d’extension telles que .seconds
ou .minutes
. Vous devez importer scala.concurrent.duration.DurationInt
pour ces extensions.
Akka fournit également un fonction opérateur pour ask
et c’est ?
. C’est similaire à !
qui est une fonction d’opérateur pour tell
. En utilisant cette fonction opérateur, le code devient comme indiqué ci-dessous
Maintenant que nous avons l’avenir de l’équilibre, nous pouvons utiliser .foreach
pour accéder au solde réel, puis l’imprimer.
Cela nécessite un contexte d’exécution implicite. Nous pouvons utiliser le contexte d’exécution global en important scala.concurrent.ExecutionContext.Implicits.global
ou nous pouvons utiliser celui dans le système d’acteur.
Notez que pour scheduler
Je dois créer une nouvelle variable implicite, mais pour executionContext
, Je pouvais juste l’importer et ça a marché. En effet, le contexte d’exécution du système d’acteurs est déjà marqué implicitement.
Dans partie 1, nous avons vu comment engendrer un nouvel acteur «système». Mais les acteurs du système ne sont pas ce que nous sommes censés engendrer pour les entités (de domaine) normales. Nous devons générer un acteur utilisateur. Un acteur utilisateur ne peut être engendré que par un autre acteur. Il peut s’agir d’un acteur utilisateur ou d’un acteur système. Le modèle le plus courant à utiliser est SpawnProtocol
. SpawnProtocol est un comportement donné par Akka. Le motif est
- Utilisez le comportement du protocole d’apparition comme comportement de gardien. Cela créera un acteur système avec ce protocole.
- Envoyer un message d’apparition à cet acteur avec le comportement souhaité en tant que paramètre
- Cet acteur engendrera le nouvel acteur et reviendra acteur ref du nouvel acteur
SpawnProtocol()
Retour Behavior[Command]
. Le commandement est un trait qui n’a qu’un seul sous-type – Spawn[T]
. Cela signifie que lorsque vous utilisez ce comportement pour générer un acteur ou un système d’acteurs, le seul message que vous pouvez envoyer à l’acteur cible est Spawn[T]
.
T
représente le type de comportement que vous générerez à l’aide du protocole de génération. En d’autres termes, le «type de message» que votre acteur engendré acceptera après sa génération.
Puisque nous attendons une réponse (référence de l’acteur) de l’acteur gardien, nous devons utiliser le modèle de demande ici. Regardons le code.
Pour envoyer un message à bankAccount, nous pouvons utiliser forEach
méthode du futur.
Scala permet de réduire le code en utilisant des traits de soulignement (_
). Le soulignement dans ce contexte signifie x => x
et nous n’avons pas besoin de la notation lambda.
Pour ceux qui ont l’habitude de lambda depuis longtemps et qui sont nouveaux, cette notation de soulignement peut parfois prendre un certain temps pour s’habituer à cette syntaxe.
Pour demander un compte bancaire, nous devons flatMap
car ask renvoie également un future[T]
.
Si nous devons effectuer une série d’interactions séquentielles avec un compte bancaire, elles impliquent désormais une cartographie et une cartographie plate sur les futures.
Production:
Cette syntaxe est cependant très compliquée. Simplifions-le.
Utilisation de «For Comprehension» avec Futures
Scala prévoit des compréhensions. Ce sont du sucre syntaxique pour flatMap
et map
. Le code ci-dessus est simplifié si nous l’utilisons pour la compréhension.
Notez que pour la compréhension n’est pas seulement pour pour Future
fonctionne pour tous les types avec map
& flapMap
méthodes.
Pour les compréhensions résoudre un problème majeur en donnant accès à toutes les variables «résolues» aux expressions suivantes. Par exemple, dans le code précédent sans compréhension, nous devions écrire bankAccountFuture.forEach
deux fois et bankAccountFuture.flatMap
une fois, même après, nous savons que l’avenir est complet car ces opérations sont séquentielles. Nous aurions pu transporter les valeurs manuellement, mais cela aurait été un code super illisible et passe-partout. Voilà pourquoi. pour les compréhensions sont l’une de mes fonctionnalités de scala préférées.
Fin de la partie 2
Voici l’intégralité Main.scala
Contenu.
Tout le code du projet se trouve sur github repo sur la branche «part-2».