Подтвердить что ты не робот

Как использовать библиотеку компонентов Stuart Sierra в Clojure

Я пытаюсь понять, как использовать библиотеку компонентов Stuart Sierra в приложении Clojure. От просмотра его видеоролика Youtube, я думаю, что у меня есть хорошее понимание проблем, которые привели к его созданию библиотеки; однако я изо всех сил пытаюсь понять, как использовать его на новом, достаточно сложном проекте.

Я понимаю, что это звучит очень расплывчато, но мне кажется, что есть ключевое понятие, которое мне не хватает, и как только я это понимаю, я хорошо пойму, как использовать компоненты. Другими словами, документы и видео Стюарта подробно рассматриваются в деталях WHAT и WHY компонентов, но мне не хватает HOW.

Есть ли какой-нибудь подробный учебник/пошаговое руководство, которое гласит:

  • почему вы использовали бы компоненты вообще для нетривиального приложения Clojure
  • - методология того, как вы нарушаете функциональность в нетривиальном приложении Clojure, чтобы компоненты могли быть реализованы в разумно оптимальном порядке. Это достаточно просто, когда все, что у вас есть, например, базы данных, сервера приложений и уровня веб-сервера, но я изо всех сил пытаюсь понять, как вы будете использовать его для системы с множеством разных уровней, которые все должны работать согласованно.
  • способы подхода к разработке/тестированию/отказоустойчивости/и т.д. в нетривиальном приложении Clojure, которое было построено с использованием компонентов

Заранее спасибо

4b9b3361

Ответ 1

Короче говоря, Компонент - это специализированная структура DI. Он может настроить внедренную систему на основе двух карт: карты системы и карты зависимостей.

Давайте посмотрим на готовое веб-приложение (отказ от ответственности, я набрал его в форме, не запуская его):

(ns myapp.system
  (:require [com.stuartsierra.component :as component]
            ;; we'll talk about myapp.components later
            [myapp.components :as app-components]))

(defn system-map [config] ;; it conventional to have a config map, but it optional
  (component/system-map
    ;; construct all components + static config
    {:db (app-components/map->Db (:db config))
     :handler (app-components/map->AppHandler (:handler config))
     :server (app-components/map->Server (:web-server config))}))

(defn dependency-map
  ;; list inter-dependencies in either:
  ;;    {:key [:dependency1 :dependency2]} form or
  ;;    {:key {:name-arg1 :dependency1
  ;;           :name-arg2 :dependency2}} form
  {:handler [:db]
   :server {:app :handler})

;; calling this creates our system
(def create-system [& [config]]
  (component/system-using
    (system-map (or config {})
    (dependency-map)))

Это позволяет нам вызывать (create-system) для создания нового экземпляра всего нашего приложения, когда оно нам нужно.

Используя (component/start created-system), мы можем запустить системные службы, которые он предоставляет. В данном случае это веб-сервер, прослушивающий порт и открытое соединение с БД.

Наконец, мы можем остановить его с помощью (component/stop created-system), чтобы остановить работу системы (например, - остановить веб-сервер, отключиться от db).

Теперь давайте посмотрим на наш components.clj для нашего приложения:

(ns myapp.components
  (:require [com.stuartsierra.component :as component]
            ;; lots of app requires would go here
            ;; I'm generalizing app-specific code to
            ;; this namespace
            [myapp.stuff :as app]))

(defrecord Db [host port]
   component/Lifecycle
   (start [c]
      (let [conn (app/db-connect host port)]
        (app/db-migrate conn)
        (assoc c :connection conn)))
   (stop [c]
      (when-let [conn (:connection c)]
        (app/db-disconnect conn))
      (dissoc c :connection)))

(defrecord AppHandler [db cookie-config]
   component/Lifecycle
   (start [c]
      (assoc c :handler (app/create-handler cookie-config db)))
   (stop [c] c))

;; you should probably use the jetty-component instead
;; https://github.com/weavejester/ring-jetty-component
(defrecord Server [app host port]
   component/Lifecycle
   (start [c]
      (assoc c :server (app/create-and-start-jetty-server
                        {:app (:handler app)
                         :host host 
                         :port port})))
   (stop [c]
      (when-let [server (:server c)]
         (app/stop-jetty-server server)
      (dissoc c :server)))

Так что мы только что сделали? Мы получили перезаряжаемую систему. Я думаю, что некоторые разработчики clojurescript, использующие figwheel, начинают видеть сходство.

Это означает, что мы можем легко перезапустить нашу систему после перезагрузки кода. На user.clj!

(ns user
    (:require [myapp.system :as system]
              [com.stuartsierra.component :as component]
              [clojure.tools.namespace.repl :refer (refresh refresh-all)]
              ;; dev-system.clj only contains: (def the-system)
              [dev-system :refer [the-system]])

(def system-config
  {:web-server {:port 3000
                :host "localhost"}
   :db {:host 3456
        :host "localhost"}
   :handler {cookie-config {}}}

(def the-system nil)

(defn init []
  (alter-var-root #'the-system
                  (constantly system/create-system system-config)))

(defn start []
  (alter-var-root #'the-system component/start))

(defn stop []
  (alter-var-root #'the-system
                  #(when % (component/stop %))))

(defn go []
  (init)
  (start))

(defn reset []
  (stop)
  (refresh :after 'user/go))

Чтобы запустить систему, мы можем напечатать это в нашем ответе:

(user)> (reset)

Что перезагрузит наш код и перезапустит всю систему. Он отключит выходящую систему, которая работает, если она работает.

Мы получаем другие преимущества:

  • Сквозное тестирование легко, просто отредактируйте конфигурацию или замените компонент, чтобы он указывал на внутрипроцессные сервисы (я использовал его, чтобы указать на внутрипроцессный сервер kafka для тестов).
  • Вы можете теоретически порождать ваше приложение несколько раз для одной и той же JVM (на самом деле не так практично, как в первом пункте).
  • Вам не нужно перезапускать REPL при внесении изменений в код и необходимости перезапускать свой сервер
  • В отличие от перезагрузки кольца, мы получаем единый способ перезапустить наше приложение независимо от его назначения: фоновый работник, микросервис или система машинного обучения могут быть спроектированы одинаково.

Стоит отметить, что, поскольку все находится в процессе, Компонент не обрабатывает ничего, связанного с переключением при сбое, распределенными системами или неисправным кодом;)

Компонент может помочь вам в управлении большим количеством "ресурсов" (объектов с состоянием):

  • Соединения со службами (очереди, базы данных и т.д.)
  • Прохождение времени (планировщик, хрон и т.д.)
  • Ведение журнала (ведение журнала приложений, ведение журнала исключений, метрики и т.д.)
  • File IO (хранилище BLOB-объектов, локальная файловая система и т.д.)
  • Входящие клиентские соединения (веб, сокеты и т.д.)
  • Ресурсы ОС (устройства, пулы потоков и т.д.)

Компонент может показаться излишним, если у вас есть только веб-сервер + БД. Но немногие веб-приложения таковы в наши дни.

Примечание: Перемещение the-system в другое пространство имен уменьшает вероятность обновления the-system var при разработке (например, вызов refresh вместо reset).