The Observer mode, also known as the Publish/Subscribe mode, was proposed by the four-person group (GoF, namely Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides) in 1994's "Design Pattern: The Basics of Reusable Object-Oriented Software" (see pages 293-313 in the book for details). Although this pattern has a considerable history, it is still widely applicable to a variety of scenarios and has even become an integral part of the standard Java library. Although there are already a lot of articles about observer patterns, they all focus on implementation in Java, but ignore the various problems encountered by developers when using observer patterns in Java.
The original intention of writing this article is to fill this gap: this article mainly introduces the implementation of the observer pattern by using the Java8 architecture, and further explores complex issues about classic patterns on this basis, including anonymous internal classes, lambda expressions, thread safety, and non-trivial time-consuming observer implementation. Although the content of this article is not comprehensive, many of the complex issues involved in this model cannot be explained in just one article. But after reading this article, readers can understand what the observer pattern is, its universality in Java, and how to deal with some common problems when implementing the observer pattern in Java.
Observer mode
According to the classic definition proposed by GoF, the theme of the observer pattern is:
Defines a one-to-many dependency between objects. When the state of an object changes, all objects that depend on it are notified and automatically updated.
What does it mean? In many software applications, the states between objects are interdependent. For example, if an application focuses on numerical data processing, this data may be displayed through tables or charts of the graphical user interface (GUI) or used at the same time, that is, when the underlying data is updated, the corresponding GUI components must also be updated. The key to the problem is how to update the underlying data when the GUI components are updated, and at the same time minimize the coupling between the GUI components and the underlying data.
A simple and non-scalable solution is to refer to the table and image GUI components of the objects that manage these underlying data, so that the objects can notify the GUI components when the underlying data changes. Obviously, this simple solution quickly showed its shortcomings for complex applications that handle more GUI components. For example, there are 20 GUI components that all rely on underlying data, so the objects that manage underlying data need to maintain references to these 20 components. As the number of objects dependent on related data increases, the degree of coupling between data management and objects becomes difficult to control.
Another better solution is to allow objects to register to get permissions to update data of interest, which the data manager will notify those objects when the data changes. In layman's terms, let the data object of interest tell the manager: "Please notify me when the data changes." In addition, these objects can not only register to obtain update notifications, but also cancel registration to ensure that the data manager no longer notifies the object when the data changes. In the original definition of GoF, the object registered to obtain updates is called "observer", the corresponding data manager is called "subject", the data that the observer is interested in is called "target state", the registration process is called "add" and the process of undoing observation is called "detach". As mentioned above, the observer mode is also called the publish-subscribe mode. It can be understood that a customer subscribes to the observer about the target. When the target status is updated, the target publishes these updates to the subscriber (this design pattern is extended to a general architecture, called the publish-subscribe architecture). These concepts can be represented by the following class diagram:
ConcereteObserver uses it to receive update state changes and pass a reference to the ConcereteSubject to its constructor. This provides a reference to a specific subject for a specific observer, from which updates can be obtained when state changes. Simply put, the specific observer will be told to update the topic, and at the same time use the references in its constructor to obtain the state of the specific topic, and finally store these search state objects under the observerState property of the specific observer. This process is shown in the following sequence diagram:
Professionalization of classic models
Although the observer model is universal, there are many specialized models, the most common of which are the following two:
Provides a parameter to the State object, passed to the Update method called by the observer. In classic mode, when the observer is notified that the Subject state has changed, its updated state will be obtained directly from the Subject. This requires the observer to save an object reference to the retrieved state. This forms a circular reference, the reference of ConcreteSubject points to its observer list, and the reference of ConcreteObserver points to the ConcreteSubject that can obtain the subject state. In addition to obtaining the updated state, there is no connection between the observer and the Subject it registers to listen to. The observer cares about the State object, not the Subject itself. That is to say, in many cases, ConcreteObserver and ConcreteSubject are forcibly linked together. On the contrary, when ConcreteSubject calls the Update function, the State object is passed to ConcreteObserver, and the two do not need to be associated. The association between ConcreteObserver and State object reduces the degree of dependence between observer and State (see Martin Fowler's article for more differences in association and dependency).
Merge the Subject abstract class and the ConcreteSubject into a singleSubject class. In most cases, the use of abstract classes in Subject does not improve program flexibility and scalability, so combining this abstract class and concrete class simplifies design.
After these two specialized models are combined, the simplified class diagram is as follows:
In these specialized models, the static class structure is greatly simplified and the interactions between classes are also simplified. The sequence diagram at this time is as follows:
Another feature of the specialization mode is the removal of the member variable observeState of ConcreteObserver. Sometimes the specific observer does not need to save the latest state of the Subject, but only needs to monitor the status of the Subject when the status is updated. For example, if the observer updates the value of the member variable to the standard output, he can delete the observerState, which removes the association between the ConcreteObserver and the State class.
More common naming rules
The classic model and even the professional model mentioned above use terms such as attach, detach and observer, while many Java implementations use different dictionaries, including register, unregister, listener, etc. It is worth mentioning that State is a general term for all objects that listener needs to monitor changes. The specific name of the state object depends on the scenario used in the observer mode. For example, in the observer mode in the scene where the listener listens to the event occurrence, the registered listener will receive a notification when the event occurs. The status object at this time is event, that is, whether the event has occurred.
In actual applications, the naming of targets rarely includes a Subject. For example, create an app about a zoo, register multiple listeners to observe the Zoo class, and receive notifications when new animals enter the zoo. The goal in this case is the Zoo class. In order to keep the terminology consistent with the given problem domain, the term "Subject" will not be used, which means that the Zoo class will not be named ZooSubject.
The naming of the listener is generally followed by the Listener suffix. For example, the listener mentioned above to monitor new animals will be named AnimalAddedListener. Similarly, the naming of functions such as register, unregister and notify are often suffixed by their corresponding listener names. For example, the register, unregister, and notify functions of AnimalAddedListener will be named registerAnimalAddedListener, unregisterAnimalAddedListener and notifyAnimalAddedListeners. It should be noted that the notify function name s is used, because the notify function handles multiple listeners rather than a single listener.
This naming method will appear lengthy, and usually a subject will register multiple types of listeners. For example, in the zoo example mentioned above, in Zoo, in addition to registering new listeners for monitoring animals, it also needs to register a listener to animals to reduce listeners. At this time, there will be two register functions: (registerAnimalAddedListener and registerAnimalRemovedListener. This way, the type of the listener is used as a qualifier to indicate the type of observer. Another solution is to create a registerListener function and then overload it, but solution 1 can more conveniently know which listener is listening. Overloading is a relatively niche approach.
Another idiomatic syntax is to use on prefix instead of update, for example, the update function is named onAnimalAdded instead of updateAnimalAdded. This situation is more common when the listener gets notifications for a sequence, such as adding an animal to the list, but it is rarely used to update a separate data, such as the animal's name.
Next, this article will use Java's symbolic rules. Although symbolic rules will not change the real design and implementation of the system, it is an important development principle to use terms that other developers are familiar with, so you must be familiar with the observer pattern symbolic rules in Java described above. The above concept will be explained below using a simple example in the Java 8 environment.
A simple example
It is also the example of the zoo mentioned above. Using Java8's API interface to implement a simple system, explaining the basic principles of the observer pattern. The problem is described as:
Create a system zoo, allowing users to listen and undo the state of adding new object animal, and create a specific listener, responsible for outputting the name of the new animal.
According to the previous learning of the observer pattern, we know that to implement such an application, we need to create 4 classes, specifically:
Zoo class: i.e. the theme in the pattern, which is responsible for storing all animals in the zoo and notifying all registered listeners when new animals join.
Animal class: Represents an animal object.
AnimalAddedListener class: that is, observer interface.
PrintNameAnimalAddedListener: The specific observer class is responsible for outputting the name of the newly added animal.
First we create an Animal class, which is a simple Java object containing name member variables, constructors, getters and setter methods. The code is as follows:
public class 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;}}Use this class to represent animal objects, and then you can create the AnimalAddedListener interface:
public interface AnimalAddedListener { public void onAnimalAdded (Animal animal);}The first two classes are very simple, so I won’t introduce them in detail. Next, create the Zoo class:
public class Zoo { private List<Animal> animals = new ArrayList<>();private List<AnimalAddedListener> listeners = new ArrayList<>();public void addAnimal (Animal animal) {// Add the animal to the list of animalsthis.animals.add(animal);// Notify the list of registered listenersthis.notifyAnimalAddedListeners(animal);}public void registerAnimalAddedListener (AnimalAddedListener listener) {// Add the listener to the list of registered listenersthis.listeners.add(listener);}public void unregisterAnimalAddedListener (AnimalAddedListener listener) {// Remove the listener from the list of the registered listenersthis.listeners.remove(listener);}protected void notifyAnimalAddedListeners (Animal animal) {// Notify each of the listeners in the list of registered listeners listenersthis.listeners.forEach(listener -> listener.updateAnimalAdded(animal));}}This analogy is complex than the previous two. It contains two lists, one is used to store all animals in the zoo and the other is used to store all listeners. Given that the objects stored in animals and listener collections are simple, this article chose ArrayList for storage. The specific data structure of the stored listener depends on the problem. For example, for the zoo problem here, if the listener has priority, you should choose another data structure, or rewrite the listener's register algorithm.
The implementation of registration and removal is both a simple delegate method: each listener is added or removed from the listener's listening list as a parameter. The implementation of the notify function is slightly off from the standard format of the observer pattern. It includes the input parameter: the newly added animal, so that the notify function can pass the newly added animal reference to the listener. Use the forEach function of the streams API to traverse the listeners and execute theonAnimalAdded function on each listener.
In the addAnimal function, the newly added animal object and listener are added to the corresponding list. If the complexity of the notification process is not taken into account, this logic should be included in a convenient call method. You only need to pass in a reference to the newly added animal object. This is why the logical implementation of the notification listener is encapsulated in the notifyAnimalAddedListeners function, which is also mentioned in the implementation of addAnimal.
In addition to the logical issues of notify functions, it is necessary to emphasize the controversial issue on the visibility of notify functions. In the classic observer model, as GoF said on page 301 of the book Design Patterns, the notify function is public, but although used in the classic pattern, this does not mean that it must be public. The selection of visibility should be based on the application. For example, in this article's zoo example, the notify function is of type protected and does not require each object to initiate a notification of a registered observer. It only needs to ensure that the object can inherit the function from the parent class. Of course, this is not exactly the case. It is necessary to figure out which classes can activate the notify function, and then determine the visibility of the function.
Next, you need to implement the PrintNameAnimalAddedListener class. This class uses the System.out.println method to output the name of the new animal. The specific code is as follows:
public class PrintNameAnimalAddedListener implements AnimalAddedListener { @Overridepublic void updateAnimalAdded (Animal animal) {// Print the name of the newly added animalSystem.out.println("Added a new animal with name '" + animal.getName() + "'");}}Finally, we need to implement the main function that drives the application:
public class Main { public static void main (String[] args) {// Create the zoo to store animalsZoo zoo = new Zoo();// Register a listener to be notified when an animal is addedzoo.registerAnimalAddedListener(new PrintNameAnimalAddedListener());// Add an animal notify the registered listenerszoo.addAnimal(new Animal("Tiger"));}}The main function simply creates a zoo object, registers a listener that outputs the animal name, and creates a new animal object to trigger the registered listener. The final output is:
Added a new animal with name 'Tiger'
Added listener
The advantages of observer mode are fully displayed when the listener is re-established and added to the Subject. For example, if you want to add a listener that calculates the total number of animals in a zoo, you just need to create a specific listener class and register it with the Zoo class without any modification to the zoo class. Adding the counting listener CountingAnimalAddedListener code is as follows:
public class CountingAnimalAddedListener implements AnimalAddedListener { private static int animalsAddedCount = 0;@Overridepublic void updateAnimalAdded (Animal animal) {// Increment the number of animalsSystem.out.println("Total animals added: " + animalsAddedCount);}}The modified main function is as follows:
public class Main { public static void main (String[] args) {// Create the zoo to store animalsZoo zoo = new Zoo();// Register listeners to be notified when an animal is addedzoo.registerAnimalAddedListener(new PrintNameAnimalAddedListener());zoo.registerAnimalAddedListener(new CountingAnimalAddedListener());// Add an animal notify the registered listenerszoo.addAnimal(new Animal("Tiger"));zoo.addAnimal(new Animal("Lion"));zoo.addAnimal(new Animal("Bear"));}}The output result is:
Added a new animal with name 'Tiger' Total animals added: 1 Added a new animal with name 'Lion' Total animals added: 2 Added a new animal with name 'Bear' Total animals added: 3
The user can create any listener if only modify the listener registration code. This scalability is mainly because the Subject is associated with the observer interface, rather than directly associated with the ConcreteObserver. As long as the interface is not modified, there is no need to modify the interface's Subject.
Anonymous internal classes, Lambda functions and listener registration
A major improvement in Java 8 is the addition of functional features, such as the addition of lambda functions. Before introducing the lambda function, Java provided similar functions through anonymous internal classes, which are still used in many existing applications. In observer mode, a new listener can be created at any time without creating a specific observer class. For example, the PrintNameAnimalAddedListener class can be implemented in the main function with anonymous internal class. The specific implementation code is as follows:
public class Main { public static void main (String[] args) {// Create the zoo to store animalsZoo zoo = new Zoo();// Register listeners to be notified when an animal is addedzoo.registerAnimalAddedListener(new AnimalAddedListener() {@Overridepublic void updateAnimalAdded (Animal animal) {// Print the name of the newly added animalSystem.out.println("Added a new animal with name '" + animal.getName() + "'");}});// Add an animal notify the registered listenerszoo.addAnimal(new Animal("Tiger"));}}Similarly, lambda functions can also be used to complete such tasks:
public class Main { public static void main (String[] args) {// Create the zoo to store animalsZoo zoo = new Zoo();// Register listeners to be notified when an animal is addedzoo.registerAnimalAddedListener((animal) -> System.out.println("Added a new animal with name '" + animal.getName() + "'"));// Add an animal notify the registered listenerszoo.addAnimal(new Animal("Tiger"));}}It should be noted that the lambda function is only suitable for situations where there is only one function in the listener interface. Although this requirement seems strict, many listeners are actually single functions, such as the AnimalAddedListener in the example. If the interface has multiple functions, you can choose to use anonymous inner classes.
There is such a problem with implicit registration of the listener created: Since the object is created within the scope of the registration call, it is impossible to store a reference to a specific listener. This means that listeners registered through lambda functions or anonymous internal classes cannot be revoked because revocation functions require a reference to the registered listener. An easy way to solve this problem is to return a reference to the registered listener in the registerAnimalAddedListener function. In this way, you can unregister the listener created with lambda functions or anonymous internal classes. The improved method code is as follows:
public AnimalAddedListener registerAnimalAddedListener (AnimalAddedListener listener) { // Add the listener to the list of registered listenersthis.listeners.add(listener); return listener;}The client code for the redesigned function interaction is as follows:
public class Main { public static void main (String[] args) {// Create the zoo to store animalsZoo zoo = new Zoo();// Register listeners to be notified when an animal is addedAnimalAddedListener listener = zoo.registerAnimalAddedListener((animal) -> System.out.println("Added a new animal with name '" + animal.getName() + "'"));// Add an animal notify the registered listenerszoo.addAnimal(new Animal("Tiger"));// Unregister the listenerzoo.unregisterAnimalAddedListener(listener);// Add another animal, which will not print the name, since the listener// has been previously unregisteredzoo.addAnimal(new Animal("Lion"));}}The result output at this time is only Added a new animal with name 'Tiger', because the listener has been cancelled before the second animal is added:
Added a new animal with name 'Tiger'
If a more complex solution is adopted, the register function can also return the receiver class so that the unregister listener is called, for example:
public class AnimalAddedListenerReceipt { private final AnimalAddedListener listener;public AnimalAddedListenerReceipt (AnimalAddedListener listener) {this.listener = listener;}public final AnimalAddedListener getListener () {return this.listener;}}Receipt will be used as the return value of the registration function and the input parameters of the registration function are cancelled. At this time, the zoo implementation is as follows:
public class ZooUsingReceipt { // ...Existing attributes and constructor...public AnimalAddedListenerReceipt registerAnimalAddedListener (AnimalAddedListener listener) {// Add the listener to the list of registered listenersthis.listeners.add(listener);return new AnimalAddedListenerReceipt(listener);}public void unregisterAnimalAddedListener (AnimalAddedListenerReceipt reception) {// Remove the listener from the list of the registered listenersthis.listeners.remove(receipt.getListener());}// ...Existing notification method...}The receiving implementation mechanism described above allows the storage of information for call to the listener when revoking, that is, if the revocation registration algorithm depends on the status of the listener when the Subject registers the listener, this status will be saved. If the revocation registration only requires a reference to the previous registered listener, the reception technology will appear troublesome and is not recommended.
In addition to particularly complex specific listeners, the most common way to register listeners is through lambda functions or through anonymous internal classes. Of course, there are exceptions, that is, the class that contains subject implements the observer interface and registers a listener that calls the reference target. The case as shown in the following code:
public class ZooContainer implements AnimalAddedListener { private Zoo zoo = new Zoo();public ZooContainer () {// Register this object as a listenerthis.zoo.registerAnimalAddedListener(this);}public Zoo getZoo () {return this.zoo;}@Overridepublic void updateAnimalAdded (Animal animal) {System.out.println("Added animal with name '" + animal.getName() + "'");}public static void main (String[] args) {// Create the zoo containerZooContainer zooContainer = new ZooContainer();// Add an animal notify the innerly notified listenerzooContainer.getZoo().addAnimal(new Animal("Tiger"));}}This approach is only suitable for simple cases and the code doesn't seem professional enough, and it's still very popular with modern Java developers, so it's necessary to understand how this example works. Because ZooContainer implements the AnimalAddedListener interface, then an instance (or object) of ZooContainer can be registered as an AnimalAddedListener. In the ZooContainer class, this reference represents an instance of the current object, namely, ZooContainer, and can be used as an AnimalAddedListener.
Generally, not all container classes are required to implement such functions, and the container class that implements the listener interface can only call the Subject registration function, but simply pass the reference to the register function as the listener object. In the following chapters, FAQs and solutions for multithreaded environments will be introduced.
OneAPM provides you with end-to-end Java application performance solutions. We support all common Java frameworks and application servers to help you quickly discover system bottlenecks and locate the root causes of abnormalities. Deployment at minute levels and experience instantly, Java monitoring has never been easier. To read more technical articles, please visit OneAPM's official technology blog.
The above content introduces the relevant content of using Java 8 to implement the observer mode (Part 1). The next article introduces the method of using Java 8 to implement the observer mode (Part 2). Interested friends will continue to learn, hoping it will be helpful to everyone!