Chaining Events, Take One
In an execellent book on Domain-Driven Design that I’m reading in my spare time, they make a lot of use of domain events. I’ve written about a strongly typed publish–subscribe infrastructure in March already to get you started. Some of the book samples include what seems to be ad-hoc event subscribers. I try to port that to Swift.
Here’s a C# example from the book, page 679:
public void Execute(ModifyCategoryCommand command)
{
var category = _catalogueRepository.FindBy(command.Id);
using (DomainEvents.Register<CategoryUpdated>(onCategoryUpdated))
{
category.Update(command);
}
}
private void onCategoryUpdated(CategoryUpdated @event)
{
_catalogueViewModel.Update(event)
}
That’s registering onCategoryUpdated
as event handler for a CategoryUpdated
event being fired by category.Update(command)
. It reads as if this is a complicated way to pass the result of the Update
call to onCategoryUpdated
.
If events are passed asynchronously, how’s the event listener kept alive? When is it de-registered again? Or is the owning object kept alive very long, the handler registered only once, and subsequent executions fall through? I assume the latter, but that doesn’t make much sense. If an event is fired but reaches onCategoryUpdated
, say, 10 minutes late, how’s that resulting in a valid view model update? Does the view model discard events that don’t match the ID, for example?
I want to achieve single-response listeners first. When it receives an event, the listener can be discarded.
This using
clause was weird at first. Here’s the requirements to achieve that in Swift:
DomainEvents.register<T>(block: (T) -> Void)
– register for events of typeT
and handle them using the block.using(eventListener: NSObjectProtocol, block: () -> Void)
can replicate the code above;NSObjectProtocol
is what you get when you useNSNotificationCenter.addObserver
.- Remove the notification observer (or event listener) after the
using
part for one-off triggers, or keep it alive .
Since there’s no built-in using
command in Swift, here’s the problem: after the attacked closure is executed, the surrounding execute
method would finish as well. There’s nothing left to do. The event listener would be dangling around. If we removed it from NSNotificationCenter
before leaving execute
, say with the new defer
command, then the execution of this method might still end before the event is dispatched on who knows which queue.
So our Swift version of using
has to be asynchronous.
Here’s the best idea I had: create an event listener just like above, but automatically remove it after, say, 2 minutes. Maybe even earlier. Essentially, give it a timeout after which it’s going to be removed from the NSNotificationCenter
and deallocated.
This means we need a small helper object which holds on to an event listener reference and a timeout block.
I played around with the names a bit and came up with a solution that can be used like this:
func execute(ModifyCategoryCommand command) {
let category = catalogueRepository.findBy(command.categoryId)
let eventExpectation = subscribeUsing(onCategoryUpdated)
do {
try eventExpectation.fulfillExecuting() {
category.update(command)
}
} catch {
NSLog("Could not update category")
}
}
private func onCategoryUpdated(CategoryUpdated event) {
catalogueViewModel.update(event)
}
I made it so fulfilExecuting
takes block of () throws -> Void)
and rethrows the error to the caller, that is execute(_:)
. I found this useful because the Domain changes which are supposed to fire events sometimes can fail. That’s unlikely for model changes like category.update(_:)
. But it will happen with Domain Services which encapsulate a complex sequence.
Another approach is to not throw (since that’s just another kind of return
) but publish a failure event instead.
You can see most of a working example as a Gist on GitHub.
It can handle more complex setups, which don’t read as nice, though:
let eventExpectation = subscribeUsing(onCategoryUpdated)
.passingTest({ $0.categoryId == aCategoryId }) // do not apply to all events of that kind
.onFailure({ NSLog("Expected XYZ event but timed out.") })
try eventExpectation.fulfilExecuting() {
// ...
}
Feedback very welcome!