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:
Specialization of classical models <br />Although the observer model is universal, there are also many specialized models, the most common of which are the following two:
1. Provide a parameter to the State object and pass it 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).
2. 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 <br />Classic modes and even the professional mode 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:
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 animals this.animals.add(animal); // Notify the list of registered listeners this.notifyAnimalAddedListeners(animal); } public void registerAnimalAddedListener (AnimalAddedListener listener) { // Add the listener to the list of registered listeners this.listeners.add(listener); } public void unregisterAnimalAddedListener (AnimalAddedListener listener) { // Remove the listener from the list of the registered listeners this.listeners.remove(listener); } protected void notifyAnimalAddedListeners (Animal animal) { // Notify each of the listeners in the list of registered listeners listeners this.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 { @Override public void updateAnimalAdded (Animal animal) { // Print the name of the newly added animal System.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 animals Zoo zoo = new Zoo(); // Register a listener to be notified when an animal is added zoo.registerAnimalAddedListener(new PrintNameAnimalAddedListener()); // Add an animal notify the registered listeners zoo.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; @Override public void updateAnimalAdded (Animal animal) { // Increment the number of animals animalsAddedCount++; // Print the number of animals System.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 animals Zoo zoo = new Zoo(); // Register listeners to be notified when an animal is added zoo.registerAnimalAddedListener(new PrintNameAnimalAddedListener()); zoo.registerAnimalAddedListener(new CountingAnimalAddedListener()); // Add an animal notify the registered listeners zoo.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: 1Added a new animal with name 'Lion'Total animals added: 2Added 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 animals Zoo zoo = new Zoo(); // Register listeners to be notified when an animal is added zoo.registerAnimalAddedListener(new AnimalAddedListener() { @Override public void updateAnimalAdded (Animal animal) { // Print the name of the newly added animal System.out.println("Added a new animal with name '" + animal.getName() + "'"); } }); // Add an animal notify the registered listeners zoo.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 animals Zoo zoo = new Zoo(); // Register listeners to be notified when an animal is added zoo.registerAnimalAddedListener( (animal) -> System.out.println("Added a new animal with name '" + animal.getName() + "'") ); // Add an animal notify the registered listeners zoo.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 listeners this.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 animals Zoo zoo = new Zoo(); // Register listeners to be notified when an animal is added AnimalAddedListener listener = zoo.registerAnimalAddedListener( (animal) -> System.out.println("Added a new animal with name '" + animal.getName() + "'") ); // Add an animal notify the registered listeners zoo.addAnimal(new Animal("Tiger")); // Unregister the listener zoo.unregisterAnimalAddedListener(listener); // Add another animal, which will not print the name, since the listener // has been previously unregistered zoo.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 listeners this.listeners.add(listener); return new AnimalAddedListenerReceipt(listener); } public void unregisterAnimalAddedListener (AnimalAddedListenerReceipt reception) { // Remove the listener from the list of the registered listeners this.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 listener this.zoo.registerAnimalAddedListener(this); } public Zoo getZoo () { return this.zoo; } @Override public void updateAnimalAdded (Animal animal) { System.out.println("Added animal with name '" + animal.getName() + "'"); } public static void main (String[] args) { // Create the zoo container ZooContainer zooContainer = new ZooContainer(); // Add an animal notify the innerly notified listener zooContainer.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.
Implementation of thread safety <br />The previous chapter introduces the implementation of the observer pattern in the modern Java environment. Although it is simple but complete, this implementation ignores a key issue: thread safety. Most open Java applications are multi-threaded, and the observer mode is mostly used in multi-threaded or asynchronous systems. For example, if an external service updates its database, the application will also receive a message asynchronously and then notify the internal component to update in observer mode, instead of directly registering and listening to the external service.
Thread safety in observer mode is mainly focused on the body of the mode, because thread conflicts are likely to occur when modifying the registered listener collection. For example, one thread tries to add a new listener, while the other thread tries to add a new animal object, which triggers notifications to all registered listeners. Given the order of sequence, the first thread may or may not have completed registration of the new listener before the registered listener receives notification of the added animal. This is a classic case of thread resource competition, and it is this phenomenon that tells developers that they need a mechanism to ensure thread safety.
The easiest solution to this problem is: all operations that access or modify the registration listener list must follow the Java synchronization mechanism, such as:
public synchronized AnimalAddedListener registerAnimalAddedListener (AnimalAddedListener listener) { /*...*/ }public synchronized void unregisterAnimalAddedListener (AnimalAddedListener listener) { /*...*/ }public synchronized void notifyAnimalAddedListeners (Animal animal) { /*...*/ }In this way, at the same time, only one thread can modify or access the registered listener list, which can successfully avoid resource competition issues, but new problems arise, and such constraints are too strict (for more information about synchronized keywords and Java concurrency models, please refer to the official webpage). Through method synchronization, concurrent access to the listener list can be observed at all times. Registering and revoking the listener is a write operation for the listener list, while notifying the listener to access the listener list is a read-only operation. Since access through notification is a read operation, multiple notification operations can be performed simultaneously.
Therefore, as long as there is no listener registration or revocation, as long as the registration is not registered, as long as any number of concurrent notifications can be executed simultaneously without triggering resource competition for the registered listener list. Of course, resource competition in other situations has existed for a long time. In order to solve this problem, resource locking for ReadWriteLock is designed to manage read and write operations separately. The thread-safe ThreadSafeZoo implementation code of Zoo class is as follows:
public class ThreadSafeZoo { private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); 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(); } }}Through such deployment, the implementation of Subject can ensure thread safety and multiple threads can issue notifications at the same time. But despite this, there are still two resource competition issues that cannot be ignored:
Concurrent access to each listener. Multiple threads can notify the listener that new animals are needed, which means that a listener may be called by multiple threads at the same time.
Concurrent access to animal list. Multiple threads may add objects to the animal list at the same time. If the order of notifications has an impact, it may lead to resource competition, which requires a concurrent operation processing mechanism to avoid this problem. If the registered listener list receives notification to add animal2 and then receives notification to add animal1, resource competition will occur. However, if the addition of animal1 and animal2 is performed by different threads, it is also possible to complete the addition of animal1 before animal2. Specifically, thread 1 adds animal1 before notifying the listener and locks the module, thread 2 adds animal2 and notifies the listener, and then thread 1 notifies the listener that animal1 has been added. Although resource competition can be ignored when the order of sequence is not considered, the problem is real.
Concurrent access to listeners
Concurrent access listeners can be implemented by ensuring the listeners' thread safety. Adhering to the spirit of "self-responsibility" of the class, the listener has the "obligation" to ensure its own thread safety. For example, for the listener counted above, increasing or decreasing animal numbers by multiple threads may lead to thread safety problems. To avoid this problem, the calculation of animal numbers must be atomic operations (atomic variables or method synchronization). The specific solution code is as follows:
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); }}The method synchronization solution code is as follows:
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); }}It should be emphasized that the listener should ensure its own thread safety. Subject needs to understand the internal logic of the listener, rather than simply ensuring thread safety for accessing and modifying the listener. Otherwise, if multiple subjects share the same listener, each subject class has to rewrite thread-safe code. Obviously, such code is not concise enough, so thread-safe needs to be implemented in the listener class.
Ordered notification of listener When the listener is required to execute in an orderly manner, the read and write lock cannot meet the needs, and a new mechanism is needed to ensure that the call order of the notify function is consistent with the order in which animal is added to zoo. Some people have tried to implement it using method synchronization, but according to the introduction of method synchronization in Oracle documentation, it can be seen that method synchronization does not provide order management of operation execution. It only ensures that atomic operations are not interrupted, and does not guarantee the thread order of first-come-first execution (FIFO). ReentrantReadWriteLock can implement such an execution order, the code is as follows:
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(); } }}In this way, register, unregister and notify functions will obtain read and write lock permissions in the order of first-in-first-out (FIFO). For example, thread 1 registers a listener, thread 2 tries to notify the registered listener after starting the registration operation, thread 3 tries to notify the registered listener when thread 2 is waiting for the read-only lock, adopting fair-ordering method, thread 1 completes the registration operation first, then thread 2 can notify the listener, and finally thread 3 notifies the listener. This ensures that the execution order and the starting order of the action are consistent.
如果采用方法同步,虽然线程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 实现观察者模式?相信通过这篇文章大家都有了大概的了解了吧!