clojure_Clojure整合运动

clojure

介绍 (Introduction)

Last December when I was having a vacation before joining Metosin I decided to train my Clojure skills a bit by implementing my Clojure Simple Server exercise once more. You can read more about that exercise in my previous blog post, Clojure Impressions Round Three. In that blog post, I stated: “You Can Do It Without Application State Management Libraries”. I still agree with that statement, but… When I started working at Metosin last January I talked with the Metosin guys regarding state management in Clojure applications and most of Metosin clojurists were using Integrant. I had some conversations regarding whether to create your own state management (as I did in my previous exercise) or whether to use some state management library. According to those conversations I realized that since these guys are really good clojurists and they are using a state management library there must be compelling reasons that one should use them. So, I decided to implement my Clojure Simple Server exercise once more, this time with Integrant. At the same time I did some refactorings to make the code a bit simpler, and also added a new data store: Postgres (there was a dummy CSV and DynamoDB as data store options before that). The refactoring work was also a good test bench for using Integrant and how a good state management library helps in refactoring work.

去年12月,在加入Metosin之前休假时,我决定通过再次实施Clojure Simple Server练习来稍微训练一下Clojure技能。 您可以在我之前的博客文章Clojure Impressions Round 3中了解有关该练习的更多信息。 在该博客文章中,我说过: “没有应用程序状态管理库就可以做到” 。 我仍然同意这一说法,但是……当我去年1月开始在Metosin工作时,我与Metosin的人员讨论了Clojure应用程序中的状态管理,并且大多数Metosin Clojurists正在使用Integrant 。 关于是否要创建自己的状态管理(如我在上一个练习中所做的那样)还是是否使用某些状态管理库,我进行了一些对话。 通过这些交谈,我意识到,由于这些人确实是优秀的clojuristist,并且他们正在使用状态管理库,因此必须有令人信服的理由来使用它们。 因此,我决定再次使用Integrant实施Clojure Simple Server练习。 同时,我进行了一些重构以使代码更简单,并且还添加了一个新的数据存储:Postgres(之前有一个虚拟CSV和DynamoDB作为数据存储选项)。 重构工作也是使用Integrant的良好测试平台,以及良好的状态管理库如何帮助进行重构工作。

You can find the project in Github.

您可以在Github中找到该项目。

Disclaimer: This was just a quick personal exercise to learn how to use Integrant so that it makes the Clojure development with state handling smoother. The exercise is by no means a perfect example how to setup a web shop or web application — there are many peculiarities for historical reasons (e.g. domain entities are passed with simple vectors — I should have used maps instead etc.).

免责声明:这只是学习个人快速技巧,以学习如何使用Integrant,从而使具有状态处理功能的Clojure开发更加顺畅。 该练习绝不是如何设置网上商店或Web应用程序的完美示例-出于历史原因有很多特殊性(例如,通过简单的矢量传递域实体-我应该改用地图等)。

为什么要整合? (Why Integrant?)

Why should you use Integrant or some other state management library and not handle state yourself? Well, as I said in my previous blog post, Clojure is pretty flexible and you can quite easily handle state e.g. using a simple atom. But a good state management library like Integrant provides much more. With Integrant, you can have a simple edn file to manage your components and how they relate to each other to make the overall state in your application. Another powerful feature of Integrant is the capability easily to create different states for different purposes (e.g. state for production and state for running tests) and to reset these states. Let’s unfold these capabilities in the following chapters.

为什么要使用Integrant或其他状态管理库而不自己处理状态? 嗯,正如我在前一篇博客文章中所说的那样,Clojure非常灵活,您可以轻松地处理状态,例如使用简单的atom 。 但是像Integrant这样的优秀状态管理库可以提供更多功能。 使用Integrant,您可以有一个简单的edn文件来管理您的组件以及它们如何相互关联以使应用程序处于整体状态。 Integrant的另一个强大功能是可以轻松创建用于不同目的的不同状态(例如,生产状态和运行测试状态)并重置这些状态的功能。 让我们在接下来的章节中展开这些功能。

集成状态配置为Edn (Integrant State Configuration as Edn)

You can create a configuration file to provide the initial setup of your state and how it is built with different components and their interactions. This is pretty nice. Look at the config.edn file. You can see that I have created three “data stores”: csv, dynamodb, and postgres and an Integrant configuration for each of these:

您可以创建一个配置文件,以提供状态的初始设置以及如何使用不同的组件及其相互作用构建状态。 很好 查看config.edn文件。 您可以看到我已经创建了三个“数据存储”:csv,dynamodb和postgres,并且为每个这些创建了一个Integrant配置:

:backend/csv {:profile #ig/ref :backend/profile
:active-db #ig/ref :backend/active-db
:data-dir "dev-resources/data"}:backend/ddb {:active-db #ig/ref :backend/active-db
:ss-table-prefix "ss"
:ss-env #profile {:prod "prod"
:dev "dev"
:test "test"}
:endpoint {:protocol :http :hostname "localhost" :port 8000}
:aws-profile "local-dynamodb"}:backend/postgres {:active-db #ig/ref :backend/active-db
:adapter "postgresql"
:username #or [#env DB_USERNAME "simpleserver"]
:password #or [#env DB_PASSWORD "simpleserver"]
:server-name #or [#env DB_HOST "localhost"]
:port-number #long #or [#env DB_PORT 5532]
:database-name #or [#env DB_NAME "simpleserver"]
...

Then the service component has references to these data store components:

然后,服务组件将引用这些数据存储组件:

; Gather different data store services here.
:backend/service {:profile #ig/ref :backend/profile
:active-db #ig/ref :backend/active-db
:csv #ig/ref :backend/csv
:ddb #ig/ref :backend/ddb
:postgres #ig/ref :backend/postgres
}

I’m omitting other components, jetty, nrepl etc. It would take quite an effort to create this kind of capability yourself. Sure, if your state is very simple you don’t need this capability but when your state has many components and different interactions between them this feature is pretty nice.

我省略了其他组件(码头,nrepl等)。自己创建这种功能将需要花费很多精力。 当然,如果您的状态非常简单,则不需要此功能,但是当您的状态包含许多组件并且它们之间有不同的交互时,此功能非常好。

For reading the Integrant configuration I use Aero. Aero provides a nice way to inject environment variables to the values inside components and mechanisms for simple conditionals and conversions from strings to numerics (e.g. :port-number #long #or [#env DB_PORT 5532] - i.e. we are using environment variable DB_PORT, and if it’s not found, use default value 5532, and convert the string value to long).

为了读取Integrant配置,我使用Aero 。 Aero提供了一种很好的方式将环境变量注入组件和机制内部的值,以进行简单的条件转换以及从字符串到数字的转换(例如:port-number #long #or [#env DB_PORT 5532] -例如,我们正在使用环境变量DB_PORT, ;如果找不到,请使用默认值5532,并将字符串值转换为long)。

不同国家的整合 (Different States with Integrant)

Another powerful feature of Integrant is to create different states for different purposes. E.g. in this exercise I have created two states, one for running the system in production (and in development) and one for running tests. The production / development state reads the config.edn configuration and constructs the actual state in core.clj file:

集成的另一个强大功能是为不同目的创建不同状态。 例如,在本练习中,我创建了两种状态,一种用于在生产(和开发)中运行系统,另一种用于运行测试。 生产/开发状态读取config.edn配置并在core.clj文件中构造实际状态:

E.g. the different data stores each have a very different state manifestation:

例如,不同的数据存储各自具有非常不同的状态表现形式:

(defmethod ig/init-key :backend/csv [_ {:keys [profile active-db data-dir]}]
(log/debug "ENTER ig/init-key :backend/csv"); We simulate this data store using atom.; We initialize the "db" from :data-dir.
(if (= active-db :csv)
(let [csv-data
{:data-dir data-dir
:db (atom {:domain {}
:session #{}
:user {}})}]; Let's keep the test database empty.
(if (not= profile :test)
(csv-db-loader/load-csv-db csv-data))
(:db csv-data))))(defmethod ig/init-key :backend/ddb [_ {:keys [active-db ss-table-prefix ss-env endpoint aws-profile]}]
(log/debug "ENTER ig/init-key :backend/ddb")
(if (= active-db :ddb)
(ddb-config/get-dynamodb-config ss-table-prefix ss-env endpoint aws-profile)))(defmethod ig/init-key :backend/postgres [_ opts]
(log/debug "ENTER ig/init-key :backend/postgres")
(if (= (:active-db opts) :postgres)
{:datasource (hikari-cp/make-datasource (dissoc opts :active-db)) :active-db (:active-db opts)}))

The “csv” database is initialized from the csv files and then we “simulate” the database using a simple Clojure atom. When we use DynamoDB as datastore we ask get-dynamodb-config function to initialize the state. When we use PostgreSQL datastore we just use hikari-pc library to initialize the database connection.

“ csv”数据库是从csv文件初始化的,然后我们使用简单的Clojure原子“模拟”数据库。 当我们使用DynamoDB作为数据存储时,我们要求get-dynamodb-config函数初始化状态。 当我们使用PostgreSQL数据存储时,我们只使用hikari-pc库来初始化数据库连接。

The test_config.clj file comprises the test state initialization. It mainly just re-uses the actual state initialization but overrides some values:

test_config.clj文件包含测试状态初始化。 它主要只是重新使用实际状态初始化,但会覆盖一些值:

(defn test-config []
(let [test-port (random-port); Overriding the port with random port, see TODO below.
_ (log/debug (str "test-config, using web-server test port: " test-port))]
(-> (core/system-config :test);; Use the same data dir also for test system. It just initializes data.
(assoc-in [:backend/csv :data-dir] "dev-resources/data")
(assoc-in [:backend/jetty :port] test-port);; In Postgres test setup use simpleserver_test database.
(assoc-in [:backend/postgres :database-name] "simpleserver_test");; No nrepl needed in tests. If used, use other port than the main system.
(assoc-in [:backend/nrepl :bind] nil)
(assoc-in [:backend/nrepl :port] nil)
(assoc-in [:backend/csv :port] nil))))

E.g. we use a different port for Jetty so that we can run Jetty for the development and tests at the same time and the ports won’t clash. For tests we also use a different PostgreSQL database.

例如,我们为Jetty使用了一个不同的端口,以便我们可以同时运行Jetty进行开发和测试,并且这些端口不会冲突。 对于测试,我们还使用其他PostgreSQL数据库。

重置状态 (Resetting the State)

Integrant provides nice auxiliary functions to reset the states: go, reset, halt etc. — check the meaning of these functions in the Integrant Repl documentation. I have created Cursive hot keys for the most used auxiliary functions, e.g. (integrant.repl/reset) is alt-J. So, when I do any changes in any namespace and hit alt-J Integrant takes care of reloading the affected namespaces and then resets the state. This happens in a blink of an eye so I’m hitting alt-J pretty often when doing Clojure.

Integrant提供了很好的辅助功能来重置状态:执行,重置,暂停等-在Integrant Repl文档中检查这些功能的含义。 我为最常用的辅助功能创建了草书热键,例如(integrant.repl/reset)alt-J 。 因此,当我在任何名称空间中进行任何更改并按alt-J Integrant都会重新加载受影响的名称空间,然后重置状态。 眨眼之间就发生了,所以我在做Clojure时经常碰到alt-J

If I have the development state running I can query its state:

如果我正在运行开发状态,则可以查询其状态:

(user/env)
=>
{:profile :dev,
:active-db :postgres,
:service {:domain #simpleserver.service.domain.domain_postgres.PostgresR{:db {:datasource #object[com.zaxxer.hikari.HikariDataSource
0x42ab0edd
"HikariDataSource (HikariPool-33)"],
...

The tests start the test state, but I can also start it manually and then query the test state:

测试开始测试状态,但是我也可以手动启动它,然后查询测试状态:

(simpleserver.test-config/go)
(simpleserver.test-config/test-env)
=>
{:profile :test,
:active-db :postgres,
:service {:domain #simpleserver.service.domain.domain_postgres.PostgresR{:db {:datasource #object[com.zaxxer.hikari.HikariDataSource
0x780ac829
"HikariDataSource (HikariPool-35)"],
...

As you can see the HikariDataSource is a different object in those states.

如您所见,在这些状态下, HikariDataSource是一个不同的对象。

This is pretty nice. I usually experiment different stuff in my scratch file hitting the development state. When I’m happy I move the code from the scratch file to the production code. If I have some issues with some test, I can just manually start the test configuration and send the forms from the test code to be run in the REPL and the forms hit the test state.

很好 我通常会在临时文件中尝试不同的内容以达到开发状态。 当我感到高兴时,我会将代码从头文件移到生产代码。 如果我对某些测试有疑问,我可以手动启动测试配置,然后从测试代码中发送表格以在REPL中运行,然后表格达到测试状态。

运行测试 (Running Tests)

I have Just recipes both for DynamoDB and Postgres databases. Let’s start both of them and then run tests…

我有DynamoDB和Postgres数据库的Just配方。 让我们同时启动它们,然后运行测试…

λ> just
Available recipes:
backend # Start backend repl.
backend-kari # Start backend repl with my toolbox.
dynamodb # Start local dynamodb emulator
lint # Lint
list
postgres # Start local postgres
test db # Test# First Postgres...
λ> just postgres
NOTE: Remember to destroy the container if running again!
Starting docker compose...
Creating ss-postgres_postgres_1 ... doneCreating Simple Server schemas...
...# ...and then DynamoDB...
λ> just dynamodb
Sending build context to Docker daemon 62.11MB
Step 1/15 : FROM python:3.8.2-slim-buster
---> e7d894e42148
Step 2/15 : RUN rm /bin/sh && ln -s /bin/bash /bin/sh
---> Using cache
...
Successfully tagged ss-uploader:0.1
Creating ss-dynamodb_local-dynamodb_1 ... doneCreating ss-dynamodb_uploader-app_1 ... doneAttaching to ss-dynamodb_local-dynamodb_1, ss-dynamodb_uploader-app_1
...

If I want to run the tests in IntelliJ IDEA / Cursive I can easily set the active data store in the config.edn:

如果我想在IntelliJ IDEA / Cursive中运行测试,则可以在config.edn中轻松设置活动数据存储区:

; csv, ddb, postgres;:backend/active-db #or [#env SS_DB :csv];:backend/active-db #or [#env SS_DB :ddb]
:backend/active-db #or [#env SS_DB :postgres]
...

… and then reset the Integrant state and run the tests in IntelliJ IDEA / Cursive:

…然后重置Integrant状态并在IntelliJ IDEA / Cursive中运行测试:

Image for post
Running tests in Clojure Integrant Exercise in IntelliJ IDEA / Cursive IDE.
在IntelliJ IDEA / Cursive IDE中的Clojure集成练习中运行测试。

I have in the configuration that if my SimpleServer DB flag (SS_DB) is present then use it. So, as the Just suggested above I can also run the test suites in command-line, providing the data store with the command, Justfile:

在配置中,如果存在我的SimpleServer DB标志( SS_DB ),请使用它。 因此,正如上面Just所建议的,我还可以在命令行中运行测试套件,为数据存储提供命令Justfile

# Test
@test db:
./run-tests.sh

… and run-tests.sh helper which populates the SS_DB flag according what was used in Just command:

…和run-tests.sh帮助程序,根据Just命令中的使用情况填充SS_DB标志:

...
MYDB=$1if [[ "$MYDB" =~ ^(csv|ddb|postgres)$ ]]; thenecho "Starting tests with $MYDB configuration..."elseecho "Unknown DB configuration: $MYDB, exiting..."
exit 2fiSS_DB=$MYDB clojure -A:dev:test:common:backend -m kaocha.runner

Let’s run the tests:

让我们运行测试:

# First using DynamoDB as data store...
λ> just test ddb
Starting tests with ddb configuration...[(........)(...........................)(...)(.......)]
14 tests, 45 assertions, 0 failures.# ...and then Postgres:
λ> just test postgres
Starting tests with postgres configuration...[(...)(...........................)(.......)(........)]
14 tests, 45 assertions, 0 failures.

… and Integrant takes care of configuring the test state so that the application uses the right data store.

…,Integrant负责配置测试状态,以便应用程序使用正确的数据存储。

结论 (Conclusions)

I’m happy I did this Integrant exercise. I now realize the benefits using a high-quality state management library versus implementing state management yourself. You can do state management quite easily yourself as well since Clojure as a Lisp is really powerful and flexible. But using a high-quality state management library gives you the basic plumming for free so that you don’t need to worry about that but just write the state as a configuration and use different states for different purposes during development.

我很高兴参加了这项综合练习。 现在,我认识到使用高质量的状态管理库相对于自己实施状态管理的好处。 您也可以轻松地自己进行状态管理,因为Clojure作为Lisp确实强大而灵活。 但是,使用高质量的状态管理库可免费为您提供基本的功能,因此您不必担心,只需将状态编写为配置并在开发期间将不同的状态用于不同的目的即可。

The writer is working at Metosin using Clojure in cloud projects. If you are interested to start a Clojure project in Finland or you are interested to get Clojure training in Finland you can contact me by sending email to my Metosin email address or contact me via LinkedIn.

作者正在Metosin的Cloud项目中使用Clojure。 如果您有兴趣在芬兰启动Clojure项目,或者有兴趣在芬兰接受Clojure培训,则可以通过发送电子邮件至Metosin电子邮件地址与我联系,或者通过LinkedIn与我联系。

Kari Marttila

卡里·玛蒂拉(Kari Marttila)

The article was first published in https://www.karimarttila.fi/

该文章最初发表在 https://www.karimarttila.fi/

翻译自: https://medium.com/@kari.marttila/clojure-integrant-exercise-78daae7a7041

clojure

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值