Sunday, February 6, 2011

It’s not about the data, it’s about what we do with it

Yeah, I promise I’ll get the NServiceBus subscriber thing published. I actually already wrote it, but I don’t wanna spoil my massive (yeah right) reader base, so I set that thing to be published on Monday.

I just saw an interesting question on the google DDD/CQRS user group. Here it is.

Hi,
Suppose that I need to get count of unique web site visitors according
to some criteria and I want that criteria to be supplied by my domain
model not through a RDBMS or another infrastructure service. My goal
is the persistency ignorance. My unique visitor business rule
currently is: It is a visitor who visited once in a day.

I figured I’d right a little blog about how I’d solve a problem like this in a behavior (instead of data) centric way.

Well, we’ve got a visitor right? Let’s write him.

   1:  public class Visitor
   2:      {
   3:          public Visitor(string identifier)
   4:          {
   5:              WebIdentifier = identifier;
   6:          }
   7:   
   8:          public string WebIdentifier { get; private set; }
   9:          public void Visit()
  10:          {
  11:              //Yeah, what goes here??   
  12:          }
  13:          public override bool Equals(object obj)
  14:          {
  15:              return base.Equals(obj);
  16:          }
  17:   
  18:          public bool Equals(Visitor other)
  19:          {
  20:              if (ReferenceEquals(null, other)) return false;
  21:              if (ReferenceEquals(this, other)) return true;
  22:              return Equals(other.WebIdentifier, WebIdentifier);
  23:          }
  24:   
  25:          public override int GetHashCode()
  26:          {
  27:              return (WebIdentifier != null ? WebIdentifier.GetHashCode() : 0);
  28:          }
  29:      }

Basically, the rules say, that we want to record a visitor when he visits for the first time in a day. So I write this test.


   1:   [Test]
   2:          public void the_last_visit_is_recorded()
   3:          {
   4:              var when = DateTime.Now;
   5:              _visitor.Visit(when);
   6:   
   7:              Assert.AreEqual(when, _visitor._lastVisit);
   8:          }
   9:          private Visitor _visitor;
  10:   
  11:          [SetUp]
  12:          public void Arrange()
  13:          {
  14:              _visitor = new Visitor("SomeSessionId");
  15:          }

Making it pass is trivial, so trivial that I’m not gunna bother showing you the code. Lets move on. We’re supposed to have some screen that shows the count of visitors per day. We’re gunna use raise a domain event if the visitors last visit was yesterday or before.


Test?


   1:   [Test]
   2:          public void a_new_visit_for_the_day_raises_a_UniqueVisitorAdded()
   3:          {
   4:              //Arrange
   5:              var yesterday = DateTime.Now.AddDays(-1);
   6:              _visitor.Visit(yesterday);
   7:              var eventWasRaised = false;
   8:              DomainEvents.Register<VisitorForFirstTimeInDay>(@event=> eventWasRaised = true);
   9:              
  10:              //act
  11:              _visitor.Visit();
  12:   
  13:              //assert
  14:              Assert.IsTrue(eventWasRaised);
  15:          }

Ok, lets go make this work….



   1:  public void Visit(DateTime? when = null)
   2:          {
   3:              when = when ?? DateTime.Now;
   4:              if (_lastVisit.Date != DateTime.Today)
   5:                  DomainEvents.Raise(new VisitorForFirstTimeInDay(WebIdentifier,when.Value));
   6:              
   7:              _lastVisit = when.Value;
   8:   
   9:          }


 


Now, we need some readmodel that counts the visitors per day.


   1:  public class VisitorsByTimeFrameReadModel
   2:      {
   3:          public VisitorsByTimeFrameReadModel(DateTime date)
   4:          {
   5:              Date = date;
   6:          }
   7:   
   8:          public DateTime Date { get; private set; }
   9:          public int UniqueVisitorsCount { get; set; }
  10:   
  11:      }

 

 

Now lets write a test to make sure we count the visitors per day with that read model.

   1:   [Test]
   2:          public void the_report_writer_adds_unique_visitor_count()
   3:          {
   4:              var when = DateTime.Now;
   5:   
   6:              var theEvent = new VisitorForFirstTimeInDay("SomeSessionId", when);
   7:              _reporter.Handle(theEvent);
   8:   
   9:              var count = _repository.GetByDate(when).UniqueVisitorsCount;
  10:              Assert.AreEqual(1,count);
  11:          }
  12:          private Visitor _visitor;
  13:          private VisitorsByDateRepository _repository;
  14:          private ReportWriter _reporter;
  15:   
  16:          [SetUp]
  17:          public void Arrange()
  18:          {
  19:              _visitor = new Visitor("SomeSessionId");
  20:              _repository = new VisitorsByDateRepository();
  21:              _reporter = new ReportWriter(_repository);
  22:          }

 

Let’s make it pass. I don’t care about persistence here really, so I’ll just throw them in a List, of course we could use an ORM instead.

   1:  public class VisitorsByDateRepository
   2:      {
   3:          private readonly List<VisitorsByTimeFrameReadModel> _allVisitors;
   4:   
   5:          public VisitorsByDateRepository()
   6:          {
   7:              _allVisitors = new List<VisitorsByTimeFrameReadModel>();
   8:          }
   9:          public VisitorsByTimeFrameReadModel GetByDate(DateTime date)
  10:          {
  11:              var dateWeCareAbout = date.Date;
  12:              var item = _allVisitors.FirstOrDefault(v => v.Date == dateWeCareAbout);
  13:              if(item == null)
  14:              {
  15:                  item = new VisitorsByTimeFrameReadModel(dateWeCareAbout);
  16:                  _allVisitors.Add(item);
  17:              }
  18:              return item;
  19:          }
  20:      }
  21:      public class VisitorsByTimeFrameReadModel
  22:      {
  23:          public VisitorsByTimeFrameReadModel(DateTime date)
  24:          {
  25:              Date = date;
  26:          }
  27:   
  28:          public DateTime Date { get; private set; }
  29:          public int UniqueVisitorsCount { get; set; }
  30:   
  31:      }

Just cause dates, get weird (at least they did when I was in high school – ha! Man, I’m funny, huh?). Let’s add some more tests to make sure things are working as expected. That we only raise the event when we need to.


   1:   public void only_one_visitor_per_day_per_identifier_is_added()
   2:          {
   3:              var eventRaisedCount = 0;
   4:              DomainEvents.Register<VisitorForFirstTimeInDay>(e => eventRaisedCount++);
   5:              var visitor = new Visitor("SomeId");
   6:              var today = DateTime.Today;
   7:              var atNoon = today.AddHours(12);
   8:              var atDinnerTime = today.AddHours(17);
   9:              visitor.Visit(today);
  10:              visitor.Visit(atNoon);
  11:              visitor.Visit(atDinnerTime);
  12:   
  13:              Assert.AreEqual(1,eventRaisedCount);
  14:   
  15:             
  16:          }

 


And there we have it… We’re now counting unique visitors per day. In a DDD kinda way (that kinda rhymed, didn’t it?). 


Whacha think?


 


You can git the code at https://github.com/elliottohara/Blogs/tree/master/Ellemy.Blogs/Ellemy.Blogs/ItsAboutBehavior

1 comment:

  1. Really enjoy reading your CQRS post. Keep them coming

    ReplyDelete