Decorators and events
Today I stumbled across a very subtle pitfall when using the Decorator pattern on an interface that exposes C# events.
Let’s say you have the following interface
1 | public interface IDocument |
and, to use the probably most canonical example for decorators, you want to add caching without violating the SRP, so you write the following decorator:
1 | public class CachingDocumentDecorator : IDocument |
And, voila!, you have just introduced a subtle bug. Can you spot it?
Imagine another type consuming IDocument
:
1 | public class DocumentBinder |
Let’s write a quick test to check that the OpenDocuments
property will be kept up-to-date:
1 | var binder = new DocumentBinder(); |
(If the marked line puzzles you, this is just how you simulate an event firing on a mock object created with NSubstitute.)
This test passes.
But what happens if DocumentBinder
is used with decorated IDocument
s? Let’s adjust our test a tiny bit to see:
1 | var binder = new DocumentBinder(); |
Now the last line of the test will fail, the actual count of OpenDocuments
will be 2.
Why?
Because when the decorated IDocument
fires Closed
, it passes itself as sender
argument. And the decorator does nothing to change sender - so handlers of Closed
will always receive undecorated IDocuments
as sender. But DocumentBinder
adds the decorated documents to OpenDocuments
. So when it handles Closed
and removes sender
from OpenDocuments
, it will remove an instance which never has been added to the collection.
Before we fix this issue, let’s write another test for the decorator to more clearly reproduce the problem at its root:
1 | var decoree = Substitute.For<IDocument>(); |
The fix for this issue is straight-forward: in the decorator, declare the event regularly, attach to the event of _decoree
and forward it while changing the sender:
1 | public class CachingDocumentDecorator : IDocument |
Now both failing tests will pass.
As a closing remark, beware that even ReSharper does this wrong: when you use the quick-fix “Delegate implementation of X to new field…”, the code generated by it will look like our first version.