Quantcast
Viewing all articles
Browse latest Browse all 25

Domain Events – Salvation

Image may be NSFW.
Clik here to view.
sphere

I’ve been hearing from people that have had a great deal of success using the Domain Event pattern and the infrastructure I previously provided for it in Domain Events – Take 2. I’m happy to say that I’ve got an improvement that I think you’ll like. The main change is that now we’ll be taking an approach that is reminiscent to how events are published in NServiceBus.

Background

Before diving right into the code, I wanted to take a minute to recall how we got here.

It started by looking for how to create fully encapsulated domain models.

The main assertion being that you do *not* need to inject anything into your domain entities.

Not services. Not repositories. Nothing.

Just pure domain model goodness.

Make Roles Explicit

I’m going to take the advice I so often give. A domain event is a role, and thus should be represented explicitly:

   1:  public interface IDomainEvent {}

If this reminds you of the IMessage marker interface in nServiceBus, you’re beginning to see where this is going…

How to define domain events

A domain event is just a simple POCO that represents an interesting occurence in the domain. For example:

   1:  public class CustomerBecamePreferred : IDomainEvent 
   2:  {
   3:      public Customer Customer { get; set; }
   4:  }

For those of you concerned about the number of events you may have, and therefore are thinking about bunching up these events by namespaces or things like that, slow down. The number of domain events and their cohesion is directly related to that of the domain model.

If you feel the need to split your domain events up, there’s a good chance that you should be looking at splitting your domain model too. This is the bottom-up way of identifying bounded contexts.

How to raise domain events

In your domain entities, when a significant state change happens you’ll want to raise your domain events like this:

   1:  public class Customer
   2:  {
   3:      public void DoSomething()
   4:      {
   5:          DomainEvents.Raise(new CustomerBecamePreferred() { Customer = this });
   6:      }
   7:  }

We’ll look at the DomainEvents class in just a second, but I’m guessing that some of you are wondering “how did that entity get a reference to that?” The answer is that DomainEvents is a static class. “OMG, static?! But doesn’t that hurt testability?!” No, it doesn’t. Here, look:

Unit testing with domain events

One of the things we’d like to check when unit testing our domain entities is that the appropriate events are raised along with the corresponding state changes. Here’s an example:

   1:  public void DoSomethingShouldMakeCustomerPreferred()
   2:  {
   3:      var c = new Customer();
   4:      Customer preferred = null;
   5:   
   6:      DomainEvents.Register<CustomerBecamePreferred>(
   7:          p => preferred = p.Customer
   8:              );
   9:   
  10:      c.DoSomething();
  11:      Assert(preferred == c && c.IsPreferred);
  12:  }

As you can see, the static DomainEvents class is used in unit tests as well. Also notice that you don’t need to mock anything – pure testable bliss.

Who handles domain events

First of all, consider that when some service layer object calls the DoSomething method of the Customer class, it doesn’t necessarily know which, if any, domain events will be raised. All it wants to do is its regular schtick:

   1:  public void Handle(DoSomethingMessage msg)
   2:  {
   3:      using (ISession session = SessionFactory.OpenSession())
   4:      using (ITransaction tx = session.BeginTransaction())
   5:      {
   6:          var c = session.Get<Customer>(msg.CustomerId);
   7:          c.DoSomething();
   8:   
   9:          tx.Commit();
  10:      }
  11:  }

The above code complies with the Single Responsibility Principle, so the business requirement which states that when a customer becomes preferred, they should be sent an email belongs somewhere else.

Notice that the key word in the requirement – “when”.

Any time you see that word in relation to your domain, consider modeling it as a domain event.

So, here’s the handling code:

   1:  public class CustomerBecamePreferredHandler : Handles<CustomerBecamePreferred>
   2:  { 
   3:     public void Handle(CustomerBecamePreferred args)
   4:     {
   5:        // send email to args.Customer
   6:     }
   7:  } 

This code will run no matter which service layer object we came in through.

Here’s the interface it implements:

   1:  public interface Handles<T> where T : IDomainEvent
   2:  {
   3:      void Handle(T args); 
   4:  } 

Fairly simple.

Please be aware that the above code will be run on the same thread within the same transaction as the regular domain work so you should avoid performing any blocking activities, like using SMTP or web services. Instead, prefer using one-way messaging to communicate to something else which does those blocking activities.

Also, you can have multiple classes handling the same domain event. If you need to send email *and* call the CRM system *and* do something else, etc, you don’t need to change any code – just write a new handler. This keeps your system quite a bit more stable than if you had to mess with the original handler or, heaven forbid, service layer code.

Where domain event handlers go

These handler classes do not belong in the domain model.

Nor do they belong in the service layer.

Well, that’s not entirely accurate – you see, there’s no *the* service layer. There is the part that accepts messages from clients and calls methods on the domain model. And there is another, independent part that handles events from the domain. Both of these will probably make use of a message bus, but that implementation detail shouldn’t deter you from keeping each in their own package.

The infrastructure

I know you’ve been patient, reading through all my architectural blah-blah, so here it is:

   1:  public static class DomainEvents
   2:  { 
   3:      [ThreadStatic] //so that each thread has its own callbacks
   4:      private static List<Delegate> actions;
   5:   
   6:      public static IContainer Container { get; set; } //as before
   7:   
   8:      //Registers a callback for the given domain event
   9:      public static void Register<T>(Action<T> callback) where T : IDomainEvent
  10:      {
  11:         if (actions == null)
  12:            actions = new List<Delegate>();
  13:   
  14:         actions.Add(callback);
  15:     }
  16:   
  17:     //Clears callbacks passed to Register on the current thread
  18:     public static void ClearCallbacks ()
  19:     {
  20:         actions = null;
  21:     }
  22:   
  23:     //Raises the given domain event
  24:     public static void Raise<T>(T args) where T : IDomainEvent
  25:     {
  26:        if (Container != null)
  27:           foreach(var handler in Container.ResolveAll<Handles<T>>())
  28:              handler.Handle(args);
  29:   
  30:        if (actions != null)
  31:            foreach (var action in actions)
  32:                if (action is Action<T>)
  33:                    ((Action<T>)action)(args);
  34:     }
  35:  } 

Notice that while this class *can* use a container, the container isn’t needed for unit tests which use the Register method.

When used server side, please make sure that you add a call to ClearCallbacks in your infrastructure’s end of message processing section. In nServiceBus this is done with a message module like the one below:

   1:  public class DomainEventsCleaner : IMessageModule
   2:  { 
   3:      public void HandleBeginMessage() { }
   4:   
   5:      public void HandleEndMessage()
   6:      {
   7:          DomainEvents.ClearCallbacks();
   8:      }
   9:  }

The main reason for this cleanup is that someone just might want to use the Register API in their original service layer code rather than writing a separate domain event handler.

Summary

Like all good things in life, 3rd time’s the charm.

It took a couple of iterations, and the API did change quite a bit, but the overarching theme has remained the same – keep the domain model focused on domain concerns. While some might say that there’s only a slight technical difference between calling a service (IEmailService) and using an event to dispatch it elsewhere, I beg to differ.

These domain events are a part of the ubiquitous language and should be represented explicitly.

CustomerBecamePreferred is nothing at all like IEmailService.

In working with your domain experts or just going through a requirements document, pay less attention to the nouns and verbs that Object-Oriented Analysis & Design call attention to, and keep an eye out for the word “when”. It’s a critically important word that enables us to model important occurrences and state changes.

What do you think? Are you already using this approach? Have you already tried it and found it broken in some way? Do you have any suggestions on how to improve it?

Let me know – leave a comment below.


Viewing all articles
Browse latest Browse all 25

Trending Articles