Режим наблюдателя , также известный как режим публикации/подписки, был предложен группой из четырех человек (GOF, а именно Эрихом Гаммой, Ричардом Хелмом, Ральфом Джонсоном и Джоном Влиссис) в «Образец дизайна 1994 года: Основы многоразового программного обеспечения, ориентированного на объект» (см. Страницы 293-313 в книге для деталей). Хотя этот шаблон имеет значительную историю, он все еще широко применим к различным сценариям и даже стал неотъемлемой частью стандартной библиотеки Java. Хотя уже есть много статей о моделях наблюдателей, все они сосредоточены на реализации в Java, но игнорируют различные проблемы, с которыми сталкиваются разработчики при использовании моделей наблюдателей в Java.
Первоначальное намерение написания этой статьи состоит в том, чтобы заполнить этот пробел: эта статья в основном вводит реализацию шаблона наблюдателя с использованием архитектуры Java8, а также исследует сложные вопросы о классических шаблонах на этой основе, включая анонимные внутренние классы, выражения Lambda, безопасность потоков и нетривиальную реализацию наблюдателя. Хотя содержание этой статьи не является всеобъемлющим, многие из сложных вопросов, связанных с этой моделью, не могут быть объяснены только в одной статье. Но после прочтения этой статьи читатели могут понять, что такое шаблон наблюдателя, его универсальность в Java и как решать некоторые общие проблемы при реализации шаблона наблюдателя в Java.
Режим наблюдателя
Согласно классическому определению, предложенному GOF, тема рисунка наблюдателя:
Определяет зависимость от одного ко многим между объектами. Когда состояние объекта меняется, все объекты, которые зависят от него, уведомляются и автоматически обновляются.
Что это значит? Во многих программных приложениях состояния между объектами взаимозависимы. Например, если приложение фокусируется на численной обработке данных, эти данные могут отображаться через таблицы или диаграммы графического пользовательского интерфейса (GUI) или использовать одновременно, то есть, когда базовые данные обновляются, соответствующие компоненты GUI также должны быть обновлены. Ключом к проблеме является то, как обновить базовые данные, когда компоненты графического интерфейса обновляются, и в то же время минимизировать связь между компонентами GUI и основными данными.
Простое и не масштабируемое решение состоит в том, чтобы ссылаться на таблицу и компоненты графического интерфейса изображений объектов, которые управляют этими основными данными, чтобы объекты могли уведомлять компоненты графического интерфейса при изменении основных данных. Очевидно, что это простое решение быстро показало свои недостатки для сложных приложений, которые обрабатывают больше компонентов графического интерфейса. Например, существует 20 компонентов графического интерфейса, которые все полагаются на основные данные, поэтому объекты, которые управляют основными данными, должны поддерживать ссылки на эти 20 компонентов. По мере увеличения количества объектов, зависящих от связанных данных, степень связи между управлением данными и объектами становится трудно контролировать.
Другое лучшее решение - позволить объектам регистрироваться для получения разрешений на обновление данных, представляющих интерес, которые диспетчер данных уведомит эти объекты при изменении данных. В условиях мирян, пусть представляющий интерес данных сообщает менеджеру: «Пожалуйста, уведомите меня, когда данные меняются». Кроме того, эти объекты могут не только зарегистрироваться для получения уведомлений об обновлении, но также отменить регистрацию, чтобы убедиться, что диспетчер данных больше не уведомляет объект при изменении данных. В исходном определении GOF объект, зарегистрированный для получения обновлений, называется «наблюдатель», соответствующий диспетчер данных называется «субъект», данные, которые интересуется наблюдателем, называется «целевым состоянием», процесс регистрации называется «добавить», а процесс отмены наблюдения называется «отделением». Как упомянуто выше, режим наблюдателя также называется режимом Publish-Subscribe. Можно понять, что клиент подписывается на наблюдателя о цели. Когда целевой статус обновляется, Target публикует эти обновления для подписчика (этот шаблон дизайна распространяется на общую архитектуру, называемую архитектурой Publish-Subscribe). Эти концепции могут быть представлены следующей классовой диаграммой:
ConcereteObserver использует его для получения изменений состояния обновления и передачи ссылки на ConceretESubject на его конструктор. Это дает ссылку на конкретный предмет для конкретного наблюдателя, из которого можно получить обновления при изменении состояния. Проще говоря, конкретному наблюдателю будет предложено обновить тему, и в то же время использовать ссылки в его конструкторе, чтобы получить состояние конкретной темы, и, наконец, сохранить эти объекты состояния поиска в рамках свойства Observerstate конкретного наблюдателя. Этот процесс показан на следующей диаграмме последовательности:
Специализация классических моделей <br /> Хотя модель наблюдателя универсальна, есть также много специализированных моделей, наиболее распространенными из которых являются следующие два:
1. Предоставьте параметр объекту состояния и передайте его методу обновления, вызванного наблюдателем. В классическом режиме, когда наблюдатель уведомляется, что состояние субъекта изменилось, его обновленное состояние будет получено непосредственно от субъекта. Это требует, чтобы наблюдатель сохранил ссылку на объект на полученное состояние. Это образует круговую ссылку, ссылка на ConcretESubject указывает на его список наблюдателей, а ссылка на ConcretESubject указывает на ConcretESubject, которые могут получить состояние субъекта. В дополнение к получению обновленного состояния, нет никакой связи между наблюдателем и предметом, который он регистрирует для прослушивания. Наблюдатель заботится о объекте штата, а не о самой субъекте. То есть во многих случаях, ConcreteObserver и Concretesubject насильно связаны вместе. Напротив, когда ConcretESubject вызывает функцию обновления, объект состояния передается в ConcreteObserver, и они не должны быть связаны. Связь между ConcretObserver и объектом штата уменьшает степень зависимости между наблюдателем и государством (см. Статью Мартина Фаулера для большего количества различий в ассоциации и зависимости).
2. Объедините класс субъекта и ConcretESubject в класс SingLesubject. В большинстве случаев использование абстрактных классов по субъекту не улучшает гибкость и масштабируемость программы, поэтому объединение этого абстрактного класса и конкретного класса упрощает дизайн.
После того, как эти две специализированные модели объединены, упрощенная классовая диаграмма выглядит следующим образом:
В этих специализированных моделях статическая классовая структура значительно упрощена, и взаимодействие между классами также упрощено. Схема последовательности в это время выглядит следующим образом:
Другой особенностью режима специализации является удаление переменной элемента наблюдательного блюда ConceteObserver. Иногда конкретному наблюдателю не нужно сохранять последнее состояние предмета, но необходимо контролировать статус субъекта только при обновлении статуса. Например, если наблюдатель обновляет значение переменной элемента для стандартного вывода, он может удалить Observerstate, который удаляет связь между ConcreteObserver и классом штата.
Более распространенные правила именования <br /> классические режимы и даже профессиональный режим, упомянутый выше, используют такие термины, как прикрепление, отделение и наблюдатель, в то время как многие реализации Java используют разные словаря, включая регистр, Unregister, слушатель и т. Д. Конкретное имя объекта штата зависит от сценария, используемого в режиме наблюдателя. Например, в режиме наблюдателя в сцене, где слушатель слушает возникновение события, зарегистрированный слушатель получит уведомление, когда произойдет событие. Объект статуса в это время - это событие, то есть независимо от того, произошло событие.
В фактических приложениях именование целей редко включает субъект. Например, создайте приложение о зоопарке, зарегистрируйте несколько слушателей, чтобы наблюдать за классом зоопарка, и получите уведомления, когда новые животные входят в зоопарк. Цель в этом случае - класс зоопарка. Чтобы сохранить терминологию в соответствии с данной проблемой проблемы, термин «субъект» не будет использоваться, что означает, что класс зоопарка не будет называться Zoosubject.
Зазаивание слушателя, как правило, сопровождается суффиксом слушателя. Например, слушатель, упомянутый выше для мониторинга новых животных, будет названа AnimalAddedListener. Аналогичным образом, именование функций, таких как регистр, Unregister и уведомление, часто суффикс их соответствующими именами слушателей. Например, функции Register, Unregister и уведомление AinveraddedListener будут названы RegisterAnimalDedListener, UnregisterAnimaladdedListener и NotifyAnimaladdedListeners. Следует отметить, что используется имя функции уведомления, потому что функция уведомления обрабатывает несколько слушателей, а не с одним слушателем.
Этот метод именования будет выглядеть длительным, и обычно субъект регистрирует несколько типов слушателей. Например, в примере зоопарка, упомянутом выше, в зоопарке, в дополнение к регистрации новых слушателей для мониторинга животных, ему также необходимо зарегистрировать слушателя на животных, чтобы уменьшить слушателей. В настоящее время будет иметь две функции регистрации: (RegistranAnimalDedListener и RegisterAnimalRemovivellistener. Таким образом, тип слушателя используется в качестве квалификатора для указания типа наблюдателя. Другое решение - создать функцию RegisterListener, а затем перегружать его, но решение 1 может знать, какой слушатель выслушает. Перегрузка - это относительно NICHE подход.
Другой идиоматический синтаксис состоит в том, чтобы использовать на префиксе вместо обновления, например, функция обновления названа OnanimalAdded вместо UpdateAnimaladded. Эта ситуация чаще встречается, когда слушатель получает уведомления о последовательности, например, добавление животного в список, но он редко используется для обновления отдельных данных, таких как имя животного.
Далее в этой статье будут использоваться символические правила Java. Хотя символические правила не изменят реальную конструкцию и реализацию системы, это важный принцип разработки для использования терминов, с которыми знакомы другие разработчики, поэтому вы должны быть знакомы с символическими правилами образца наблюдателя в Java, описанных выше. Приведенная выше концепция будет объяснена ниже с использованием простого примера в среде Java 8.
Простой пример
Это также пример зоопарка, упомянутого выше. Используя интерфейс API Java8 для реализации простой системы, объясняет основные принципы шаблона наблюдателя. Проблема описывается как:
Создайте системный зоопарк, позволяя пользователям слушать и отменить состояние добавления нового объектного животного и создать конкретного слушателя, отвечающего за вывод названия нового животного.
Согласно предыдущему изучению шаблона наблюдателя, мы знаем, что для реализации такого приложения нам необходимо создать 4 класса, в частности:
Сначала мы создаем класс животных, который представляет собой простой объект Java, содержащий переменные члена, конструкторы, конструкторы, гетры и методы сеттера. Код заключается в следующем:
открытый класс животное {частное название строки; Public Animal (String name) {this.name = name; } public String getName () {return this.name; } public void setName (string name) {this.name = name; }}Используйте этот класс, чтобы представлять объекты животных, а затем вы можете создать интерфейс AnimalAddedListener:
Общественный интерфейс AnimalAddedListener {public void onAnimalAdded (животное животное);}Первые два класса очень просты, поэтому я не буду их подробно. Далее создайте класс зоопарка:
Общественный класс Zoo {Private List <Animal> Animals = new ArrayList <> (); Частный список <AnveryAddedListener> слушатели = new ArrayList <> (); public void addanimal (животное животное) {// добавить животное в список животных this.animals.add (животное); // Уведомить список зарегистрированных слушателей this.notifyanimaladdedListeners (Animal); } public void RegisterAnimalAddedListener (AnimalAddedListener прослушиватель) {// Добавить слушателя в список зарегистрированных слушателей this.listeners.add (слушатель); } public void unregisterAnimaladdedListener (AnimalAddedListener слушатель) {// Удалить слушателя из списка зарегистрированных слушателей this.listeners.remove (слушатель); } Защищенный void notifyAnimaldedListeners (животное животное) {// Уведомление каждого из слушателей в списке зарегистрированных слушателей слушателей this.listeners.foreach (слушатель -> слушатель.updateanimaladded (животное)); }}Эта аналогия сложна, чем предыдущие два. Он содержит два списка, один используется для хранения всех животных в зоопарке, а другой используется для хранения всех слушателей. Учитывая, что объекты, хранящиеся в коллекциях животных и слушателей, просты, эта статья выбрала ArrayList для хранения. Конкретная структура данных хранимого слушателя зависит от проблемы. Например, для проблемы зоопарка здесь, если у слушателя есть приоритет, вы должны выбрать другую структуру данных или переписать алгоритм регистрации слушателя.
Реализация регистрации и удаления - это простой метод делегата: каждый слушатель добавляется или удаляется из списка прослушивания слушателя в качестве параметра. Реализация функции уведомления немного выключена от стандартного формата шаблона наблюдателя. Он включает в себя входной параметр: вновь добавленное животное, так что функция уведомления может передавать недавно добавленную ссылку на животного к слушателю. Используйте функцию FOREACH API Streams, чтобы пройти слушатели и выполнить функцию TheOnanimaladded для каждого слушателя.
В дополнительной функции недавно добавленный объект и слушатель животных добавляются в соответствующий список. Если сложность процесса уведомления не учитывается, эта логика должна быть включена в удобный метод вызова. Вам нужно только передать ссылку на недавно добавленный объект животного. Вот почему логическая реализация слушателя уведомлений инкапсулируется в функции notifyAnimaladdedListeners, которая также упоминается в реализации Addanimal.
В дополнение к логическим вопросам функций уведомления, необходимо подчеркнуть противоречивую проблему на видимости функций уведомления. В классической модели наблюдателя, как сказал GOF на странице 301 шаблонов дизайна книг, функция уведомления является общедоступной, но, хотя используется в классическом шаблоне, это не означает, что она должна быть публичной. Выбор видимости должен основываться на приложении. Например, в примере зоопарка этой статьи функция уведомления имеет защиту типа и не требует, чтобы каждый объект инициировал уведомление о зарегистрированном наблюдателе. Он должен только убедиться, что объект может наследовать функцию от родительского класса. Конечно, это не совсем так. Необходимо выяснить, какие классы могут активировать функцию уведомления, а затем определить видимость функции.
Далее вам нужно реализовать класс PrintnameAnimaldedListener. Этот класс использует метод System.out.println для вывода имени нового животного. Конкретный код заключается в следующем:
Общедоступный класс printnameAnimaldedlistener реализует AnimalAddedListener {@Override public void updateAnimalded (животное животное) {// печатать имя недавно добавленной системы животных. }}Наконец, нам нужно реализовать основную функцию, которая управляет приложением:
открытый класс main {public static void main (string [] args) {// Создать зоопарк для хранения животных Zoo Zoo = new Zoo (); // зарегистрировать слушателя, чтобы быть уведомленным, когда животное добавляется Zoo.RegisterAnimalAddedListener (New PrintNameAnimalDedListener ()); // Добавить животное уведомить зарегистрированные слушатели Zoo.Addanimal (New Animal ("Tiger")); }}Основная функция просто создает объект зоопарка, регистрирует слушателя, который выводит имя животного, и создает новый объект животного, чтобы запустить зарегистрированного слушателя. Окончательный вывод:
Добавил новое животное с названием «Тигр»
Добавлен слушатель
Преимущества режима наблюдателя полностью отображаются, когда слушатель восстанавливается и добавляется к предмету. Например, если вы хотите добавить слушателя, который вычисляет общее количество животных в зоопарке, вам просто нужно создать определенный класс слушателя и зарегистрировать его в классе зоопарка без каких -либо изменений в классе зоопарка. Добавление подсчетного слушателя CountingAnimaladdedListener Код следующего:
Общедоступный класс TountingAnimaldedListener реализует AnimalAddedListener {Private Static int AnimaldDcount = 0; @Override public void udVitateAnimalded (животное животное) {// увеличение количества животных животных, ададд, ++; // Распечатать количество животных System.out.println («Тотальные животные добавлены:» + AnimalDdedCount); }}Модифицированная основная функция заключается в следующем:
открытый класс main {public static void main (string [] args) {// Создать зоопарк для хранения животных Zoo Zoo = new Zoo (); // регистрировать слушателей, чтобы быть уведомленными, когда животное добавляется Zoo.registerAnimaladdedListener (новый PrintNameAnimalAddedListener ()); Zoo.RegisterAnimaladdedListener (new TougntingAnimalAddedListener ()); // Добавить животное уведомить зарегистрированные слушатели Zoo.Addanimal (New Animal ("Tiger")); Zoo.Addanimal (New Animal ("Lion")); Zoo.Addanimal (новое животное ("медведь")); }}Результатом вывода:
Добавлено новое животное с названием «Tiger'total Animals Добавлено: Добавлено новое животное с именем». Добавлены Lion'total Animals: 2 Дополнили новое животное с названием «Bear'total Animals добавлены: 3
Пользователь может создать любого прослушивателя, если изменить код регистрации прослушивателя. Эта масштабируемость в основном связана с тем, что субъект связан с интерфейсом наблюдателя, а не непосредственно связан с бетоном. Пока интерфейс не изменен, нет необходимости изменять субъект интерфейса.
Анонимные внутренние классы, функции лямбда и регистрация слушателя
Основным улучшением в Java 8 является добавление функциональных функций, таких как добавление функций Lambda. Прежде чем ввести функцию лямбды, Java предоставила аналогичные функции с помощью анонимных внутренних классов, которые все еще используются во многих существующих приложениях. В режиме наблюдателя новый слушатель может быть создан в любое время без создания определенного класса наблюдателей. Например, класс PrintNameAnimalDedListener может быть реализован в основной функции с анонимным внутренним классом. Конкретный код реализации выглядит следующим образом:
открытый класс main {public static void main (string [] args) {// Создать зоопарк для хранения животных Zoo Zoo = new Zoo (); // Регистрировать слушателей, чтобы быть уведомленными, когда животное добавляется Zoo.registerAnimaladdedListener (new AnimalAddedListener () {@Override public void updateAnimaladded (животное животное) {// Печать названия недавно добавленной системы животных. // Добавить животное уведомить зарегистрированные слушатели Zoo.Addanimal (New Animal ("Tiger")); }}Точно так же функции Lambda также могут использоваться для выполнения таких задач:
открытый класс main {public static void main (string [] args) {// Создать зоопарк для хранения животных Zoo Zoo = new Zoo (); // Регистрировать слушателей, чтобы быть уведомленными, когда животное добавляется Zoo.registerAnimalAddedListener ((Animal) -> System.out.println («Добавлено новое животное с именем» " + Animal.getName () +" '")); // Добавить животное уведомить зарегистрированные слушатели Zoo.Addanimal (New Animal ("Tiger")); }}Следует отметить, что функция лямбда подходит только для ситуаций, когда в интерфейсе слушателя есть только одна функция. Хотя это требование кажется строгим, многие слушатели на самом деле являются отдельными функциями, такими как AnimalAddedListener в примере. Если интерфейс имеет несколько функций, вы можете использовать анонимные внутренние классы.
Существует такая проблема с неявной регистрацией созданного слушателя: поскольку объект создается в рамках регистрационного вызова, невозможно сохранить ссылку на конкретного слушателя. Это означает, что слушатели, зарегистрированные с помощью функций Lambda или анонимных внутренних классов, не могут быть отозваны, поскольку функции отзывы требуют ссылки на зарегистрированного слушателя. Простой способ решить эту проблему - вернуть ссылку на зарегистрированный слушатель в функции RegisterAnimaladdedListener. Таким образом, вы можете нерегистрировать слушателя, созданный с помощью Lambda функций или анонимных внутренних классов. Улучшенный код метода выглядит следующим образом:
Public AnimalAddedListener RegisterAnimalDedListener (AnimalAddedListener Slister) {// Добавить слушателя в список зарегистрированных слушателей this.listeners.add (слушатель); вернуть слушатель;}Клиентский код для взаимодействия функции перепроектирована следующим образом:
открытый класс main {public static void main (string [] args) {// Создать зоопарк для хранения животных Zoo Zoo = new Zoo (); // Регистрировать слушателей, чтобы быть уведомленными, когда животное добавляется AnimalAddedListener Slister = ZOO.RegisterAnimaladdedListener ((Animal) -> System.out.println («Добавлено новое животное с именем» « + Animal.getName () +» '' ")); // Добавить животное уведомить зарегистрированные слушатели Zoo.Addanimal (New Animal ("Tiger")); // Нерегистрация Zoo Slieder Zoo.unregisterAnimaladdedListener (слушатель); // Добавить другое животное, которое не будет печатать название, поскольку слушатель // ранее был незарегистрированный зоопарк. }}Результат результата в настоящее время добавлен только новое животное с названием «Тигр», потому что слушатель был отменен до добавления второго животного:
Добавил новое животное с названием «Тигр»
Если принято более сложное решение, функция регистра также может вернуть класс приемника так, чтобы, например, вызов нерегистрального слушателя:
Public Class AnimalAddEdEnerReceipt {Private Final AnimalAddedListener Слушатель; public AnimalAddEdEnerReceipt (AnimalAddedListener Slister) {this.Listener = слушатель; } public final AnimalAddedListener getListener () {return this.Listener; }}Квитанция будет использоваться в качестве возвращаемого значения функции регистрации, а входные параметры функции регистрации отменены. В настоящее время реализация зоопарка выглядит следующим образом:
открытый класс ZoousingReceipt {// ... существующие атрибуты и конструктор ... public AnimalAdDistEnerReceipt RegisterAnimaladdEdListener (AnimalAddedListener) {// Добавить слушателя в список зарегистрированных слушателей this.listeners.add (слушатель); вернуть New AnimalAddEdEnerReceipt (слушатель); } public void unregisterAnimaladdedListener (AnimalAddedListEnerReceipt reception) {// Удалить слушателя из списка зарегистрированных слушателей this.listeners.remove (chestipt.getListener ()); } // ... Существующий метод уведомления ...}Приведенный выше механизм реализации позволяет хранить информацию для вызова слушателя при отмене, то есть, если алгоритм регистрации отзывы зависит от статуса слушателя, когда субъект регистрирует слушатель, этот статус будет сохранен. Если регистрация отзывы требует только ссылки на предыдущего зарегистрированного слушателя, технология приема будет казаться неприятной и не рекомендуется.
В дополнение к особенно сложным слушателям, наиболее распространенным способом регистрации слушателей является функции Lambda или через анонимные внутренние классы. Конечно, есть исключения, то есть класс, который содержит субъект, реализует интерфейс наблюдателя и регистрирует слушателя, который называет эталонную цель. Случай, как показано в следующем коде:
открытый класс Zoocontainer реализует AnimalAddedListener {Private Zoo Zoo = New ZOO (); public Zoocontainer () {// Зарегистрировать этот объект как слушатель this.zoo.registeranimaladdedlistener (это); } public Zoo getzoo () {return this.zoo; } @Override public void updateAnimaladded (животное животное) {System.out.println («Добавлено животное с именем». + Animal.getName () + "'"); } public static void main (String [] args) {// Создать контейнер зоопарка ZooContainer ZooContainer = new ZooContainer (); // Добавить животное уведомление о внутреннем уведомлении слушателя Zoocontainer.getzoo (). Addanimal (New Animal ("Tiger")); }}Этот подход подходит только для простых случаев, и код не кажется достаточно профессиональным, и он все еще очень популярен среди современных разработчиков Java, поэтому необходимо понять, как работает этот пример. Поскольку Zoocontainer реализует интерфейс AnimalAddedListener, тогда экземпляр (или объект) ZooContainer может быть зарегистрирован как AinterAddedListener. В классе Zoocontainer эта ссылка представляет собой экземпляр текущего объекта, а именно, ZooContainer, и может использоваться в качестве AnimalAddedListener.
Как правило, не все классы контейнеров необходимы для реализации таких функций, и класс контейнеров, который реализует интерфейс слушателя, может вызвать только функцию регистрации субъекта, но просто передавать ссылку на функцию регистра в качестве объекта слушателя. В следующих главах будут введены часто задаваемые вопросы и решения для многопоточных сред.
Внедрение безопасности потока <br /> Предыдущая глава представляет реализацию шаблона наблюдателя в современной среде Java. Хотя это просто, но полное, эта реализация игнорирует вопрос о ключе: безопасность потока. Большинство открытых Java-приложений являются многопоточными, а режим наблюдателя в основном используется в многопоточных или асинхронных системах. Например, если внешняя служба обновляет свою базу данных, приложение также будет получать сообщение асинхронно, а затем уведомит внутренний компонент об обновлении в режиме Observer, а не напрямую регистрирует и прослушивает внешнюю службу.
Безопасность потока в режиме наблюдателя в основном сосредоточена на теле режима, потому что конфликты потоков, вероятно, возникнут при изменении зарегистрированной коллекции слушателей. Например, один поток пытается добавить нового слушателя, в то время как другой поток пытается добавить новый объект животного, который запускает уведомления всем зарегистрированным слушателям. Учитывая порядок последовательности, первый поток может или не может завершить регистрацию нового слушателя, прежде чем зарегистрированный слушатель получит уведомление о добавленном животном. Это классический случай соревнования по ресурсам потоков, и именно это явление сообщает разработчикам, что им нужен механизм для обеспечения безопасности потока.
Самое простое решение этой проблемы: все операции, которые получают доступ или изменяют список регистрационных слушателей, должны следовать механизму синхронизации Java, такие как:
Общедоступный синхронизированный AnimalAddedListener RegisterAnimaladdEdlistener (AnimalAddedLister Slister) {/*....*//} Public Synchronized void ungisteranimaladdedlistener (AnimalAddedListener) {/*...Таким образом, в то же время только один поток может изменить или получить доступ к зарегистрированному списку слушателей, который может успешно избежать проблем соревнований по ресурсам, но возникают новые проблемы, и такие ограничения слишком строгие (для получения дополнительной информации о синхронизированных ключевых словах и моделях параллели Java, пожалуйста, обратитесь к официальной веб -странице). Благодаря синхронизации метода, одновременный доступ к списку слушателей может наблюдаться всегда. Регистрация и отзыв слушателя-это операция записи для списка слушателей, в то время как уведомление слушателя о доступе к списку слушателя-это операция только для чтения. Поскольку доступ через уведомление является операцией чтения, многочисленные операции уведомлений могут быть выполнены одновременно.
Следовательно, до тех пор, пока нет никакой регистрации или отзывы слушателя, если регистрация не зарегистрирована, до тех пор, пока любое количество параллельных уведомлений может быть выполнено одновременно без запуска конкуренции за ресурсы для списка зарегистрированного слушателя. Конечно, конкурс ресурсов в других ситуациях существовал в течение длительного времени. Чтобы решить эту проблему, блокировка ресурсов для ReadWritelock предназначена для управления операциями чтения и записи отдельно. Код реализации Threadsafezoo Code of Zoo Class выглядит следующим образом:
public Class Threadsafezoo {private final ReadWritelock ReadWritelock = new ReenterTreadWriteLock (); Защищенная окончательная блокировка readlock = readwritelock.readlock (); Защищенный Final Lock WriteLock = readWritelock.writelock (); Частный список <Animal> Animals = new ArrayList <> (); Частный список <AnveryAddedListener> слушатели = new ArrayList <> (); public void addanimal (животное животное) {// добавить животное в список животных this.animals.add (животное); // Уведомить список зарегистрированных слушателей this.notifyanimaladdedListeners (Animal); } public AnimalAddedListener RegisterAnimaladdedListener (AnimalAddedListener прослушитель) {// заблокировать список слушателей для написания этого.writelock.lock (); try {// добавить слушателя в список зарегистрированных слушателей this.listeners.add (слушатель); } наконец {// разблокировать писатель Lock this.writelock.unlock (); } вернуть слушатель; } public void unregisterAnimaladdedListener (AnimalAddedListener слушатель) {// заблокировать список слушателей для написания этого.writelock.lock (); try {// удалить слушателя из списка зарегистрированных слушателей this.listeners.remove (слушатель); } наконец {// разблокировать писатель Lock this.writelock.unlock (); }} public void notifyAnimaldedlisteners (животное животное) {// заблокировать список слушателей для чтения этого.readlock.lock (); try {// уведомить каждого из слушателей в списке зарегистрированных слушателей this.listeners.foreach (слушатель -> слушатель.updateanimaladded (Animal)); } наконец {// разблокировать читатель заблокировать это.readlock.unlock (); }}}Благодаря такому развертыванию, реализация субъекта может обеспечить безопасность потока, и несколько потоков могут одновременно выпускать уведомления. Но, несмотря на это, есть еще две проблемы соревнований по ресурсам, которые нельзя игнорировать:
Одновременный доступ к каждому слушателю. Несколько потоков могут уведомить слушателя о том, что нужны новые животные, что означает, что слушатель может быть вызван несколькими потоками одновременно.
Одновременный доступ к списку животных. Несколько потоков могут добавлять объекты в список животных одновременно. Если порядок уведомлений оказывает влияние, это может привести к конкуренции за ресурсами, что требует одновременной механизма обработки операции, чтобы избежать этой проблемы. Если зарегистрированный список слушателей получает уведомление о добавлении Animal2, а затем получает уведомление о добавлении Animal1, будет происходить конкуренция за ресурсами. Однако, если добавление Animal1 и Animal2 выполняется различными потоками, также возможно завершить добавление Animal1 до Animal2. В частности, нить 1 добавляет Animal1, прежде чем уведомлять слушателя и блокирует модуль, нить 2 добавляет Animal2 и уведомляет слушателя, а затем нить 1 уведомляет слушателя, что Animal1 был добавлен. Хотя конкуренция за ресурсами может быть проигнорирована, когда порядок последовательности не рассматривается, проблема реальна.
对监听器的并发访问
并发访问监听器可以通过保证监听器的线程安全来实现。秉承着类的“责任自负”精神,监听器有“义务”确保自身的线程安全。例如,对于前面计数的监听器,多线程的递增或递减动物数量可能导致线程安全问题,要避免这一问题,动物数的计算必须是原子操作(原子变量或方法同步),具体解决代码如下:
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 实现观察者模式?相信通过这篇文章大家都有了大概的了解了吧!