Agregate & Identity

Агрегат

Ранее мы уже знакомились с Агрегатами. Теперь поговорим об их реализации.

Возьмем сущности пост и комментарий. В большинстве случаев они образуют агрегат, где корнем будет пост, а внутренними сущностями будут комментарии. Пост будет моделироваться Записью, а комментарии, например вектором хешей. Если комментарии могут быть иерархическими, стоит воспользоваться специализированными структурами вроде datascript.

Агрегат должен иметь идентификатор и проверять свою целостность. Идентификатор может быть глобально уникальным либо уникальным в рамках контекста. Этим контекстом может быть класс базовой сущности. Для удобства будем использовать глобально уникальные идентификаторы. Для проверки целостности будем использовать clojure.spec.

Смоделируем это с помощью протокола:

(ns app.aggregate
  (:require
   [clojure.spec.alpha :as s]))

(defprotocol Aggregate
  (id [this])
  (spec [this]))

(s/def ::aggregate #(satisfies? Aggregate %))

Пост, как корень агрегата, должен реализовать этот протокол:

(ns app.post
  (:require
   [app.aggregate :as aggregate]
   [app.post.comment :as comment]
   [clojure.spec.alpha :as s]))

(s/def ::id pos-int?)
(s/def ::title string?)
(s/def ::content string?)
(s/def ::comments (s/coll-of ::comment/comment :kind vector?))
(s/def ::post (s/keys :req-un [::id ::title ::content ::comments]))

(defrecord Post [id title content comments]
  aggregate/Aggregate
  (id [_] id)
  (spec [_] ::post))

Комментарии моделируются простыми ассоциативными массивами. Если будет нужно можно будет легко перейти на записи(record).

(ns app.post.comment
  (:require
   [clojure.spec.alpha :as s]))

(s/def ::content string?)
(s/def ::author string?)
(s/def ::comment (s/keys :req-un [::content ::author]))

Как вы уже заметили, комментарии не хранят идентификатор. Комментарии хранятся в виде вектора, и идентификатором будет индекс комментария в этом векторе:

;; map->Post генерируется при объявлении записи Post
(map->Post {:id       1
            :title    "Lorem ipsum"
            :content  "Some text"
            :comments [{:content "Awesome post!"
                        :author  "anonymous"}]})

Identity

Мы смоделировали состояние агрегата. Но нам еще нужна идентичность, чтобы работать с изменениями.

Есть 2 альтернативы: атомы и ссылки. Атомы используются для независимого изменения состояния, а ссылки для скоординированного. Вряд ли приложение будет изменять одни и те же сущности из нескольких потоков, однако важно правильно выразить намерение:

;; версия с атомами
(swap! alice-acount dec 100)
(swap! bob-acount inc 90)
(swap! bank inc 10)

;; версия с ссылками
(dosync
 (alter alice-acount dec 100)
 (alter bob-acount inc 90)
 (alter bank inc 10))

Т.е. мы показываем, что эти изменения часть транзакции, а не сами по себе. Таким образом мы будем использовать ссылки для моделирования идентичности.

Если забыли, то прочитайте параграфы про управление состоянием: 1, 2.

Агрегат изменяется как одно целое, поэтому ссылка будет хранить весь агрегат целиком.

Ссылки могут иметь валидаторы. Воспользуемся ими, чтобы проверять изменения:

  • нельзя менять идентификатор корня агрегата
  • нельзя менять класс корня агрегата (может быть неактуально для некоторых приложений)
  • нельзя записывать невалидный агрегат

Задание

В исходниках к этому параграфу есть вышеперечисленные листинги плюс неймспейс для идентичности и падающий тест валидатора. Вам нужно реализовать валидатор.

Ознакомьтесь с:

Проверьте себя