Skip to main content

Changing the mindset - part 3 / 4 - Segregation of Responsibility

In the previous part we created quite verbose object oriented Model of our Domain. The Domain itself is extremely simple, but try to imagine your Model, which you are working on day by day, that would be so explicit. Wouldn't be easier to understand it? Whatever you can do on an object, you do it directly through methods, which names and arguments, comes strictly from the Domain Language. And even those objects' names come from this Language too. Well, this is just OOP done right, isn't it?

There is actually one issue with that approach. We want to show some of the data to the user. And if so, then we have to expose internals. So in addition to our well-crafted, encapsulating behavior methods, we have leaky getters...

Clean the model

Let's do something that may look a little strange for the first time. Remove those getters. For a moment don't think about the View. Doesn't the Model look pure and clean without them? It has only behavior as a boundary.

So, how to generate the View? First of all we have to note, that we may have many Views (screens) looking at the same data from different angles. In classic (anemic) approach we would have one Model and many, many SQL joins. But we can do something different - something more explicit.

We can create another, second Model strictly optimized for each single View. What is more - we can store those Views in some document database (NoSQL), so our reads can become really fast. I won't go into the details with their implementation, since they are fairly simple anemic-like classes created for each particular View.

So we will have the Write side (modeled in terms of Ubiquitous Language, with strong boundaries definition), and the Read side (modeled as classic anemic entities highly optimized for particular Views). Both sides will be fully separated and responsible for different user actions.

Probably you are wondering - how to feed those Views with data? If we are using single SQL database, we can just create database views and map them to View classes with some ORM. But if we use many data storages, then we have to make something different.

The Past Simple tense

There is another approach, which gives us a lot more opportunities. After each method call in Domain Objects we will generate one or more Events that describe a state transition, and send them to the Event Bus. They should be named in Past Simple tense and tell what happened (not what will happen, and not what might). They are supposed to be relatively small, with no business logic - only state changes.

Here are the simplest implementations of aggregate’s methods:

@Entity
@Table(name=”matches”)
public class Match extends AggregateRoot {
  @Id
  private UUID id; 
  @Embedded
  private Team homeTeam; 
  @Embedded
  private Team awayTeam;
  @Basic
  private Date matchDate
  @Basic
  private Date finishDate

  @Embedded
  private Score score; 

  public Match(UUID id, Team homeTeam, Team awayTeam) {
    this.id = id;
    this.homeTeam = homeTeam;
    this.awayTeam = awayTeam; 

    apply(new MatchCreatedEvent(id, homeTeam.getId(), awayTeam.getId());
  }
  
  public setupMatchDate(Date matchDate) throws MatchAlreadyFinishedException {
    if (finishDate != null) {
      throw new MatchAlreadyFinishedException();
    }
    this.matchDate = matchDate;

    apply(new MatchDateSetUpEvent(id, matchDate);
  }
  
  public finishWithScore(Score score, Date finishDate)
      throws MatchFinishedBeforeStartException, MatchAlreadyFinishedExcepion {
    if (matchDate == null || finishDate.before(matchDate)) {
      throw new MatchFinishedBeforeStartException();
    }
    if (this.finishDate != null) {
      throw new MatchAlreadyFinishedException();
    }
    this.finishDate = finishDate;
    this.score = score;

    apply(new MatchFinishedEvent(id, finshDate,
        score.getHomeGoals(), score.getAwayGoals());
  }
}

As you can see, there are three Events (MatchCreatedEvent, MatchDateSetUpEvent, and MatchFinishedEvent) that will be applied to the Aggregate through the method apply()from the base class (AggregateRoot). It will take care to replay all those Events on the Event Bus after the end of the transaction.

On the other side of the Bus, there will be an EventHandler that will be waiting for those Events and populate the View Model. It is worth to notice, that Events are sent after the transaction on the Write side finishes. We have to understand that the View side may not be consistent for all the time with the Write side, but eventually it will...

Additionally, if our Read side is generated out of Events, then we need to store those Events in some persistent Event Store… We can for example serialize them into documents, so hello NoSQL again. The side effect is that we will have the duplication of data on the Write side – in regular SQL database, and in Event Store. And those stores has to be consistent all the time...

Throw away the database!

If we have two sources of data in our Write side, than we can really fast get into troubles – we don’t want to support distributed transaction, don’t we? So we need to get rid of one of those databases.

Our choice is… Yes, you guessed it - the SQL database. Delete it!

Scary, isn’t it? ;)

But stop for a moment and imagine – no more problems with mappings, joins, queries, database optimization…


It can be done, however we have to make some assumptions. Our Aggregates have to be loaded from Events that they produced in the past, so we have to change something in our Model.

First of all, we need to create appropriate methods for handling Events. Those methods will be called by a Repository that load the Aggregate (I won’t go much into the details how Repository should be organized, since it is quite a big topic), and should not be exposed to the outside world. Moreover we need to remove any property changes from the business methods, so that we don't repeat ourselves. And in the end, we may (and want) remove ORM annotations. 

So our code can look like this:

public class Match extends AggregateRoot { 
  private UUID id; 

  private Team homeTeam; 
  private Team awayTeam; 
  private Date matchDate 
  private Date finishDate 
  private Score score;

  public Match(UUID id, Team homeTeam, Team awayTeam) {
    apply(new MatchCreatedEvent(id, homeTeam.getId(), awayTeam.getId());
  }

  private void handle(MatchCreatedEvent event) {
    this.id = event.getId();
    this.homeTeam = new Team(event.getHomeTeamId());
    this.awayTeam = new Team(event.getAwayTeam());
  }
  
  public setupMatchDate(Date matchDate) throws MatchAlreadyFinishedException {
    if (finishDate != null) {
      throw new MatchAlreadyFinishedException();
    }
    apply(new MatchDateSetUpEvent(id, matchDate);
  }

  private void handle(MatchDateSetUpEvent event) {
    this.matchDate = event.getMatchDate();
  }
  
  public finishWithScore(Score score, Date finishDate)
      throws MatchFinishedBeforeStartException, MatchAlreadyFinishedExcepion {
    if (matchDate == null || finishDate.before(matchDate)) {
      throw new MatchFinishedBeforeStartException();
    }
    if (this.finishDate != null) {
      throw new MatchAlreadyFinishedException();
    }
    apply(new MatchFinishedEvent(
        id, finshDate, score.getHomeGoals(), score.getAwayGoals());
  }

  private void handle(MatchFinishedEvent event) {
    this.finishDate = event.getFinishDate();
    this.score = new Score(event.getHomeGoals(), event.getAwayGoals());
  }
}

As you can see, we have now a really nice looking Aggregate that exposes only behavior, is Event Sourced and doesn’t have any ORM annotations.

To load this Aggregate from our Event Store, the Repository loads all Events that were applied to this particular Aggregate, and then applies them on the instance of the Aggregate in order they were generated. One may argue, if this may be slow (after all we can have hundreds of Events for a single instance), but we can always make a Snapshot of the current Aggregate state, and replay only Events that were generated after the Snaphot had been taken.

After loading the Aggregate, we can execute behavioral methods on it. They will generate more Events, which will be added to previous ones, stored. And after the transaction ends, they will be propagated to the View side and our view model will be updated. Eventually…

Oh, and one more thing - how hard would it be to give the user some historical data from some specified point of time? If we have everything stored as a sequence of Events... Well, I will leave this up to you, dear Reader to consider. ;)



Changing the mindset series:
  1. Changing the mindset - part 1 / 4 - Classic approach
  2. Changing the mindset - part 2 / 4 - Modeling the Domain
  3. Changing the mindset - part 3 / 4 - Segregation of Responsibility
  4. Changing the mindset - part 4 / 4 - Subtle difference

Comments

  1. The most simple, clear and smooth introduction to CqRS and Event Sourcing. Great Job!

    ReplyDelete

Post a Comment