DI and events: Reactive Extensions by Mark Seemann
With Reactive Extensions, you can convert event subscription to something that looks more object-oriented, but all is still not quite as it should be...
In my series about Dependency Injection and events, you previously saw how to let a third party connect publisher and subscriber. I think that approach is valuable in all those cases where you connect publishers and subscribers in a narrow and well-defined area of your application, such as in the Composition Root. However, it's not a good fit if you need to connect and disconnect publishers and subscribers throughout your application's code base.
This article examines alternatives based on Reactive Extensions.
Re-design #
For the rest of this article, I'm going to assume that you're in a position where you can change the design - particularly the design of the IDependency interface. If not, you can always convert a standard .NET event into the appropriate IObservable<T>.
Using small iterations, you can first make IDependency implement IObservable<Unit>:
public interface IDependency : IObservable<Unit> { event EventHandler ItHappened; }
This enables you to change the implementation of NeedyClass to subscribe to IObservable<Unit> instead of the ItHappened event:
public class NeedyClass : IObserver<Unit>, IDisposable { private readonly IDisposable subscription; public NeedyClass(IDependency dependency) { if (dependency == null) throw new ArgumentNullException("dependency"); this.subscription = dependency.Subscribe(this); } public void OnCompleted() { } public void OnError(Exception error) { } public void OnNext(Unit value) { // Handle event here } public void Dispose() { this.subscription.Dispose(); } }
Because IObservable<T>.Subscribe returns an IDisposable, NeedyClass still needs to store that object in a field and dipose of it when it's done, which also means that it must implement IDisposable itself. Thus, you seem to be no better off than with Constructor Subscription. Actually, you're slightly worse off, because now NeedyClass gained three new public methods (OnCompleted, OnError, and OnNext), compared to a single public method with Constructor Subscription.
What's even worse is that you're still violating Nikola Malovic's 4th law of IoC: Injection Constructors should perform no work.
This doesn't seem promising.
Simplification #
While you seem to have gained little from introducing Reactive Extensions, at least you can simplify IDependency a bit. No classes should have to subscribe to the old-fashioned .NET event, so you can remove that from IDependency:
public interface IDependency : IObservable<Unit> { }
That leaves IDependency degenerate, so you might as well dispense entirely with it and let NeedyClass subscribe directly to IObservable<Unit>:
public class NeedyClass : IObserver<Unit>, IDisposable { private readonly IDisposable subscription; public NeedyClass(IObservable<Unit> dependency) { if (dependency == null) throw new ArgumentNullException("dependency"); this.subscription = dependency.Subscribe(this); } public void OnCompleted() { } public void OnError(Exception error) { } public void OnNext(Unit value) { // Handle event here } public void Dispose() { this.subscription.Dispose(); } }
At least you got rid of a custom type in favor of a well-known abstraction, so that will have to count for something.
Injection disconnect #
If you refer back to the discussion about Constructor Subscription, you may recall an inkling that NeedyClass requests the wrong type of dependency via the constructor. If it's saving an IDisposable as its class field, then why is it requesting an IObservable<Unit>? Shouldn't it simply request an IDisposable?
This sounds promising, but in the end turns out to be a false lead. Still, this actually compiles:
public class NeedyClass : IObserver<Unit>, IDisposable { private readonly IDisposable subscription; public NeedyClass(IDisposable subscription) { if (subscription == null) throw new ArgumentNullException("subscription"); this.subscription = subscription; } public void OnCompleted() { } public void OnError(Exception error) { } public void OnNext(Unit value) { // Handle event here } public void Dispose() { this.subscription.Dispose(); } }
The problem with this is that while it compiles, it doesn't work. Considering the implementation, you should also be suspicious: it's basically a degenerate Decorator of IDisposable. That subscription
field doesn't seem to add anything to NeedyClass...
Examining why it doesn't work should be enlightening, though. A third party can attempt to connect NeedyClass with the observable. One attempt might look like this:
var observable = new FakyDependency(); var nc = new NeedyClass(observable.Subscribe());
However, this doesn't work, because that overload of the Subscribe method only exists to evaluate an event stream for side effects. The overload you'd need is the Subscribe(IObserver<T>) overload. However, the IObserver<Unit> you'd like to supply is an instance of NeedyClass, but you can't supply an instance of NeedyClass before you've supplied an IDisposable to it (a Catch-22)!
Third-party Connect #
Once more, this suggests that NeedyClass really shouldn't have a dependency in order to react to events. You can simply let it be a stand-alone implementation of IObserver<Unit>:
public class NeedyClass : IObserver<Unit> { public void OnCompleted() { } public void OnError(Exception error) { } public void OnNext(Unit value) { // Handle event here } }
Once again, you have a nice, stand-alone class you can connect to a publisher:
var nc = new NeedyClass(); var subscription = observable.Subscribe(nc);
That pretty much puts you back at the Third-party Connect solution, so that didn't seem to buy you much.
(Preliminary) conclusion #
So far, using Reactive Extensions instead of .NET events seems to have provided little value. You're able to replace a custom IDependency interface with a BCL interface, and that's always good. Apart from that, there seems to be little value to be gained from Reactive Extensions in this scenario.
The greatest advantage gained so far is that, hopefully you've learned something. You've learned (through two refactoring attempts) that NeedyClass isn't needy at all, and should not have a dependency in order to react to events.
The last advantage gained by using Reactive Extensions may seem like a small thing, but actually turns out to the most important of them all: by using IObservable<T>/IObserver<T> instead of .NET events, you've converted your code to work with objects. You know, .NET events are not real objects, so they can't be passed around, but IObservable<T> and IObserver<T> instances are real objects. This means that now that you know that NeedyClass shouldn't have a dependency, perhaps some other class should. Remember what I originally said about Inversion of Inversion of Control? In the next article in the series, I'll address that issue, and arrive at a more idiomatic DI solution.