KKP BLOG

Personal space

View on GitHub
21 October 2020

DDD course basic notes!

by kkp

ReadModels and CQRS:

Base info

Event Sourcing

Base info

DDD

Base Info

Tactical Pattern

Value Object

Its not a string, date, number, etc. its not a primitive type of data. They have no identity, they are identical to another VO if they have the same values. They are also immutable. It is usually a class that compounds attributes.

Entity

Domain object with unique identity. It is usually mutable. Something like ‘Product’, ‘Invoice’ etc. ActiveRecord::Base is close to it somewhat. They can be described by Value Objects.

Aggregate

Put simply it is just few entities together.

Aggregates will often publish events. Role of aggregates is to protect certain business stuff, that need to be kept safe, when two objects state depend on each other, when one is not valid, when the other one has a certain state.

Strategic Pattern

Bounded Context

CQRS (command query responsibility segregation)

Commands and Events

Command is a user intent to do something — to perform some action. This action may or may not be possible to perform, it is the business rules that decides about in particular case. Those rules live in a model that responds to the invoked command. Event is something that describes what happened (in the past form, as this already happened and cannot be rejected like a Command). it is a result produced by a model from a Command invoked on it. Aggregate is the model, containing rules

Example of all this working together:

require 'rails_event_store'
require 'aggregate_root'
require 'arkency/command_bus'

Rails.configuration.to_prepare do
  Rails.configuration.event_store = RailsEventStore::Client.new
  Rails.configuration.command_bus = Arkency::CommandBus.new

  AggregateRoot.configure do |config|
    config.default_event_store = Rails.configuration.event_store
  end

  # Subscribe event handlers below
  Rails.configuration.event_store.tap do |store|
    store.subscribe(Movies::OnMovieAddedToRepertoire, to: [Movies::MovieAddedToRepertoire]) #READ MODEL SUBSCRIBES TO EVENT
    store.subscribe_to_all_events(->(event) { Rails.logger.info(event.type) })
  end

  Rails.configuration.command_bus.tap do |bus|
    bus.register(Movies::AddMovieToRepertoire, Movies::OnMovieAddedToRepertoire.new(
      imdb_adapter: OpenStruct.new(fetch_number: "Mocked"))
    )  # ADD COMMAND HANDLER TO THE COMMAND
  end
end
# Bounded Context for movies
module Movies
end
require_dependency 'movies/movie' #aggregate root
require_dependency 'movies/movie_added_to_repertoire' #event
require_dependency 'movies/on_movie_add_to_repertoire' #command handler
require_dependency 'movies/add_movie_to_repertoire' #command itself
require 'aggregate_root'

module Movies
  class Movie
    include AggregateRoot

    AlreadyInRepertoire = Class.new(StandardError)
    MovieOutdated = Class.new(StandardError)
    MissingPublisher = Class.new(StandardError)

    def initialize(id)
      @id = id
      @state = :draft
      @tickets = []
    end

    def add_to_repertoire(imdb_id, publisher)
      raise AlreadyInRepertoire if @state == :in_repertoire
      raise MovieOutdated if @state == :outdated
      raise MissingPublisher unless publisher
      apply MovieAddedToRepertoire.new(data: {movie_id: @id, imdb_id: imdb_id, publisher: publisher})
    end

    on MovieAddedToRepertoire do |event|
      @publisher_id = event.data[:publisher_id]
      @imdb_id = event.data[:imdb_id]
      @state = :in_repertoire
    end

    private
  end
end

Command is the thing that gets called for example in controller, and then the whole chain follow of command handler, and Read Model accessing the applied event data. Command and command handler:

# frozen_string_literal: true

module Movie
  class AddMovieToRepertoire < Command
    attribute :movie_id, Types::UUID
    attribute :publisher, Types::Publisher

    alias :aggregate_id :movie
  end
end


# frozen_string_literal: true

module Movies
  class OnMovieAddToRepertoire
    include CommandHandler

    def initialize(imdb_adapter:)
      @imdb_adapter = imdb_adapter
    end

    def call(command)
      with_aggregate(Movie, command.aggregate_id) do |movie|
        imdb_number = imdb_adapter.fetch_number
        movie.add_to_repertoire(imdb_number, command.customer_id)
      end
    end

    private

    attr_accessor :imdb_adapter
  end
end

Read Model (this those not sit in bounded context in this approach, it is located in app/read_models/movies)

# frozen_string_literal: true

module Movies
  class OnMovieAddedToRepertoire
    def call(event)
      movie = Movie.find_by(uid: event.data[:movie_id])
      movie.number = event.data[:movie_number]
      movie.customer = Publisher.find(event.data[:publisher]).name
      movie.state = "in_repertoir"
      movie.save!
    end
  end
end

And finally, the event:

#frozen_string_literal: true
module Movies
  class MovieAddedToRepertoire < Event
    attribute :movie_id, Types::UUID
    attribute :imdb_number, Types::ImdbNumber
    attribute :publisher, Types::Publisher
  end
end

So now, the whole chain of events, commands etc can be started like this.

Movies::AddMovieToRepertoire.new(movie_id: params[:movie_id], publisher: publisher)
tags: