Состояние сущностей¶
Сделаем одну сущность Person в отдельном проекте.
Есть несколько способов моделировать состояние сущности в clojure
Использовать мапы:
{:id 1 :type :person :name "Alise"}
Его минус в том, что реализовать полиморфизм для мап можно только с помощью мультиметодов. Также нам явно нужно указывать тип.
Использовать записи:
(defrecord Person [id name])
При этом мы можем использовать как протоколы, так и мультиметоды. Каждая сущность имеет свой тип(класс). При этом записи поддерживают интерфейс мап. И их объявление является документацией того, какие поля они имеют.
Модель datomic. Не рассматриваем.
Мы будем использовать Записи.
Для начала напишем тест на конструктор.
Конструктор - это обычная функция, возвращающая экземпляр Person
.
Назовем наш конструктор build
:
(ns app.person
(:require
[clojure.test :as t]))
(declare build)
(declare person?)
(t/deftest build-test
(let [params {:name "Alice"}
person (build params)]
(t/is (person? person))))
Добавим реализацию:
(defrecord Person [id name])
(defn build [{:keys [name]}]
(map->Person {:name name}))
(defn person? [x] (instance? Person x))
Отмечу, что наш конструктор не устанавливает идентификатор. И наши сущности получаются неполноценными.
Напишем спецификацию на наш конструктор, чтобы проверять корректность возвращаемого значения.
(ns app.person
(:require
[clojure.test :as t]
[clojure.spec.alpha :as s]
[orchestra.spec.test :as st]))
(s/def ::id pos-int?)
(s/def ::name string?)
(s/def ::person (s/keys :req-un [::id ::name]))
(defrecord Person [id name])
(s/fdef build
:args (s/cat :params (s/keys :req-un [::name]))
:ret ::person)
(defn build [{:keys [name]}]
(map->Person {:name name}))
(defn person? [x] (instance? Person x))
;; подменяем функции на вариант, проверяющий спецификацию
(st/instrument)
(t/deftest build-test
(let [params {:name "Alice"}
person (build params)]
(t/is (person? person))))
Ожидаемо наш тест не прошел, т.к. build
не устанавливает
обязательное для персоны(::person
) поле id
.
Мы пока не знаем, как мы будем сохранять наши сущности, но уже сейчас нам нужно генерировать идентификаторы. Отложим принятие решения о конкретной реализации генератора и объявим абстракцию генератора:
(defprotocol IdGenerator
(-generate-id [this]))
(declare ^:dynamic *id-generator*)
(s/fdef generate-id
:ret ::id)
(defn generate-id []
(-generate-id *id-generator*))
Т.е. наш генератор должен реализовывать протокол IdGenerator
и его экземпляр должен храниться в динамической переменной
*id-generator
.
Теперь мы можем использовать наш генератор в конструкторе:
(defn build [{:keys [name]}]
(map->Person {:id (generate-id)
:name name}))
Для тестов напишем фейковую реализацию, хранящую данные в памяти. Подробнее про фейки, моки и стабы можно посмотреть тут.
(deftype FakeIdGenerator [counter]
IdGenerator
(-generate-id [_]
(swap! counter inc)))
(defn build-fake-id-generator []
(FakeIdGenerator. (atom 0)))
Теперь перед каждым тестом нужно создавать экземпляр генератора и устанавливать его в динамическую переменную. Для этого воспользуемся фикстурами:
(t/use-fixtures :each (fn [test]
(binding [*id-generator* (build-fake-id-generator)]
(test))))
И теперь тест проходит.
Очевидно, что мешать весь этот функционал в одном файле - плохая идея. Не пугайтесь, я дам в дальнейшем пример структуры.
Задание¶
- Возьмите за основу пример.
- Реализуйте Пользователя(User) с набором полей:
id
,login
,full-name
,password-digest
,created-at
. Параметры конструктора:login
,full-name
,password
. - Функцию
(defn authenticated? [user password])
для проверки пароля.
Вам понадобится абстрактный PasswordHasher
для получения password-digest
и сверки пароля.
По аналогии нужно предусмотреть возможность задавать текущее время в тестах.
Проверьте себя: