Режим наблюдателя, также известный как режим публикации/подписки, был предложен группой из четырех человек (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 конкретного наблюдателя. Этот процесс показан на следующей диаграмме последовательности:
Профессионализация классических моделей
Хотя модель наблюдателя универсальна, существует много специализированных моделей, наиболее распространенными из которых являются следующие два:
Предоставляет параметр объекту состояния, переданный методу обновления, вызванного наблюдателем. В классическом режиме, когда наблюдатель уведомляется, что состояние субъекта изменилось, его обновленное состояние будет получено непосредственно от субъекта. Это требует, чтобы наблюдатель сохранил ссылку на объект на полученное состояние. Это образует круговую ссылку, ссылка на ConcretESubject указывает на его список наблюдателей, а ссылка на ConcretESubject указывает на ConcretESubject, которые могут получить состояние субъекта. В дополнение к получению обновленного состояния, нет никакой связи между наблюдателем и предметом, который он регистрирует для прослушивания. Наблюдатель заботится о объекте штата, а не о самой субъекте. То есть во многих случаях, ConcreteObserver и Concretesubject насильно связаны вместе. Напротив, когда ConcretESubject вызывает функцию обновления, объект состояния передается в ConcreteObserver, и они не должны быть связаны. Связь между ConcretObserver и объектом штата уменьшает степень зависимости между наблюдателем и государством (см. Статью Мартина Фаулера для большего количества различий в ассоциации и зависимости).
Объедините курс абстрактного предмета и ConcretEsubject в класс SingLesubject. В большинстве случаев использование абстрактных классов по субъекту не улучшает гибкость и масштабируемость программы, поэтому объединение этого абстрактного класса и конкретного класса упрощает дизайн.
После того, как эти две специализированные модели объединены, упрощенная классовая диаграмма выглядит следующим образом:
В этих специализированных моделях статическая классовая структура значительно упрощена, и взаимодействие между классами также упрощено. Схема последовательности в это время выглядит следующим образом:
Другой особенностью режима специализации является удаление переменной элемента наблюдательного блюда ConceteObserver. Иногда конкретному наблюдателю не нужно сохранять последнее состояние предмета, но необходимо контролировать статус субъекта только при обновлении статуса. Например, если наблюдатель обновляет значение переменной элемента для стандартного вывода, он может удалить Observerstate, который удаляет связь между ConcreteObserver и классом штата.
Более распространенные правила именования
Классическая модель и даже профессиональная модель, упомянутая выше, используют такие термины, как Attach, Detach и Observer, в то время как многие реализации 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 класса, в частности:
Класс зоопарка: т.е. тема в шаблоне, которая отвечает за хранение всех животных в зоопарке и уведомление всех зарегистрированных слушателей, когда присоединятся новые животные.
Класс животных: представляет объект животного.
AnimalAddedListener Class: то есть интерфейс наблюдателя.
PrintnameAnimaladdedListener: конкретный класс наблюдателей отвечает за вывод названия вновь добавленного животного.
Сначала мы создаем класс животных, который представляет собой простой объект Java, содержащий переменные члена, конструкторы, конструкторы, гетры и методы сеттера. Код заключается в следующем:
открытый класс Animal {Private String name; 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 {частный список <Animal> Animals = new ArrayList <> (); Private List <AnveryAddedListener> слушатели = new ArrayList <> (); Public void Addanimal (Animal Animal) {// Добавить животное в список животных This.Animals.add (животное); // Уведомление о списке зарегистрированных слушателей. RegisterAnimalDedListener (AnimalAddedListener Slister) {// Добавить слушателя в список зарегистрированных прослушивателей thisteners.add (слушатель);} public void unregisterAnimaldedListener (AnimalAddedListener) {// Удалить прослушивателя из списка зарегистрированных слушателей. notifyAnimaladdedListeners (животное животное) {// уведомление каждого из слушателей в списке зарегистрированных слушателей слушателей stirlhis.listeners.foreach (слушатель -> слушатель.updateanimaladded (животное));}}Эта аналогия сложна, чем предыдущие два. Он содержит два списка, один используется для хранения всех животных в зоопарке, а другой используется для хранения всех слушателей. Учитывая, что объекты, хранящиеся в коллекциях животных и слушателей, просты, эта статья выбрала ArrayList для хранения. Конкретная структура данных хранимого слушателя зависит от проблемы. Например, для проблемы зоопарка здесь, если у слушателя есть приоритет, вы должны выбрать другую структуру данных или переписать алгоритм регистрации слушателя.
Реализация регистрации и удаления - это простой метод делегата: каждый слушатель добавляется или удаляется из списка прослушивания слушателя в качестве параметра. Реализация функции уведомления немного выключена от стандартного формата шаблона наблюдателя. Он включает в себя входной параметр: вновь добавленное животное, так что функция уведомления может передавать недавно добавленную ссылку на животного к слушателю. Используйте функцию FOREACH API Streams, чтобы пройти слушатели и выполнить функцию TheOnanimaladded для каждого слушателя.
В дополнительной функции недавно добавленный объект и слушатель животных добавляются в соответствующий список. Если сложность процесса уведомления не учитывается, эта логика должна быть включена в удобный метод вызова. Вам нужно только передать ссылку на недавно добавленный объект животного. Вот почему логическая реализация слушателя уведомлений инкапсулируется в функции notifyAnimaladdedListeners, которая также упоминается в реализации Addanimal.
В дополнение к логическим вопросам функций уведомления, необходимо подчеркнуть противоречивую проблему на видимости функций уведомления. В классической модели наблюдателя, как сказал GOF на странице 301 шаблонов дизайна книг, функция уведомления является общедоступной, но, хотя используется в классическом шаблоне, это не означает, что она должна быть публичной. Выбор видимости должен основываться на приложении. Например, в примере зоопарка этой статьи функция уведомления имеет защиту типа и не требует, чтобы каждый объект инициировал уведомление о зарегистрированном наблюдателе. Он должен только убедиться, что объект может наследовать функцию от родительского класса. Конечно, это не совсем так. Необходимо выяснить, какие классы могут активировать функцию уведомления, а затем определить видимость функции.
Далее вам нужно реализовать класс PrintnameAnimaldedListener. Этот класс использует метод System.out.println для вывода имени нового животного. Конкретный код заключается в следующем:
Общедоступный класс PrintNameAnimalDedListener реализует AnimalAddedListener {@OverridePublic void updateAnimaladded (Animal Animal) {// Распечатать имя недавно добавленного животного.Наконец, нам нужно реализовать основную функцию, которая управляет приложением:
открытый класс Main {public static void main (string [] args) {// Создать зоопарк для хранения животных Zoo Zoo = new Zoo (); // Зарегистрировать слушателя, чтобы быть уведомленным, когда животное добавлено.Основная функция просто создает объект зоопарка, регистрирует слушателя, который выводит имя животного, и создает новый объект животного, чтобы запустить зарегистрированного слушателя. Окончательный вывод:
Добавил новое животное с названием «Тигр»
Добавлен слушатель
Преимущества режима наблюдателя полностью отображаются, когда слушатель восстанавливается и добавляется к предмету. Например, если вы хотите добавить слушателя, который вычисляет общее количество животных в зоопарке, вам просто нужно создать определенный класс слушателя и зарегистрировать его в классе зоопарка без каких -либо изменений в классе зоопарка. Добавление подсчетного слушателя CountingAnimaladdedListener Код следующего:
Общедоступный класс TounttingAnimaldedListener реализует AnimalAddedListener {Private Static int AnimaldDcount = 0; @OverridePublic void updateAnimaldded (животное животное) {// увеличение количества животных.Модифицированная основная функция заключается в следующем:
открытый класс Main {public static void main (string [] args) {// Создать зоопарк для хранения животных Zoo Zoo = new Zoo (); // регистрации слушателей, которые будут уведомлены, когда животное добавлено. СЛУШАТЕЛЬНОСТЬ.Результатом вывода:
Добавлено новое животное с названием «Тигр».
Пользователь может создать любого прослушивателя, если изменить код регистрации прослушивателя. Эта масштабируемость в основном связана с тем, что субъект связан с интерфейсом наблюдателя, а не непосредственно связан с бетоном. Пока интерфейс не изменен, нет необходимости изменять субъект интерфейса.
Анонимные внутренние классы, функции лямбда и регистрация слушателя
Основным улучшением в Java 8 является добавление функциональных функций, таких как добавление функций Lambda. Прежде чем ввести функцию лямбды, Java предоставила аналогичные функции с помощью анонимных внутренних классов, которые все еще используются во многих существующих приложениях. В режиме наблюдателя новый слушатель может быть создан в любое время без создания определенного класса наблюдателей. Например, класс PrintNameAnimalDedListener может быть реализован в основной функции с анонимным внутренним классом. Конкретный код реализации выглядит следующим образом:
открытый класс Main {public static void main (string [] args) {// Создать зоопарк для хранения животных Zoo Zoo = new Zoo (); // регистрации слушателей, которые будут уведомлены, когда животное добавлено. AnimalyStem.out.println («Добавлено новое животное с именем» « + Animal.getName () +" '");}}); // Добавить животное уведомление о зарегистрированном слушании.Точно так же функции Lambda также могут использоваться для выполнения таких задач:
открытый класс main {public static void main (string [] args) {// Создать зоопарк для хранения животных Zoo Zoo = new Zoo (); // Регистрации слушателей, которые будут уведомлены, когда животное добавлено. Sliederszoo.addanimal (New Animal ("Tiger"));}}Следует отметить, что функция лямбда подходит только для ситуаций, когда в интерфейсе слушателя есть только одна функция. Хотя это требование кажется строгим, многие слушатели на самом деле являются отдельными функциями, такими как AnimalAddedListener в примере. Если интерфейс имеет несколько функций, вы можете использовать анонимные внутренние классы.
Существует такая проблема с неявной регистрацией созданного слушателя: поскольку объект создается в рамках регистрационного вызова, невозможно сохранить ссылку на конкретного слушателя. Это означает, что слушатели, зарегистрированные с помощью функций Lambda или анонимных внутренних классов, не могут быть отозваны, поскольку функции отзывы требуют ссылки на зарегистрированного слушателя. Простой способ решить эту проблему - вернуть ссылку на зарегистрированный слушатель в функции RegisterAnimaladdedListener. Таким образом, вы можете нерегистрировать слушателя, созданный с помощью Lambda функций или анонимных внутренних классов. Улучшенный код метода выглядит следующим образом:
Public AnimalAddedListener RegisterAnimalDedListener (AnimalAddedListener Slister) {// Добавить слушателя в список зарегистрированных прослушивателей. вернуть слушатель;}Клиентский код для взаимодействия функции перепроектирована следующим образом:
открытый класс main {public static void main (string [] args) {// Создать зоопарк для хранения животных Zoo Zoo = new Zoo (); // Регистрации слушателей, которые будут уведомлены, когда животное добавлено adanimaladdedlistener allirler = Zoo.registerAnimalDdedListener ((животное) -> system.out.println (добавлено новое животное с name. + jatemes giname. Животное уведомит зарегистрированные слушатели.Результат результата в настоящее время добавлен только новое животное с названием «Тигр», потому что слушатель был отменен до добавления второго животного:
Добавил новое животное с названием «Тигр»
Если принято более сложное решение, функция регистра также может вернуть класс приемника так, чтобы, например, вызов нерегистрального слушателя:
Public Class AnimalAddEdEnerReceipt {Private Final AnimalAddedListener Слушатель; public AnameraddEdEnerReceipt (AnveryAddedListener Slister) {this.Listener = слушатель;} public Final AnimalAddEntener getListener () {return this.Listener;}}Квитанция будет использоваться в качестве возвращаемого значения функции регистрации, а входные параметры функции регистрации отменены. В настоящее время реализация зоопарка выглядит следующим образом:
открытый класс ZoousingReceipt {// ... существующие атрибуты и конструктор ... public AnimalAddEdListEnerReceipt RegisterAnimalDdedListener (AnimalAddedLister Sluster) {// Добавить слушателя в список зарегистрированных слушателей. (AnimalAddedListEnerReceipt reception) {// Удалить прослушивателя из списка зарегистрированных прослушивателей.Приведенный выше механизм реализации позволяет хранить информацию для вызова слушателя при отмене, то есть, если алгоритм регистрации отзывы зависит от статуса слушателя, когда субъект регистрирует слушатель, этот статус будет сохранен. Если регистрация отзывы требует только ссылки на предыдущего зарегистрированного слушателя, технология приема будет казаться неприятной и не рекомендуется.
В дополнение к особенно сложным слушателям, наиболее распространенным способом регистрации слушателей является функции Lambda или через анонимные внутренние классы. Конечно, есть исключения, то есть класс, который содержит субъект, реализует интерфейс наблюдателя и регистрирует слушателя, который называет эталонную цель. Случай, как показано в следующем коде:
Общедоступный класс Zoocontainer реализует AnimalAddedListener {Private Zoo Zoo = New Zoo (); Public ZooContainer () {// Зарегистрировать этот объект как слушатель. {System.out.println ("Добавлено животное с именем" + animal.getName () + "'");} public static void main (string [] args) {// Создать зоопарк containerzoocontainer Zoocontainer = new Zoocontainer (); // Добавить животное уведомление об innerly natifififififive arderifififififive arderifive arderalaLemaLaL (). Животное ("тигр"));}}Этот подход подходит только для простых случаев, и код не кажется достаточно профессиональным, и он все еще очень популярен среди современных разработчиков Java, поэтому необходимо понять, как работает этот пример. Поскольку Zoocontainer реализует интерфейс AnimalAddedListener, тогда экземпляр (или объект) ZooContainer может быть зарегистрирован как AinterAddedListener. В классе Zoocontainer эта ссылка представляет собой экземпляр текущего объекта, а именно, ZooContainer, и может использоваться в качестве AnimalAddedListener.
Как правило, не все классы контейнеров необходимы для реализации таких функций, и класс контейнеров, который реализует интерфейс слушателя, может вызвать только функцию регистрации субъекта, но просто передавать ссылку на функцию регистра в качестве объекта слушателя. В следующих главах будут введены часто задаваемые вопросы и решения для многопоточных сред.
OneApm предоставляет вам комплексные решения для приложений Java. Мы поддерживаем все общие рамки Java и серверы приложений, чтобы помочь вам быстро обнаружить узкие места системы и найти основные причины аномалий. Развертывание на минутных уровнях и мгновенно испытано, мониторинг Java никогда не был проще. Чтобы прочитать больше технических статей, посетите официальный технологический блог Oneapm.
Приведенный выше контент вводит соответствующий содержимое использования Java 8 для реализации режима наблюдателя (часть 1). В следующей статье представлен метод использования Java 8 для реализации режима наблюдателя (часть 2). Заинтересованные друзья будут продолжать учиться, надеясь, что это будет полезно для всех!