Biff.core:Clojure Web 应用的系统构建框架
Biff.core: system composition for Clojure web apps

原始链接: https://biffweb.com/p/core/

作者正在将 Biff 框架重构为十二个模块化、独立的库,并以 `biff.core` 的发布作为开端。该基础库负责管理系统组成,简化了应用程序模块与组件的集成方式。 为了简化项目配置,作者引入了“初始化函数”(init functions),允许开发者通过在项目中添加模块来集成功能,从而无需在主命名空间中编写冗长的样板代码。 一项关键的技术挑战是保持“后期绑定”(late binding),即在不重启 Web 服务器的情况下更新应用程序逻辑(如 Ring 处理程序)。其解决方案是向初始化函数传递模块向量的引用(var),确保系统映射能动态获取最新状态。 尽管作者考虑过进一步自动化生命周期管理(启动/停止),但出于简单性和透明度的考虑,他们最终保留了现有的“组件”序列模型。通过优先考虑显式排序而非复杂的依赖解析,系统变得更易于理解和调试。 此次更新标志着 Biff 项目向更简洁、更模块化的架构迈出了重要一步。此外,作者提到他们的团队目前正在招聘一名从事可再生能源建模的高级软件工程师。

抱歉。
相关文章

原文

As I wrote about previously, I've been working on splitting Biff up into a bunch of separate libraries and changing various things along the way. I've completed a rough draft of all twelve libraries and am now going through them one-by-one to polish and release them. The first library is now ready.

biff.core: system composition and other interfaces for Biff projects. This is the glue that holds all the other libraries together, and that's why I'm releasing it first.

For a long time Biff has had this "modules and components" structure where each application namespace in your project exposes a "module" map, then you have a bunch of boilerplate to combine stuff from those modules into a single "system" map, and then we thread the system map through your "component" functions on startup. Biff 2 retains that structure, and it has some additional stuff to deal with that boilerplate.

For an example of what I'm talking about, see this code which takes the :routes (and :api-routes) keys from your modules and turns them into a :biff/handler value for the system map. I wanted a first-class way to be able to extract that kind of logic cleanly into a library so that the library's instructions can just be "add this module to your project" without an accompanying "and then paste all this stuff into your main namespace."

So this new biff.core library includes a concept of "init functions." These are functions that take a collection of modules and return a single map that can be merged into your system map. Ta da. Here's an example. Init functions are stored in the :biff.core/init key in your module maps, so we get that nice "all you need are modules (well, and components)" effect.

The main complication here is that the boilerplate of defining a (def handler ...) var in your application code actually has a nice side benefit: late binding. If you change any of your modules, the handler var will get updated, and if you set :biff/handler in your system map to the var instead of the value (#’handler), incoming Ring requests get the latest handler without you having to restart the web server. If we extract that boilerplate into library code, we don't get the var.

I ended up on this solution:

  • Init functions take a var of your modules vector, not the vector value itself.
  • Anything in the system map that you want to get updated without a restart needs to be a function. In some cases this means instead of setting a :com.example/my-thing key on the system map, you need to set a :com.example/get-my-thing function which returns my-thing.
  • That function on the system map should dereference the modules var and pass it to a memoized function that builds whatever thing it is you need (like the Ring handler).

Again, see this example. The result is kind of aesthetically pleasing: you get a nice clean main namespace that shouldn't need to change much, and all you do is add modules and components.

There's always the temptation to consolidate things further. Why even have a separate components vector? Why not have modules support :biff.core/on-start and :biff.core/on-stop keys and then have some way to express dependencies between these lifecycle functions so we can call them in the right order?

And the answer is so that we don't have to have some way to express dependencies between these lifecycle functions so we can call them in the right order. It's not that hard to put the components in the right order yourself (especially since the Biff starter project does that for you), and then it's easier to understand how components work. It's just a sequence of functions that you pass a map through. If you work on a project with so many stateful resources that it's hard to keep track of them all, you can always layer something on top that figures out what your components vector should be before you pass it to biff.core.


Plug: my team is hiring for a senior software engineer, writing ClojureScript and Python mostly. We make modeling software for renewable energy projects.

联系我们 contact @ memedata.com