This is a very flexible pattern but also a huge footgun in complex systems. The core issue is that you have no knowledge of the state of the observers when notification is done. Cyclic notification, overnotification, deadlocks, lifetime issues (crashes or memory leaks), all hidden in pretty deep stack traces are very common when using Observer. In my opinion a better pattern is accumulating those messages and processing them in bulk at certain points of the program when a system is in a better known state (discussed in the game programming patterns book as asynchronous or "lazy" notification if I remember correctly). Otherwise one needs to be very conservative about how observers process notifications. Trivia: The software shown in the Blender slide is actually quadriflow, though blender itself does use asynchronous notifications in some of its subsystems. The unsafe property of the Observer pattern can be easily seen when trying to implement it in the Rust language, where it can easily fail if done naively, since we need to store a reference of the observer in the observable.
Things to consider when designing this kind of notification system: 1) When I'm watcher1 subscribed to subject1 and to subject2, how do I know which subject is notifying me? 2) Is additional data coming with the notification? 3) Can the watchers affect the notification before it is send to other watchers? (Think of "can I close the app, is there unsaved document" type of event.) 4) Adding and removing (and destroying) watchers as reaction to notification. 5) Notification being fired as reaction to another notification, nesting, infinite chain of notifications. This is only from top of my head (learned some of those points the hard way).
In my opinion, the use of shared pointers for this pattern is a code smell. If you are forced to use a shared pointer, there is a problem with the application design; there may be missing levels of abstraction. If the object lifetimes arent clear, then a weak pointer is preferable. Also, the inheritance paradigm still couples the observer to the subject. One way to resolve this is to use std::function, which I've found quite useful when working across libraries.
It is definitely a mistake. A part of the observable-observer protocol is to unsubscribe at the moment of death. If observable dies it unsubscribes all the observers. If the observer dies it unsubscribes from the current observable if any. Thus there is no need in the shared_ptr in any way shape or form. The way it is presented in the video is a design mistake. Because now observable controls the life-time of each observer subscribed to it.
Another name for this pattern is the "Listener" pattern. the JUCE framework (audio development framework) uses that term, instead of 'Observer/Observable'.
I think that one of the main traps of this pattern is when people try to overgeneralize the use-cases handled by a single subject instantiation. The speaker tried to show how to include different "systems" in a single subject (multiple frameworks do this as well), but I think that this should be a last resort. The code gets much cleaner and easier to manage when you have different subject instantiations for different systems or use-cases. You can more clearly see which system each class in your codebase depends on. You can better control the scope of each subject. You can also then templatize each subject to pass actual data to the observers.
Beginner questions if one of you would mind clearing out: 1) in the UML, the observer holds a reference to the subject yet it was not shown in the implementation unless I missed something? 2) why was there a reference to the subject in the observer instead of a reference to the Isubject incase the use wants to subscribe to different subjects? 3) shouldn't the observer has a method to subscribe to different subject, maybe the observer should have a list of references to Isubject? Thank you for taking the time and sharing from experience.
The reason why you are asking this question is because the implementation of the pattern in the video is, lets say, awful. These should have been the main key differences: 1) Each observer must support at least three types of notifications: OnSubscribe, OnNotify, and OnUnsubscribe. They could be the same but you must notify the observer when it is subscribing, because you want the observer to be updated and to become in a coherent state at the moment of subscription. For example, if you connect the view to the model you want the view to show the sate of the model immediately. 2) Also, the observer observable relationship guaranties that each observer is subscribed to at most one observable at each moment of the time. 3) All connections are done via observable method subscribe and nowhere else. Now, Qt signals/slots. 3) In Qt you have a global function connecting a signal with a slot. So, anyone in any place of the program may connect a signal and a slot. Good luck with tracking those connections. 2) There is no restriction on the connection type. For example, you may have several parallel connections, that is, you may connect the same signal with the same slot several times. Or you may connect a slot to several signals. It is painful if you need to distinguish the origin of the signal. 1) And the best part. The signal does not notify the slot at the moment of connection. So, you cannot update the recipient of the information at the moment of connection. And it is impossible to do that. Because you connect the signal and the slot from the outside of the object by a global function. But in order to notify the slot, you need to know the state of the object with the signal and its internal logic to notify with the actual data. So, when you connect a model with a view via Qt signal/slot connection you have to update the view somehow. In my opinion, you should implement observables and observers as better versions of signals and slots. They must be special members of the class that provide connections of specific types with specific safety guaranties.
I guess it will be problematic to make the observable pattern in the standard as each use cases may have different requisites, so to make it general, it will mandate a very general approach. Making the observer pattern by hand is quite easy, and for the lifetime, it will be the same problem if the library is general. Because a general pattern will not be flexible if they enforce shared_ptr or unique_ptr and so on. So there is no free lunch here. The lifetime of objects will be something you need to control in some way regardless of having a standard library that supports the pattern.
Maybe concrete classes should HAVE-A observable feature, or more, probably templated with event arguments. Deriving observables from an interface feels unnecessary, as there is no need to refer to them in an abstract way.
I understand that this was designed for beginners, but I really wish the implementations were more moderns and less Java. drop the virtual inheritance in favor of inheriting from multiple template and employ the "using declarations" syntax. (and use the visitor design pattern and std::variant for type safety).
Disappointing. Talks like this should be show casing best practices. Don't use raw pointers and enum in the slides and mention the better ways as an aside 😞. Also I think a weak_ptr is probably the best to hold the observers. A shared_ptr could keep the object alive when no one else cares about it.
if he had shown us the how threads and locks are used along with weak pointers, would you be satisfied ??. As of my observation, the audience, they seem to be interested and listened to this basic implementation very well. I wonder what could have been their reaction if Mike started with complex stuffs from the beginning...
Guys, C++ can do things at compile-time .. Sometimes that can give speed. class SoundObserver { using SUPPORT_SOUND = true; }; class VisualObserver { using SUPPORT_SOUND = void; }; template class ProcessSound { TO observer; void notifySound(Bla soundEvent) { if(is_void(TO.SUPPORT_SOUND)) { return; } observer.playSound(soundEvent); } };
this is one of the best lesson on design pattern i've ever seen. practical and interactive. great job!!
This is a very flexible pattern but also a huge footgun in complex systems. The core issue is that you have no knowledge of the state of the observers when notification is done. Cyclic notification, overnotification, deadlocks, lifetime issues (crashes or memory leaks), all hidden in pretty deep stack traces are very common when using Observer. In my opinion a better pattern is accumulating those messages and processing them in bulk at certain points of the program when a system is in a better known state (discussed in the game programming patterns book as asynchronous or "lazy" notification if I remember correctly). Otherwise one needs to be very conservative about how observers process notifications.
Trivia: The software shown in the Blender slide is actually quadriflow, though blender itself does use asynchronous notifications in some of its subsystems.
The unsafe property of the Observer pattern can be easily seen when trying to implement it in the Rust language, where it can easily fail if done naively, since we need to store a reference of the observer in the observable.
Things to consider when designing this kind of notification system: 1) When I'm watcher1 subscribed to subject1 and to subject2, how do I know which subject is notifying me? 2) Is additional data coming with the notification? 3) Can the watchers affect the notification before it is send to other watchers? (Think of "can I close the app, is there unsaved document" type of event.) 4) Adding and removing (and destroying) watchers as reaction to notification. 5) Notification being fired as reaction to another notification, nesting, infinite chain of notifications. This is only from top of my head (learned some of those points the hard way).
In my opinion, the use of shared pointers for this pattern is a code smell. If you are forced to use a shared pointer, there is a problem with the application design; there may be missing levels of abstraction. If the object lifetimes arent clear, then a weak pointer is preferable. Also, the inheritance paradigm still couples the observer to the subject. One way to resolve this is to use std::function, which I've found quite useful when working across libraries.
It is definitely a mistake. A part of the observable-observer protocol is to unsubscribe at the moment of death. If observable dies it unsubscribes all the observers. If the observer dies it unsubscribes from the current observable if any. Thus there is no need in the shared_ptr in any way shape or form. The way it is presented in the video is a design mistake. Because now observable controls the life-time of each observer subscribed to it.
Another name for this pattern is the "Listener" pattern. the JUCE framework (audio development framework) uses that term, instead of 'Observer/Observable'.
Thank you!!
I think that one of the main traps of this pattern is when people try to overgeneralize the use-cases handled by a single subject instantiation. The speaker tried to show how to include different "systems" in a single subject (multiple frameworks do this as well), but I think that this should be a last resort. The code gets much cleaner and easier to manage when you have different subject instantiations for different systems or use-cases. You can more clearly see which system each class in your codebase depends on. You can better control the scope of each subject. You can also then templatize each subject to pass actual data to the observers.
Beginner questions if one of you would mind clearing out: 1) in the UML, the observer holds a reference to the subject yet it was not shown in the implementation unless I missed something? 2) why was there a reference to the subject in the observer instead of a reference to the Isubject incase the use wants to subscribe to different subjects? 3) shouldn't the observer has a method to subscribe to different subject, maybe the observer should have a list of references to Isubject?
Thank you for taking the time and sharing from experience.
Where can I find the code?
Qt's signals and slots: "Am I a joke to you?"
The reason why you are asking this question is because the implementation of the pattern in the video is, lets say, awful. These should have been the main key differences:
1) Each observer must support at least three types of notifications: OnSubscribe, OnNotify, and OnUnsubscribe. They could be the same but you must notify the observer when it is subscribing, because you want the observer to be updated and to become in a coherent state at the moment of subscription. For example, if you connect the view to the model you want the view to show the sate of the model immediately.
2) Also, the observer observable relationship guaranties that each observer is subscribed to at most one observable at each moment of the time.
3) All connections are done via observable method subscribe and nowhere else.
Now, Qt signals/slots.
3) In Qt you have a global function connecting a signal with a slot. So, anyone in any place of the program may connect a signal and a slot. Good luck with tracking those connections.
2) There is no restriction on the connection type. For example, you may have several parallel connections, that is, you may connect the same signal with the same slot several times. Or you may connect a slot to several signals. It is painful if you need to distinguish the origin of the signal.
1) And the best part. The signal does not notify the slot at the moment of connection. So, you cannot update the recipient of the information at the moment of connection. And it is impossible to do that. Because you connect the signal and the slot from the outside of the object by a global function. But in order to notify the slot, you need to know the state of the object with the signal and its internal logic to notify with the actual data. So, when you connect a model with a view via Qt signal/slot connection you have to update the view somehow.
In my opinion, you should implement observables and observers as better versions of signals and slots. They must be special members of the class that provide connections of specific types with specific safety guaranties.
I guess it will be problematic to make the observable pattern in the standard as each use cases may have different requisites, so to make it general, it will mandate a very general approach. Making the observer pattern by hand is quite easy, and for the lifetime, it will be the same problem if the library is general. Because a general pattern will not be flexible if they enforce shared_ptr or unique_ptr and so on. So there is no free lunch here. The lifetime of objects will be something you need to control in some way regardless of having a standard library that supports the pattern.
Maybe concrete classes should HAVE-A observable feature, or more, probably templated with event arguments. Deriving observables from an interface feels unnecessary, as there is no need to refer to them in an abstract way.
a std::function is still an object, but you think of it as a function. It has a function like behavior.
noice
I understand that this was designed for beginners, but I really wish the implementations were more moderns and less Java.
drop the virtual inheritance in favor of inheriting from multiple template and employ the "using declarations" syntax. (and use the visitor design pattern and std::variant for type safety).
I am happy for him that he‘s so happy, and he‘s a great presenter otherwise I think - but his frequent laughs seem out of place somehow.
Disappointing. Talks like this should be show casing best practices. Don't use raw pointers and enum in the slides and mention the better ways as an aside 😞. Also I think a weak_ptr is probably the best to hold the observers. A shared_ptr could keep the object alive when no one else cares about it.
if he had shown us the how threads and locks are used along with weak pointers, would you be satisfied ??.
As of my observation, the audience, they seem to be interested and listened to this basic implementation very well. I wonder what could have been their reaction if Mike started with complex stuffs from the beginning...
Guys, C++ can do things at compile-time .. Sometimes that can give speed.
class SoundObserver
{
using SUPPORT_SOUND = true;
};
class VisualObserver
{
using SUPPORT_SOUND = void;
};
template
class ProcessSound
{
TO observer;
void notifySound(Bla soundEvent)
{
if(is_void(TO.SUPPORT_SOUND)) { return; }
observer.playSound(soundEvent);
}
};