【问题标题】:How to use Stuart Sierra's component library in Clojure [closed]如何在 Clojure 中使用 Stuart Sierra 的组件库 [关闭]
【发布时间】:2015-05-18 05:11:51
【问题描述】:

我正在努力弄清楚如何在 Clojure 应用程序中使用 Stuart Sierra's component library。通过观看他的Youtube video,我想我已经很好地掌握了导致他创建图书馆的问题;但是我正在努力弄清楚如何在一个新的、相当复杂的项目中实际使用它。

我意识到这听起来很模糊,但感觉好像缺少一些关键概念,一旦我理解了它,我就会很好地掌握如何使用组件。换句话说,Stuart 的文档和视频非常详细地介绍了组件的用途和原因,但我错过了如何。

是否有任何详细的教程/演练进入:

  • 为什么要为一个重要的 Clojure 应用程序使用组件
  • 一种方法,用于分解非平凡 Clojure 应用程序中的功能,以便可以以合理的最佳方式实现组件。当你所拥有的只是例如,它相当简单。一个数据库、一个应用服务器和一个 Web 服务器层,但我很难掌握如何将它用于具有许多不同层的系统,这些层都需要连贯地协同工作
  • 处理开发/测试/故障转移/等的方法。在使用组件构建的非平凡 Clojure 应用中

提前致谢

【问题讨论】:

  • 我只想对这样一个答案的效用表示“阿门”。
  • 组件在 Walmart 中使用,非常不平凡的 Clojure 应用程序。这里有一个谈话youtube.com/watch?v=av9Xi6CNqq4 可能会有所启发。

标签: clojure dependency-injection functional-programming components


【解决方案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's conventional to have a config map, but it's 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) 停止它以停止系统运行(例如 - 停止 Web 服务器,断开与 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)))

那么我们刚刚做了什么?我们得到了一个可重新加载的系统。我认为一些使用 figwheel 的 clojurescript 开发人员开始看到相似之处。

这意味着我们可以在重新加载代码后轻松地重新启动系统。转到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))

要运行系统,我们可以在 repl 中输入:

(user)> (reset)

这将重新加载我们的代码,并重新启动整个系统。如果它启动,它将关闭正在运行的现有系统。

我们还有其他好处:

  • 端到端测试很简单,只需编辑配置或替换组件以指向进程内服务(我已使用它指向进程内 kafka 服务器进行测试)。
  • 理论上,您可以为同一个 JVM 多次生成应用程序(实际上不如第一点实用)。
  • 进行代码更改时无需重新启动 REPL,而必须重新启动服务器
  • 与 ring reload 不同,我们有一种统一的方式来重新启动我们的应用程序,无论其用途如何:后台工作器、微服务或机器学习系统都可以以相同的方式构建。

值得注意的是,由于一切都在进行中,因此组件不会处理与故障转移、分布式系统或错误代码相关的任何事情;)

组件可以帮助您在服务器中管理大量“资源”(也称为有状态对象):

  • 与服务的连接(队列、数据库等)
  • 时间流逝(调度程序、cron 等)
  • 日志记录(应用日志记录、异常日志记录、指标等)
  • 文件 IO(blob 存储、本地文件系统等)
  • 传入的客户端连接(Web、套接字等)
  • 操作系统资源(设备、线程池等)

如果您只有一个 Web 服务器 + 数据库,组件可能看起来有点矫枉过正。但是现在很少有网络应用程序是这样的。

旁注:the-system 移动到另一个命名空间会降低在开发时刷新the-system var 的可能性(例如,调用refresh 而不是reset)。

【讨论】:

    猜你喜欢
    • 2017-08-01
    • 1970-01-01
    • 1970-01-01
    • 2020-05-14
    • 1970-01-01
    • 1970-01-01
    • 2013-12-07
    • 2018-04-01
    • 2015-12-25
    相关资源
    最近更新 更多