0

There are two ways to provide a way unsubscribe in Observer Design Pattern.

1. Provide a simple void UnSubscribe method:

public void UnSubscribe(IObserver observer){
 // remove observer from List of Observers in current observable class
}

2. Return a UnSubscribe class upon subscription

        private class Unsubscriber : IDisposable
        {
            private List<IObserver<Temperature>> _observers;
            private IObserver<Temperature> _observer;

            public Unsubscriber(List<IObserver<Temperature>> observers, IObserver<Temperature> observer)
            {
                this._observers = observers;
                this._observer = observer;
            }

            public void Dispose()
            {
                if (!(_observer == null)) _observers.Remove(_observer);
            }
        }

In #2 the Unsubscriber class is a nested class within the observable class. The observer is supposed to hold the instance of Unsubscriber which encapsulates the logic to unsubscribe.

In #1 the observer is just supposed to call the UnSubscribe method on the Oberservable.

What are the pros and cons of each? Is #2 significantly better approach? Why?

1
  • how do you call these in practice?
    – Ewan
    Dec 11, 2023 at 17:01

4 Answers 4

2

I think wikipedia gives you the explicit answer

The observer pattern can cause memory leaks, known as the lapsed listener problem, because in a basic implementation, it requires both explicit registration and explicit deregistration, as in the dispose pattern, because the subject holds strong references to the observers, keeping them alive. This can be prevented if the subject holds weak references to the observers.

https://en.wikipedia.org/wiki/Observer_pattern

the Unsubscriber class with its implementation of IDisposable, ensures that the reference is removed when the subscription goes out of scope.

If you just call mediator.unsubscribe() you might forget.

3
  • I do not use C#, so this seems fishy to me. Does GC call IDisposable.dispose()? If not, why can't user forget to use using? Also, both approaches suffer from memory leaks (and both may benefit from weak references with a caveat of a potential resource leak)
    – Basilevs
    Dec 11, 2023 at 17:45
  • you don't have to use using the object will be disposed when the GC comes around
    – Ewan
    Dec 11, 2023 at 17:50
  • also it looks like the OPs code is copied and pasted out of that article
    – Ewan
    Dec 11, 2023 at 17:51
1

The UnSubscriber class is simply a closure of the Remove (or UnSubscribe) method.

Having a closure may be useful for example if the caller needs a mechanism to generically remove themselves from multiple lists, however if needed the caller can implement that themselves, either by creating a similar class or some other mechanism supported by the language (lamdba or nested function).

The counterpoint is that if you only provide an UnSubscriber class the caller has to maintain a reference to this class, so that they can unsubscribe instead of being able to use their own identity for example: something.UnSubscribe(this)

I don't see the value in always return the Unsubscriber class. In languages where it's trivial to create a closure it has no value, in other languages I might create a generic helper class/function to make it easy for the caller to create an Unsubscriber if they need it (i.e. only when they need it).


Edit Based on Ewan`s answer:

In languages that provide an explicit deconstruction semantic (such as C++) a mechanism that ensures that invalid listeners are not left dangling is preferable, as it means that the caller won't forget to Unsubscribe.

However in the generic case, for a language that doesn't provide deconstruction semantics, the answer above still stands.

4
  • > The UnSubscriber class is simply a closure of the Remove (or UnSubscribe) method. Not necessarily.
    – Basilevs
    Dec 11, 2023 at 17:42
  • @Basilevs I acknowledged Ewan's point (about deconstruction semantics) before your comment, are you making a different point?
    – DavidT
    Dec 11, 2023 at 17:53
  • My point is that storage of listeners is not necessary. (not about automated removal).
    – Basilevs
    Dec 11, 2023 at 17:57
  • Also, depending on unsubscriber implementation, it does not need to store any references at all besides system resources like sockets.
    – Basilevs
    Dec 11, 2023 at 17:58
1

In Objective-C, for a long time observers were not automatically removed from the observable when they got deleted, leading to a crash when the observable sent a notification to an object that didn’t exist anymore. At that time you just created an object to remove the observable in its destructor and stored it in the observer.

The observer could stop observing by setting this object to nil, and the same would automatically happen when the observer got deleted, because the destructor sets all contained objects to nil.

I added methods “start” and “stop” to this observation object so you could stop and restart observing at any time.

0

This answer is not applicable to C# as it has built-in event handling and it would probably be rejected by their users. This answer is a bit outdated with introduction of Flow in Java. Flow implementation is too complex for basic Observer, so I'm still posting this.

I return java.util.Closeable (similar to IDisposable in C#) or its analogs in other languages.

Disadvantages

  • it is much less popular than removeListener() and is therefore harder to read for new contributors

Advantages

  • Publisher Decorator, Facade, Adapter, Wrapper patterns are much easier to implement
  • Better encapsulation of publisher implementation
  • Compiler enforced contract management

Adapter Pattern

Consider an interface and its implementation:

public interface Value<T> {
  T get();
  Closeable listen(Consumer<T> listener);
}

public class IsPositiveValue implements Value<Boolean> {
  private final Value<Integer> input;
  public IsPositiveValue(Value<Integer> input) {
    this.input = input;
  }
  
  @Override
  Boolean get() {
    return this.input.get() > 0;
  }
  
  @Override
  Closeable listen(Consumer<Boolean> listener) {
    return input.listen(() -> listener.accept(get()));
  }
}

If we used the standard approach of removeListener() we would have to manage a dedicated list of listeners.

To be fair, in this particular design, replacing Consumer (Unary operation) with Runnable (nullary operation) would eliminate the advantage of this approach over other, as nullary operations require no conversions.

Encapsulation

Storage of listeners in a collection is not a given. Just like in the wrapper/facade above, there may be no list to manage. For example, a publisher could contact a remote API via a Socket and dispose it, once client is unsubscribed. There is no need to manage a list of listeners in this case (unless optimization to reduce socket count is required).

Compiler enforced contract management

If a Publisher has a removeListener() object, any client that has a reference to a listener, may remove it from the Publisher. More than once. At any time. Clients may also attempt to remove a wrong listener. This may lead to subtle bugs of double disposal and missing disposal.

Such bugs are impossible with explicit single-use ubsubscriber - even if reference to it is leaked (which is less likely, because it is less useful than listener), the worst damage that can happen is a premature listener removal.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.