Le mode observateur , également connu sous le nom de mode publication / sous-être, a été proposé par le groupe Four-Person (GoF, à savoir Erich Gamma, Richard Helm, Ralph Johnson et John Vlissides) en 1994 "Design Pattern: The Basics of Reusable Object-Oriented Software" (voir pages 293-313 dans le livre pour plus de détails). Bien que ce modèle ait une histoire considérable, il est toujours largement applicable à une variété de scénarios et est même devenu une partie intégrante de la bibliothèque Java standard. Bien qu'il existe déjà de nombreux articles sur les modèles d'observateurs, ils se concentrent tous sur la mise en œuvre en Java, mais ignorent les divers problèmes rencontrés par les développeurs lors de l'utilisation de modèles d'observateurs en Java.
L'intention originale de rédiger cet article est de combler cette lacune: cet article présente principalement la mise en œuvre du modèle d'observateur en utilisant l'architecture Java8, et explore plus en détail les problèmes complexes sur les modèles classiques sur cette base, y compris les classes internes anonymes, les expressions lambda, la sécurité des fils et la mise en œuvre de l'observateur non trivial. Bien que le contenu de cet article ne soit pas complet, de nombreux problèmes complexes impliqués dans ce modèle ne peuvent pas être expliqués dans un seul article. Mais après avoir lu cet article, les lecteurs peuvent comprendre ce qu'est le modèle d'observateur, son universalité en Java et comment faire face à des problèmes communs lors de la mise en œuvre du modèle d'observateur en Java.
Mode observateur
Selon la définition classique proposée par GOF, le thème du modèle d'observateur est:
Définit une dépendance un à plusieurs entre les objets. Lorsque l'état d'un objet change, tous les objets qui en dépendent sont notifiés et mis à jour automatiquement.
Qu'est-ce que ça veut dire? Dans de nombreuses applications logicielles, les états entre les objets sont interdépendants. Par exemple, si une application se concentre sur le traitement numérique des données, ces données peuvent être affichées via des tables ou des graphiques de l'interface utilisateur graphique (GUI) ou utilisé en même temps, c'est-à-dire lorsque les données sous-jacentes sont mises à jour, les composants de l'interface graphique correspondants doivent également être mis à jour. La clé du problème est de savoir comment mettre à jour les données sous-jacentes lorsque les composants de l'interface graphique sont mis à jour, et en même temps minimiser le couplage entre les composants de l'interface graphique et les données sous-jacentes.
Une solution simple et non évaluable consiste à se référer aux composants de la table et de l'interface graphique des objets qui gèrent ces données sous-jacentes, afin que les objets puissent informer les composants de l'interface graphique lorsque les données sous-jacentes changent. De toute évidence, cette solution simple a rapidement montré ses lacunes pour des applications complexes qui gèrent plus de composants de l'interface graphique. Par exemple, il existe 20 composants GUI qui s'appuient tous sur des données sous-jacentes, de sorte que les objets qui gèrent les données sous-jacentes doivent maintenir les références à ces 20 composants. À mesure que le nombre d'objets dépendant des données connexes augmente, le degré de couplage entre la gestion des données et les objets devient difficile à contrôler.
Une autre meilleure solution consiste à permettre aux objets de s'inscrire pour obtenir des autorisations pour mettre à jour les données d'intérêt, ce que le gestionnaire de données informera ces objets lorsque les données changent. En termes simples, laissez l'objet de données d'intérêt indique au gestionnaire: "Veuillez m'informer lorsque les données changent." De plus, ces objets peuvent non seulement s'inscrire pour obtenir des notifications de mise à jour, mais également annuler l'enregistrement pour s'assurer que le gestionnaire de données n'informe plus l'objet lorsque les données changent. Dans la définition d'origine de GOF, l'objet enregistré pour obtenir des mises à jour est appelé "observateur", le gestionnaire de données correspondant est appelé "sujet", les données qui l'intéressent est appelée "État cible", le processus d'enregistrement est appelé "ADD" et le processus d'observation de l'annulation est appelé "détachage". Comme mentionné ci-dessus, le mode observateur est également appelé le mode publication-subscription. On peut comprendre qu'un client s'abonne à l'observateur sur la cible. Lorsque le statut cible est mis à jour, la cible publie ces mises à jour de l'abonné (ce modèle de conception est étendu à une architecture générale, appelée l'architecture de publication-abonnement). Ces concepts peuvent être représentés par le diagramme de classe suivant:
ConcerteObserver l'utilise pour recevoir des modifications de l'état de mise à jour et transmettre une référence à la concereTesubject à son constructeur. Cela fournit une référence à un sujet spécifique pour un observateur spécifique, à partir de laquelle les mises à jour peuvent être obtenues lorsque l'état change. En termes simples, l'observateur spécifique sera informé de mettre à jour le sujet et d'utiliser en même temps les références de son constructeur pour obtenir l'état du sujet spécifique, et enfin stocker ces objets d'état de recherche sous la propriété ObserverState de l'observateur spécifique. Ce processus est illustré dans le diagramme de séquence suivant:
Spécialisation des modèles classiques <br /> Bien que le modèle d'observateur soit universel, il existe également de nombreux modèles spécialisés, dont les plus courants sont les deux suivants:
1. Fournissez un paramètre à l'objet d'état et passez-le à la méthode de mise à jour appelée par l'observateur. En mode classique, lorsque l'observateur est informé que l'état du sujet a changé, son état mis à jour sera obtenu directement à partir du sujet. Cela nécessite que l'observateur enregistre une référence d'objet à l'état récupéré. Cela forme une référence circulaire, la référence de Concretsubject pointe vers sa liste d'observateurs, et la référence de ConcreteObserver pointe vers le concrètes qui peut obtenir l'état du sujet. En plus d'obtenir l'état mis à jour, il n'y a aucun lien entre l'observateur et le sujet qu'il enregistre à écouter. L'observateur se soucie de l'objet d'état, pas du sujet lui-même. C'est-à-dire que, dans de nombreux cas, ConcreteObserver et Concretsubject sont liés de force. Au contraire, lorsque Concretsubject appelle la fonction de mise à jour, l'objet d'état est transmis à ConcreteObserver et les deux n'ont pas besoin d'être associés. L'association entre ConcreteObserver et l'objet d'état réduit le degré de dépendance entre l'observateur et l'État (voir l'article de Martin Fowler pour plus de différences d'association et de dépendance).
2. Fusionner la classe abstraite du sujet et le concrètes en une classe de singlesubject. Dans la plupart des cas, l'utilisation de classes abstraites dans le sujet n'améliore pas la flexibilité et l'évolutivité du programme, donc la combinaison de cette classe abstraite et de la classe de béton simplifie la conception.
Une fois ces deux modèles spécialisés combinés, le diagramme de classe simplifié est le suivant:
Dans ces modèles spécialisés, la structure de classe statique est grandement simplifiée et les interactions entre les classes sont également simplifiées. Le diagramme de séquence à ce moment est le suivant:
Une autre caractéristique du mode de spécialisation est l'élimination de l'observable de la variable membre de ConcreteObserver. Parfois, l'observateur spécifique n'a pas besoin de sauvegarder le dernier état du sujet, mais n'a besoin que de surveiller l'état du sujet lorsque l'état est mis à jour. Par exemple, si l'observateur met à jour la valeur de la variable membre à la sortie standard, il peut supprimer l'ObserverState, qui supprime l'association entre le CONCRETOBSERVER et la classe d'état.
Règles de dénomination plus courantes <BR /> Modes classiques et même le mode professionnel mentionné ci-dessus utilisent des termes tels que l'attachement, le détachement et l'observateur, tandis que de nombreuses implémentations Java utilisent différents dictionnaires, y compris le registre, le désinscription, l'auditeur, etc. Il convient de mentionner que l'État est un terme général pour tous les objets que l'auditeur doit surveiller les changements. Le nom spécifique de l'objet d'état dépend du scénario utilisé en mode observateur. Par exemple, dans le mode observateur de la scène où l'auditeur écoute l'occurrence de l'événement, l'auditeur enregistré recevra une notification lorsque l'événement se produira. L'objet de statut pour le moment est un événement, c'est-à-dire si l'événement s'est produit.
Dans les applications réelles, la dénomination des cibles comprend rarement un sujet. Par exemple, créez une application sur un zoo, enregistrez plusieurs auditeurs pour observer la classe de zoo et recevez des notifications lorsque de nouveaux animaux entrent dans le zoo. L'objectif dans ce cas est la classe de zoo. Afin de maintenir la terminologie cohérente avec le domaine de problème donné, le terme "sujet" ne sera pas utilisé, ce qui signifie que la classe de zoo ne sera pas nommée Zoosubject.
La dénomination de l'auditeur est généralement suivie par le suffixe de l'auditeur. Par exemple, l'auditeur mentionné ci-dessus pour surveiller les nouveaux animaux sera nommé AnimalAddedListener. De même, la dénomination de fonctions telles que le registre, le non-enregistrement et le notifie est souvent suffixée par leurs noms d'écoute correspondants. Par exemple, le registre, le non-inscription et les fonctions de notification de AnimalAddedListener seront nommés registeranimalAddedListener, UnregisteranImalAddedListener et notifieranimaladdedListeners. Il convient de noter que le nom de fonction notifier est utilisé, car la fonction notification gère plusieurs écouteurs plutôt qu'un seul auditeur.
Cette méthode de dénomination semblera longue et un sujet enregistrera généralement plusieurs types d'auditeurs. Par exemple, dans l'exemple du zoo mentionné ci-dessus, dans le zoo, en plus d'enregistrer de nouveaux auditeurs pour surveiller les animaux, il doit également enregistrer un auditeur aux animaux pour réduire les auditeurs. À l'heure actuelle, il y aura deux fonctions de registre: (registeranimalAddedListener et registeranimalremovedListener. De cette façon, le type d'écouteur est utilisé comme qualificatif pour indiquer le type d'observateur. Une autre solution consiste à créer une fonction RegisterListener, puis à la surcharger, mais la solution 1 peut savoir plus commodement quel écouteur est écouté. La surcharge est une approche de Niche relative.
Une autre syntaxe idiomatique consiste à utiliser sur le préfixe au lieu de la mise à jour, par exemple, la fonction de mise à jour est nommée onanimaladded au lieu de updateanimaladded. Cette situation est plus courante lorsque l'auditeur obtient des notifications pour une séquence, comme l'ajout d'un animal à la liste, mais il est rarement utilisé pour mettre à jour des données distinctes, comme le nom de l'animal.
Ensuite, cet article utilisera les règles symboliques de Java. Bien que les règles symboliques ne modifieront pas la conception et la mise en œuvre réelles du système, il est un principe de développement important d'utiliser des termes que les autres développeurs connaissent, vous devez donc être familier avec le modèle d'observateur des règles symboliques dans Java décrites ci-dessus. Le concept ci-dessus sera expliqué ci-dessous en utilisant un exemple simple dans l'environnement Java 8.
Un exemple simple
C'est également l'exemple du zoo mentionné ci-dessus. Utiliser l'interface API de Java8 pour implémenter un système simple, expliquant les principes de base du modèle d'observateur. Le problème est décrit comme:
Créez un zoo système, permettant aux utilisateurs d'écouter et d'annuler l'état d'ajouter un nouvel animal d'objet, et de créer un auditeur spécifique, responsable de la sortie du nom du nouvel animal.
Selon l'apprentissage précédent du modèle d'observateur, nous savons que pour implémenter une telle application, nous devons créer 4 classes, en particulier:
Nous créons d'abord une classe animale, qui est un simple objet Java contenant des variables de membre, des constructeurs, des getters et des méthodes de setter. Le code est le suivant:
classe publique Animal {nom de chaîne privée; Animal public (nom de chaîne) {this.name = name; } public String getName () {return this.name; } public void setName (string name) {this.name = name; }}Utilisez cette classe pour représenter des objets animaux, puis vous pouvez créer l'interface AnimalAddedListener:
Interface publique AnimalAddededListener {public void onanimaladded (animal animal);}Les deux premières classes sont très simples, donc je ne les présenterai pas en détail. Ensuite, créez la classe de zoo:
classe publique zoo {List privé <Animal> Animals = new ArrayList <> (); Liste privée <AnimalAddedListener> écouteurs = new ArrayList <> (); Addanimal du public public (animal animal) {// ajouter l'animal à la liste des animaux this.animals.add (animal); // informer la liste des auditeurs enregistrés ceci.NotifyAnimalAddedDisteners (Animal); } public void registeranimalAddedListener (AnimalAddedListener Écouteur) {// Ajouter l'auditeur à la liste des auditeurs enregistrés this.Listeners.add (auditeur); } public void UnregistanImalAddedListener (AnimalAddedListener Écouteur) {// Supprimer l'auditeur de la liste des auditeurs enregistrés this.Listeners.Remove (auditeur); } Void Protected NotifyAnimalAddedListeners (Animal Animal) {// Notifier chacun des auditeurs de la liste des auditeurs enregistrés auditeurs this.Listeners.Foreach (auditeur -> auditeur.Updateanimaladded (animal)); }}Cette analogie est complexe que les deux précédents. Il contient deux listes, l'une est utilisée pour stocker tous les animaux du zoo et l'autre est utilisé pour stocker tous les auditeurs. Étant donné que les objets stockés chez les animaux et les collections d'auditeur sont simples, cet article a choisi ArrayList pour le stockage. La structure de données spécifique de l'auditeur stocké dépend du problème. Par exemple, pour le problème du zoo ici, si l'auditeur a la priorité, vous devez choisir une autre structure de données ou réécrire l'algorithme de registre de l'auditeur.
La mise en œuvre de l'enregistrement et de la suppression est à la fois une méthode de délégué simple: chaque écouteur est ajouté ou supprimé de la liste d'écoute de l'écoute en tant que paramètre. La mise en œuvre de la fonction Notifie est légèrement éteinte par rapport au format standard du modèle d'observateur. Il comprend le paramètre d'entrée: l'animal nouvellement ajouté, afin que la fonction de notification puisse passer la référence animale nouvellement ajoutée à l'auditeur. Utilisez la fonction foreach de l'API Streams pour traverser les écouteurs et exécuter la fonction Theonanimaladded sur chaque écouteur.
Dans la fonction Addanimal, l'objet animal et l'écoute nouvellement ajouté sont ajoutés à la liste correspondante. Si la complexité du processus de notification n'est pas prise en compte, cette logique doit être incluse dans une méthode d'appel pratique. Il vous suffit de passer dans une référence à l'objet animal nouvellement ajouté. C'est pourquoi l'implémentation logique de l'auditeur de notification est encapsulée dans la fonction NotifyanimalAddedDisteners, qui est également mentionnée dans l'implémentation d'addanimal.
En plus des problèmes logiques de notification des fonctions, il est nécessaire de souligner la question controversée sur la visibilité de la notification des fonctions. Dans le modèle d'observateur classique, comme GOF l'a dit à la page 301 des modèles de conception du livre, la fonction de notification est publique, mais bien que utilisée dans le modèle classique, cela ne signifie pas qu'il doit être public. La sélection de la visibilité doit être basée sur l'application. Par exemple, dans l'exemple du zoo de cet article, la fonction de notification est de type protégé et ne nécessite pas que chaque objet lance une notification d'un observateur enregistré. Il suffit de garantir que l'objet peut hériter de la fonction de la classe parent. Bien sûr, ce n'est pas exactement le cas. Il est nécessaire de déterminer quelles classes peuvent activer la fonction de notification, puis de déterminer la visibilité de la fonction.
Ensuite, vous devez implémenter la classe PrintNameAnimalAddedListener. Cette classe utilise la méthode System.out.println pour sortir le nom du nouvel animal. Le code spécifique est le suivant:
classe publique PrintNameanImalAddedListener implémente AnimalAddedListener {@Override public void updateanimaladded (animal animal) {// imprime le nom du Système animal nouvellement ajouté.out.println ("a ajouté un nouvel animal avec nom '" + animal.getname () + "'"); }}Enfin, nous devons implémenter la fonction principale qui pilote l'application:
classe publique Main {public static void main (String [] args) {// Créer le zoo pour stocker les animaux zoo zoo = new zoo (); // Enregistrez un auditeur à notifier lorsqu'un animal est ajouté zoo.RegisteRanImalAddedListener (new PrintNameAnimalAddedListener ()); // Ajouter un animal informer les auditeurs enregistrés zoo.addanimal (nouvel animal ("Tiger")); }}La fonction principale crée simplement un objet zoo, enregistre un auditeur qui sortira le nom de l'animal et crée un nouvel objet animal pour déclencher l'auditeur enregistré. La sortie finale est:
Ajout d'un nouvel animal avec nom «tigre»
Écouteur ajouté
Les avantages du mode observateur sont entièrement affichés lorsque l'auditeur est rétabli et ajouté au sujet. Par exemple, si vous souhaitez ajouter un auditeur qui calcule le nombre total d'animaux dans un zoo, il vous suffit de créer une classe d'écoute spécifique et de l'enregistrer auprès de la classe de zoo sans aucune modification de la classe de zoo. L'ajout du code de comptage de l'écoute compteuranImalAddedDistener est le suivant:
classe publique CountinganimalAddedListener implémente AnimalAddedListener {private static int animauxAddedCount = 0; @Override public void updateanimaladded (animal animal) {// incrément le nombre d'animaux AnimalAddedCount ++; // Imprime le nombre d'animaux System.out.println ("Total Animals Ajout:" + AnimalAddedCount); }}La fonction principale modifiée est la suivante:
classe publique Main {public static void main (String [] args) {// Créer le zoo pour stocker les animaux zoo zoo = new zoo (); // Enregistrer les auditeurs à notifier lorsqu'un animal est ajouté zoo.RegisteranImalAddedListener (new PrintNameAnimalAddedListener ()); zoo.registeranimalAddedListener (new CountinganImalAddedListener ()); // Ajouter un animal informer les auditeurs enregistrés zoo.addanimal (nouvel animal ("Tiger")); zoo.addanimal (nouvel animal ("lion")); zoo.addanimal (nouvel animal ("ours")); }}Le résultat de la sortie est:
Ajout d'un nouvel animal avec nom 'Tiger'total Animals Ajout: 1 ajout d'un nouvel animal avec un nom' lion'total Animaux ajoutés: 2Added un nouvel animal avec un nom "Bear'total Animaux ajoutés: 3
L'utilisateur peut créer n'importe quel écouteur si vous ne modifiez que le code d'enregistrement de l'écoute. Cette évolutivité est principalement due au sujet associé à l'interface d'observateur, plutôt que directement associée au bétono-serveur. Tant que l'interface n'est pas modifiée, il n'est pas nécessaire de modifier le sujet de l'interface.
Cours internes anonymes, fonctions lambda et enregistrement de l'auditeur
Une amélioration majeure de Java 8 est l'ajout de caractéristiques fonctionnelles, telles que l'ajout de fonctions lambda. Avant d'introduire la fonction lambda, Java a fourni des fonctions similaires via des classes internes anonymes, qui sont toujours utilisées dans de nombreuses applications existantes. En mode observateur, un nouvel écouteur peut être créé à tout moment sans créer une classe d'observateurs spécifique. Par exemple, la classe PrintNameAnimalAddedDistener peut être implémentée dans la fonction principale avec une classe interne anonyme. Le code d'implémentation spécifique est le suivant:
classe publique Main {public static void main (String [] args) {// Créer le zoo pour stocker les animaux zoo zoo = new zoo (); // Enregistrer les auditeurs à avertir lorsqu'un animal est ajouté zoo.RegisteranimalAddedListener (New AnimalAddedListener () {@Override public void updateanimaladded (Animal Animal) {// Imprimez le nom de la nouvelle Animal System.out.println ("Ajout d'un nouvel animal avec nom '" + animal.getname () + "" ");}); // Ajouter un animal informer les auditeurs enregistrés zoo.addanimal (nouvel animal ("Tiger")); }}De même, les fonctions de lambda peuvent également être utilisées pour effectuer ces tâches:
classe publique Main {public static void main (String [] args) {// Créer le zoo pour stocker les animaux zoo zoo = new zoo (); Cette // Ajouter un animal informer les auditeurs enregistrés zoo.addanimal (nouvel animal ("Tiger")); }}Il convient de noter que la fonction lambda ne convient qu'aux situations où il n'y a qu'une seule fonction dans l'interface de l'écoute. Bien que cette exigence semble stricte, de nombreux auditeurs sont en fait des fonctions uniques, comme le AnimalAddedListener dans l'exemple. Si l'interface dispose de plusieurs fonctions, vous pouvez choisir d'utiliser des classes intérieures anonymes.
Il y a un tel problème avec l'enregistrement implicite de l'auditeur créé: puisque l'objet est créé dans le cadre de l'appel d'enregistrement, il est impossible de stocker une référence à un écouteur spécifique. Cela signifie que les auditeurs enregistrés via des fonctions lambda ou des classes internes anonymes ne peuvent pas être révoquées car les fonctions de révocation nécessitent une référence à l'auditeur enregistré. Un moyen facile de résoudre ce problème consiste à renvoyer une référence à l'auditeur enregistré dans la fonction RegisteranimalAddedListener. De cette façon, vous pouvez désinscrire l'auditeur créé avec des fonctions lambda ou des classes internes anonymes. Le code de méthode amélioré est le suivant:
public AnimalAddedListener RegisteranImalAddedListener (AnimalAddedListener Écouteur) {// Ajouter l'auditeur à la liste des auditeurs enregistrés this.Listeners.add (auditeur); retourner l'auditeur;}Le code client pour l'interaction de fonction redessinée est le suivant:
classe publique Main {public static void main (String [] args) {// Créer le zoo pour stocker les animaux zoo zoo = new zoo (); // Enregistrer les auditeurs à avertir lorsqu'un animal est ajouté AnimalAddedListener auditeur = zoo.RegisteranimalAddedListener ((Animal) -> System.out.println ("Ajout d'un nouvel animal avec nom '" + animal.getName () + "'")); // Ajouter un animal informer les auditeurs enregistrés zoo.addanimal (nouvel animal ("Tiger")); // Dépister à l'écoute zoo.unregisteranimaladdedListener (écouteur); // Ajouter un autre animal, qui n'imprimera pas le nom, car l'écouteur // a déjà été non enregistré Zoo.addanimal (nouvel animal ("lion")); }}La sortie du résultat pour le moment ne s'ajoute qu'à un nouvel animal avec le nom «Tiger», car l'auditeur a été annulé avant l'ajout du deuxième animal:
Ajout d'un nouvel animal avec nom «tigre»
Si une solution plus complexe est adoptée, la fonction de registre peut également renvoyer la classe de récepteur afin que l'auditeur non enregistré soit appelé, par exemple:
classe publique AnimalAddedListeneReceipt {Écouteur privé AnimalAddedListener; public AnimalAddedListeNerReceipt (AnimalAddedListener Écouteur) {this.Listener = écouteur; } public final AnimalAddedListener getListener () {return this.Listener; }}Le réception sera utilisé car la valeur de retour de la fonction d'enregistrement et les paramètres d'entrée de la fonction d'enregistrement sont annulés. Pour le moment, l'implémentation du zoo est la suivante:
classe publique ZoousingReceipt {// ... Attributs et constructeurs existants ... public AnimalAddedListeNerreceipt RegisterAnimalAddedListener (AnimalAddedListener Écouteur) {// Ajouter l'éditeur à la liste des auditeurs enregistrés this.liteners.add (auteur); Retourne New AnimalAddedListeneReceipt (auditeur); } public void UnregistanImalAddedListener (AnimalAddedListeNerreceipt réception) {// Supprimez l'auditeur de la liste des auditeurs enregistrés this.Listeners.Remove (receipt.getListener ()); } // ... Méthode de notification existante ...}Le mécanisme de mise en œuvre de réception décrit ci-dessus permet le stockage d'informations pour appeler à l'auditeur lors de la révocation, c'est-à-dire, si l'algorithme d'enregistrement de révocation dépend de l'état de l'auditeur lorsque le sujet enregistre l'auditeur, ce statut sera enregistré. Si l'enregistrement de révocation ne nécessite qu'une référence à l'auditeur enregistré précédent, la technologie de réception semblera gênante et n'est pas recommandée.
En plus des auditeurs spécifiques particulièrement complexes, la façon la plus courante d'enregistrer les auditeurs est via des fonctions lambda ou via des classes internes anonymes. Bien sûr, il existe des exceptions, c'est-à-dire que la classe qui contient le sujet implémente l'interface d'observateur et enregistre un auditeur qui appelle la cible de référence. Le cas comme indiqué dans le code suivant:
La classe publique Zoocontainer implémente AnimalAddedListener {private zoo zoo = new zoo (); public zoocontainer () {// Enregistrez cet objet en tant qu'auditeur this.zoo.registeranimalAddedListener (this); } public zoo getzoo () {return this.zoo; } @Override public void updateanimalAdded (animal animal) {System.out.println ("Animal ajouté avec nom '" + animal.getName () + "'"); } public static void main (String [] args) {// Créer le conteneur zoo zoocontainer zoocontainer = new zoocontainer (); // Ajouter un animal avertir l'écouteur intérieur intérieur zoocontainer.getzoo (). Addanimal (nouvel animal ("tigre")); }}Cette approche ne convient qu'aux cas simples et le code ne semble pas assez professionnel, et il est toujours très populaire auprès des développeurs Java modernes, il est donc nécessaire de comprendre comment cet exemple fonctionne. Étant donné que Zoocontainer implémente l'interface AnimalAddedListener, alors une instance (ou un objet) de zoocontainer peut être enregistrée comme AnimalAddedListener. Dans la classe Zoocontainer, cette référence représente une instance de l'objet actuel, à savoir le zoocontainer, et peut être utilisé comme AnimalAddedListener.
Généralement, toutes les classes de conteneurs ne sont pas nécessaires pour implémenter de telles fonctions, et la classe de conteneurs qui implémente l'interface de l'écoute ne peut appeler la fonction d'enregistrement du sujet, mais simplement passer la référence à la fonction de registre comme objet d'écoute. Dans les chapitres suivants, les FAQ et les solutions pour les environnements multithread seront introduits.
Implémentation de la sécurité des threads <BR /> Le chapitre précédent présente la mise en œuvre du modèle d'observateur dans l'environnement Java moderne. Bien qu'il soit simple mais complet, cette implémentation ignore un problème clé: la sécurité des threads. La plupart des applications Java ouvertes sont multipliées et le mode observateur est principalement utilisé dans les systèmes multi-thread ou asynchrones. Par exemple, si un service externe met à jour sa base de données, l'application recevra également un message de manière asynchrone, puis informera le composant interne pour mettre à jour en mode observateur, au lieu d'enregistrer et d'écouter directement le service externe.
La sécurité des threads en mode observateur est principalement axée sur le corps du mode, car les conflits de threads sont susceptibles de modifier la collection d'écoute enregistrée. Par exemple, un thread essaie d'ajouter un nouvel auditeur, tandis que l'autre thread essaie d'ajouter un nouvel objet animal, qui déclenche des notifications à tous les auditeurs enregistrés. Compte tenu de l'ordre de séquence, le premier thread peut avoir terminé l'enregistrement du nouvel écouteur avant que l'auditeur enregistré ne reçoive la notification de l'animal ajouté. Il s'agit d'un cas classique de concours de ressources de fil, et c'est ce phénomène qui indique aux développeurs qu'ils ont besoin d'un mécanisme pour assurer la sécurité des filetages.
La solution la plus simple à ce problème est: toutes les opérations qui accèdent ou modifient la liste des auditeurs d'enregistrement doivent suivre le mécanisme de synchronisation Java, tel que:
Public synchronisé AnimalAddedListener RegisteranimalAddedListener (AnimalAddedListener Écouteur) {/*...*/} public synchronisé void UnregistanImalAddedListener (AnimalAddedener ÉcoucteurDe cette façon, en même temps, un seul thread peut modifier ou accéder à la liste des auditeurs enregistrés, qui peuvent réussir à éviter les problèmes de concurrence des ressources, mais de nouveaux problèmes surviennent, et ces contraintes sont trop strictes (pour plus d'informations sur les mots clés synchronisés et les modèles de concurrence Java, veuillez vous référer à la page Web officielle). Grâce à la synchronisation des méthodes, un accès simultané à la liste des auditeurs peut être observé à tout moment. L'enregistrement et la révocation de l'auditeur est une opération d'écriture pour la liste des auditeurs, tout en notifiant l'auditeur pour accéder à la liste des auditeurs est une opération en lecture seule. Étant donné que l'accès via la notification est une opération de lecture, plusieurs opérations de notification peuvent être effectuées simultanément.
Par conséquent, tant qu'il n'y a pas d'enregistrement ou de révocation de l'auditeur, tant que l'enregistrement n'est pas enregistré, tant qu'un certain nombre de notifications concurrentes peuvent être exécutées simultanément sans déclencher une concurrence de ressources pour la liste des auditeurs enregistrés. Bien sûr, le concours de ressources dans d'autres situations existe depuis longtemps. Afin de résoudre ce problème, le verrouillage des ressources pour ReadWriteLock est conçu pour gérer les opérations de lecture et d'écriture séparément. Le code d'implémentation ThreadSaFezOO à filetage de la classe de zoo est le suivant:
classe publique ThreadSaFEZOO {private final readWriteLock readWriteLock = new ReentRanTreadWriteLock (); Lock final protégé readlock = readWriteLock.readlock (); Lock final protégé writeLock = readWriteLock.WriteLock (); Liste privée <Animal> Animals = New ArrayList <> (); Liste privée <AnimalAddedListener> écouteurs = new ArrayList <> (); Addanimal du public public (animal animal) {// ajouter l'animal à la liste des animaux this.animals.add (animal); // informer la liste des auditeurs enregistrés ceci.NotifyAnimalAddedDisteners (Animal); } public AnimalAddedListener RegisteranImalAddedListener (AnimalAddedListener Écouteur) {// Verrouillez la liste des auditeurs pour écrire this.writelock.lock (); essayez {// ajouter l'auditeur à la liste des auditeurs enregistrés this.Listeners.add (écouteur); } Enfin {// Déverrouillez l'écrivain Lock This.WriteLock.unlock (); } return auditeur; } public void UnregistanImalAddedListener (AnimalAddedListener Écouteur) {// Verrouillez la liste des auditeurs pour écrire ce.writeLock.lock (); Essayez {// Supprimez l'auditeur de la liste des auditeurs enregistrés this.Listeners.Remove (écouteur); } Enfin {// Déverrouillez l'écrivain Lock This.WriteLock.unlock (); }} public void notifyanimaladdedListeners (animal animal) {// verrouiller la liste des auditeurs pour lire this.readlock.lock (); essayez {// informer chacun des auditeurs de la liste des auditeurs enregistrés this.Listeners.ForEach (auditeur -> écouteur.UpdateAnimalAdded (animal)); } Enfin {// Déverrouiller le lecteur Lock This.readlock.unlock (); }}}Grâce à un tel déploiement, la mise en œuvre du sujet peut garantir la sécurité des threads et plusieurs threads peuvent émettre des notifications en même temps. Mais malgré cela, il y a encore deux problèmes de concurrence en matière de ressources qui ne peuvent pas être ignorés:
Accès simultané à chaque auditeur. Plusieurs threads peuvent informer l'auditeur que de nouveaux animaux sont nécessaires, ce qui signifie qu'un auditeur peut être appelé par plusieurs threads en même temps.
Accès simultané à la liste des animaux. Plusieurs threads peuvent ajouter des objets à la liste des animaux en même temps. Si l'ordre des notifications a un impact, cela peut conduire à une concurrence sur les ressources, ce qui nécessite un mécanisme de traitement des opérations simultanées pour éviter ce problème. Si la liste des auditeurs enregistrés reçoit une notification pour ajouter Animal2, puis reçoit une notification pour ajouter Animal1, une compétition de ressources se produira. Cependant, si l'ajout d'Animal1 et Anims2 est effectué par différents fils, il est également possible de compléter l'ajout d'Animal1 avant Anims2. Plus précisément, Thread 1 ajoute Animal1 avant de notifier l'auditeur et verrouille le module, Thread 2 ajoute Animal2 et informe l'auditeur, puis le thread 1 informe l'auditeur qu'Animal1 a été ajouté. Bien que la concurrence des ressources puisse être ignorée lorsque l'ordre de séquence n'est pas pris en compte, le problème est réel.
对监听器的并发访问
并发访问监听器可以通过保证监听器的线程安全来实现。秉承着类的“责任自负”精神,监听器有“义务”确保自身的线程安全。例如,对于前面计数的监听器,多线程的递增或递减动物数量可能导致线程安全问题,要避免这一问题,动物数的计算必须是原子操作(原子变量或方法同步),具体解决代码如下:
public class ThreadSafeCountingAnimalAddedListener implements AnimalAddedListener { private static AtomicLong animalsAddedCount = new AtomicLong(0); @Override public void updateAnimalAdded (Animal animal) { // Increment the number of animals animalsAddedCount.incrementAndGet(); // Print the number of animals System.out.println("Total animals added: " + animalsAddedCount); }}方法同步解决方案代码如下:
public class CountingAnimalAddedListener implements AnimalAddedListener { private static int animalsAddedCount = 0; @Override public synchronized void updateAnimalAdded (Animal animal) { // Increment the number of animals animalsAddedCount++; // Print the number of animals System.out.println("Total animals added: " + animalsAddedCount); }}要强调的是监听器应该保证自身的线程安全,subject需要理解监听器的内部逻辑,而不是简单确保对监听器的访问和修改的线程安全。否则,如果多个subject共用同一个监听器,那每个subject类都要重写一遍线程安全的代码,显然这样的代码不够简洁,因此需要在监听器类内实现线程安全。
监听器的有序通知当要求监听器有序执行时,读写锁就不能满足需求了,而需要引入一个新的机制,可以保证notify函数的调用顺序和animal添加到zoo的顺序一致。有人尝试过用方法同步来实现,然而根据Oracle文档中的方法同步介绍,可知方法同步并不提供操作执行的顺序管理。它只是保证原子操作,也就是说操作不会被打断,并不能保证先来先执行(FIFO)的线程顺序。ReentrantReadWriteLock可以实现这样的执行顺序,代码如下:
public class OrderedThreadSafeZoo { private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(true); protected final Lock readLock = readWriteLock.readLock(); protected final Lock writeLock = readWriteLock.writeLock(); private List<Animal> animals = new ArrayList<>(); private List<AnimalAddedListener> listeners = new ArrayList<>(); public void addAnimal (Animal animal) { // Add the animal to the list of animals this.animals.add(animal); // Notify the list of registered listeners this.notifyAnimalAddedListeners(animal); } public AnimalAddedListener registerAnimalAddedListener (AnimalAddedListener listener) { // Lock the list of listeners for writing this.writeLock.lock(); try { // Add the listener to the list of registered listeners this.listeners.add(listener); } finally { // Unlock the writer lock this.writeLock.unlock(); } return listener; } public void unregisterAnimalAddedListener (AnimalAddedListener listener) { // Lock the list of listeners for writing this.writeLock.lock(); try { // Remove the listener from the list of the registered listeners this.listeners.remove(listener); } finally { // Unlock the writer lock this.writeLock.unlock(); } } public void notifyAnimalAddedListeners (Animal animal) { // Lock the list of listeners for reading this.readLock.lock(); try { // Notify each of the listeners in the list of registered listeners this.listeners.forEach(listener -> listener.updateAnimalAdded(animal)); } finally { // Unlock the reader lock this.readLock.unlock(); }}}这样的实现方式,register, unregister和notify函数将按照先进先出(FIFO)的顺序获得读写锁权限。例如,线程1注册一个监听器,线程2在开始执行注册操作后试图通知已注册的监听器,线程3在线程2等待只读锁的时候也试图通知已注册的监听器,采用fair-ordering方式,线程1先完成注册操作,然后线程2可以通知监听器,最后线程3通知监听器。这样保证了action的执行顺序和开始顺序一致。
如果采用方法同步,虽然线程2先排队等待占用资源,线程3仍可能比线程2先获得资源锁,而且不能保证线程2比线程3先通知监听器。问题的关键所在:fair-ordering方式可以保证线程按照申请资源的顺序执行。读写锁的顺序机制很复杂,应参照ReentrantReadWriteLock的官方文档以确保锁的逻辑足够解决问题。
截止目前实现了线程安全,在接下来的章节中将介绍提取主题的逻辑并将其mixin类封装为可重复代码单元的方式优缺点。
主题逻辑封装到Mixin类<br />把上述的观察者模式设计实现封装到目标的mixin类中很具吸引力。通常来说,观察者模式中的观察者包含已注册的监听器的集合;负责注册新的监听器的register函数;负责撤销注册的unregister函数和负责通知监听器的notify函数。对于上述的动物园的例子,zoo类除动物列表是问题所需外,其他所有操作都是为了实现主题的逻辑。
Mixin类的案例如下所示,需要说明的是为使代码更为简洁,此处去掉关于线程安全的代码:
public abstract class ObservableSubjectMixin<ListenerType> { private List<ListenerType> listeners = new ArrayList<>(); public ListenerType registerListener (ListenerType listener) { // Add the listener to the list of registered listeners this.listeners.add(listener); return listener; } public void unregisterAnimalAddedListener (ListenerType listener) { // Remove the listener from the list of the registered listeners this.listeners.remove(listener); } public void notifyListeners (Consumer<? super ListenerType> algorithm) { // Execute some function on each of the listeners this.listeners.forEach(algorithm); }}正因为没有提供正在注册的监听器类型的接口信息,不能直接通知某个特定的监听器,所以正需要保证通知功能的通用性,允许客户端添加一些功能,如接受泛型参数类型的参数匹配,以适用于每个监听器,具体实现代码如下:
public class ZooUsingMixin extends ObservableSubjectMixin<AnimalAddedListener> { private List<Animal> animals = new ArrayList<>(); public void addAnimal (Animal animal) { // Add the animal to the list of animals this.animals.add(animal); // Notify the list of registered listeners this.notifyListeners((listener) -> listener.updateAnimalAdded(animal)); }}Mixin类技术的最大优势是把观察者模式的Subject封装到一个可重复调用的类中,而不是在每个subject类中都重复写这些逻辑。此外,这一方法使得zoo类的实现更为简洁,只需要存储动物信息,而不用再考虑如何存储和通知监听器。
然而,使用mixin类并非只有优点。比如,如果要存储多个类型的监听器怎么办?例如,还需要存储监听器类型AnimalRemovedListener。mixin类是抽象类,Java中不能同时继承多个抽象类,而且mixin类不能改用接口实现,这是因为接口不包含state,而观察者模式中state需要用来保存已经注册的监听器列表。
其中的一个解决方案是创建一个动物增加和减少时都会通知的监听器类型ZooListener,代码如下所示:
public interface ZooListener { public void onAnimalAdded (Animal animal); public void onAnimalRemoved (Animal animal);}这样就可以使用该接口实现利用一个监听器类型对zoo状态各种变化的监听了:
public class ZooUsingMixin extends ObservableSubjectMixin<ZooListener> { private List<Animal> animals = new ArrayList<>(); public void addAnimal (Animal animal) { // Add the animal to the list of animals this.animals.add(animal); // Notify the list of registered listeners this.notifyListeners((listener) -> listener.onAnimalAdded(animal)); } public void removeAnimal (Animal) animal) { // Remove the animal from the list of animals this.animals.remove(animal); // Notify the list of registered listeners this.notifyListeners((listener) -> listener.onAnimalRemoved(animal)); }}将多个监听器类型合并到一个监听器接口中确实解决了上面提到的问题,但仍旧存在不足之处,接下来的章节会详细讨论。
Multi-Method监听器和适配器
在上述方法,监听器的接口中实现的包含太多函数,接口就过于冗长,例如,Swing MouseListener就包含5个必要的函数。尽管可能只会用到其中一个,但是只要用到鼠标点击事件就必须要添加这5个函数,更多可能是用空函数体来实现剩下的函数,这无疑会给代码带来不必要的混乱。
其中一种解决方案是创建适配器(概念来自GoF提出的适配器模式),适配器中以抽象函数的形式实现监听器接口的操作,供具体监听器类继承。这样一来,具体监听器类就可以选择其需要的函数,对adapter不需要的函数采用默认操作即可。例如上面例子中的ZooListener类,创建ZooAdapter(Adapter的命名规则与监听器一致,只需要把类名中的Listener改为Adapter即可),代码如下:
public class ZooAdapter implements ZooListener { @Override public void onAnimalAdded (Animal animal) {} @Override public void onAnimalRemoved (Animal animal) {}}乍一看,这个适配器类微不足道,然而它所带来的便利却是不可小觑的。比如对于下面的具体类,只需选择对其实现有用的函数即可:
public class NamePrinterZooAdapter extends ZooAdapter { @Override public void onAnimalAdded (Animal animal) { // Print the name of the animal that was added System.out.println("Added animal named " + animal.getName()); }}有两种替代方案同样可以实现适配器类的功能:一是使用默认函数;二是把监听器接口和适配器类合并到一个具体类中。默认函数是Java8新提出的,在接口中允许开发者提供默认(防御)的实现方法。
Java库的这一更新主要是方便开发者在不改变老版本代码的情况下,实现程序扩展,因此应该慎用这个方法。部分开发者多次使用后,会感觉这样写的代码不够专业,而又有开发者认为这是Java8的特色,不管怎样,需要明白这个技术提出的初衷是什么,再结合具体问题决定是否要用。使用默认函数实现的ZooListener接口代码如下示:
public interface ZooListener { default public void onAnimalAdded (Animal animal) {} default public void onAnimalRemoved (Animal animal) {}}通过使用默认函数,实现该接口的具体类,无需在接口中实现全部函数,而是选择性实现所需函数。虽然这是接口膨胀问题一个较为简洁的解决方案,开发者在使用时还应多加注意。
第二种方案是简化观察者模式,省略了监听器接口,而是用具体类实现监听器的功能。比如ZooListener接口就变成了下面这样:
public class ZooListener { public void onAnimalAdded (Animal animal) {} public void onAnimalRemoved (Animal animal) {}}这一方案简化了观察者模式的层次结构,但它并非适用于所有情况,因为如果把监听器接口合并到具体类中,具体监听器就不可以实现多个监听接口了。例如,如果AnimalAddedListener和AnimalRemovedListener接口写在同一个具体类中,那么单独一个具体监听器就不可以同时实现这两个接口了。此外,监听器接口的意图比具体类更显而易见,很显然前者就是为其他类提供接口,但后者就并非那么明显了。
如果没有合适的文档说明,开发者并不会知道已经有一个类扮演着接口的角色,实现了其对应的所有函数。此外,类名不包含adapter,因为类并不适配于某一个接口,因此类名并没有特别暗示此意图。综上所述,特定问题需要选择特定的方法,并没有哪个方法是万能的。
在开始下一章前,需要特别提一下,适配器在观察模式中很常见,尤其是在老版本的Java代码中。Swing API正是以适配器为基础实现的,正如很多老应用在Java5和Java6中的观察者模式中所使用的那样。zoo案例中的监听器或许并不需要适配器,但需要了解适配器提出的目的以及其应用,因为我们可以在现有的代码中对其进行使用。下面的章节,将会介绍时间复杂的监听器,该类监听器可能会执行耗时的运算或进行异步调用,不能立即给出返回值。
Complex & Blocking监听器关于观察者模式的一个假设是:执行一个函数时,一系列监听器会被调用,但假定这一过程对调用者而言是完全透明的。例如,客户端代码在Zoo中添加animal时,在返回添加成功之前,并不知道会调用一系列监听器。如果监听器的执行需要时间较长(其时间受监听器的数量、每个监听器执行时间影响),那么客户端代码将会感知这一简单增加动物操作的时间副作用。
本文不能面面俱到的讨论这个话题,下面几条是开发者调用复杂的监听器时应该注意的事项:
监听器启动新线程。新线程启动后,在新线程中执行监听器逻辑的同时,返回监听器函数的处理结果,并运行其他监听器执行。
Subject启动新线程。与传统的线性迭代已注册的监听器列表不同,Subject的notify函数重启一个新的线程,然后在新线程中迭代监听器列表。这样使得notify函数在执行其他监听器操作的同时可以输出其返回值。需要注意的是需要一个线程安全机制来确保监听器列表不会进行并发修改。
队列化监听器调用并采用一组线程执行监听功能。将监听器操作封装在一些函数中并队列化这些函数,而非简单的迭代调用监听器列表。这些监听器存储到队列中后,线程就可以从队列中弹出单个元素并执行其监听逻辑。这类似于生产者-消费者问题,notify过程产生可执行函数队列,然后线程依次从队列中取出并执行这些函数,函数需要存储被创建的时间而非执行的时间供监听器函数调用。例如,监听器被调用时创建的函数,那么该函数就需要存储该时间点,这一功能类似于Java中的如下操作:
public class
如何使用Java8 实现观察者模式?相信通过这篇文章大家都有了大概的了解了吧!