Permalink
212 lines (142 sloc) 11.3 KB
Differences from "Component"

Perception

Solving the "application state" in Clojure, where an application is not a tool or a library, but a product that has lots of state to deal with, is not a trivial task. The Component framework is a solution that has been gaining popularity:

source:

I think all agreed that Component is the industry standard for managing lifecycle of Clojure applications. If you are a Java developer you may think of it as a Spring (DI) replacement – you declare dependencies between “components” which are resolved on “system” startup. So you just say “my component needs a repository/database pool” and component library “injects” it for you.

While this is a common understanding, the Component is far from being Spring, in a good sense:

  • its codebase is fairly small
  • it aims to solve one thing and one thing only: manage application state via inversion of control

The not so hidden benefit is REPL time reloadability that it brings to the table with component/start and component/stop

Table of Contents generated with DocToc

Then why "mount"!?

mount was created after using Component for several projects.

While Component is an interesting way to manage state, it has its limitations that prevented us from having the ultimate super power of Clojure: fun working with it. Plus several other disadvantages that we wanted to "fix".

Before moving on to differences, here is a piece by Rich Hickey. While he is not talking about application state, it is an interesting insight into LISP design principles:

Lisps were designed to receive a set of interactions/forms via a REPL, not to compile files/modules/programs etc. This means you can build up a Lisp program interactively in very small pieces, switching between namespaces as you go, etc. It is a very valuable part of the Lisp programming experience. It implies that you can stream fragments of Lisp programs as small as a single form over sockets, and have them be compiled and evaluated as they arrive. It implies that you can define a macro and immediately have the compiler incorporate it in the compilation of the next form, or evaluate some small section of an otherwise broken file.

So what are the differences?

Component requires whole app buy in

Component really only works if you build your entire app around its model: application is fully based on Components where every Component is an Object.

Mount does not require you to "buy anything at all", it is free :) Just create a defstate whenever/wherever you need it and use it.

This one was a big deal for all the projects we used Component with, "the whole app buy in" converts an "open" application of Namespaces and Functions to a "closed" application of Objects and Methods. "open" and "close" here are rather feelings, but it is way easier and more natural to

  • go to a namespace to see this function than to
  • go to a namespace, go to a component, go to another component that this function maybe using/referenced at via a component key, to get the full view of the function.

Again this is mostly a personal preference: the code works in both cases.

Start and Stop Order

Component relies on a cool dependency library to build a graph of dependencies, and start/stop them via topological sort based on the dependencies in this graph.

Since Mount relies on Clojure namespaces and :require/:use, the order of states and their dependencies are revealed by the Clojure Compiler itself. Mount just records that order and replays it back and forth on stop and start.

Refactoring an existing application

Since to get the most benefits of Component the approach is "all or nothing", to rewrite an existing application in Component, depending on the application size, is daunting at best.

Mount allows adding defstates incrementally, the same way you would add functions to an application.

Code navigation

Component changes the way the code is structured. Depending on the size of the code base, and how rich the dependency graph is, Component might add a good amount of cognitive load. To a simple navigation from namespace to namespace, from function to function, Components add, well.. "Components" that can't be ignored when loading the codebase in one's head

Since Mount relies on Clojure namespaces (:require/:use), navigation across functions / states is exactly the same with or without Mount: there are no extra mental steps.

Objects vs. Namespaces

One thing that feels a bit "unClojure" about Component is "Objects". Objects everywhere, and Objects for everything. This is how Component "separates explicit dependencies" and "clears the bounaries".

This is also how an Object Oriented language does it, which does not leave a lot of room for functions: with Component most of the functions are methods which is an important distinction.

Mount relies on Clojure namespaces to clear the boundaries. No change from Clojure here: defstate in one namespace can be easily :required in another.

Starting and stopping parts of an application

Component can't really start and stop parts of an application within the same "system". Other sub systems can be created from scratch or by dissoc'ing / merging with existing systems, but it is usually not all that flexible in terms of REPL sessions where lots of time is spent.

Mount can start and stop parts of an application via given states with their namespaces:

dev=> (mount/start #'app.config/app-config #'app.nyse/conn)

11:35:06.753 [nREPL-worker-1] INFO  mount - >> starting..  app-config
11:35:06.756 [nREPL-worker-1] INFO  mount - >> starting..  conn
:started
dev=>

Here is more documentation on how to start/stop parts of an app.

Boilerplate code

Component does not require a whole lot of "extra" code but:

  • a system with dependencies
  • components as records
  • with optional constructors
  • and a Lifecycle/start Lifecycle/stop implementations
  • destructuring component maps

Depending on the number of application components the "extra" size may vary.

Mount is pretty much:

(defstate name :start fn 
               :stop fn)

no "ceremony".

Library vs. Framework

Mount uses namespaces and vars where Component uses records and protocols.

Component manages protocols and records, and in order to do that it requires a whole app buyin, which makes it a framework.

Mount does not need to manage namespaces and vars, since it is very well managed by the Clojure Compiler, which makes it a library.

What Component does better

Multiple separate systems within the same JVM

With Component multiple separate systems can be started in the same Clojure runtime with different settings. Which might be useful for testing, i.e. if you need to have dev db and test db started in the same REPL, to run tests within the same REPL you develop in.

Development workflows vary and tend to be a subjective / preference based more than a true recipe, but I believe it is much cleaner to run tests in the separate REPL / process. Moreover run them continuously: i.e. boot watch speak test: this way you don't event need to look at that other REPL / terminal, Boot will tell you whether the tests pass or fail after any file is changed.

Mount keeps states in namespaces, hence the app becomes "The One", and there can't be "multiples The Ones". In practice, if we are talking about stateful external resources, there is trully only one of them with a given configuration. Different configuration => different state. It's that simple.

Testing is not alien to Mount and it knows how to do a thing or two:

After booting mount I was secretly thinking of achieving multiple separate systems by running them in different Boot Pods.

But the more I think about it, the less it feels like a mount's core functionality. So I created Yurt that can easily create and run multiple separate mount systems simultaniously.

conclusion: can be done with mount as well, but via a different dependency.

Visualizing dependency graph

Component keeps an actual graph which can be visualized with great libraries like loom. Having this visualization is really helpful, especially during code discusions between multiple developers.

Mount does not have this at the moment. It does have all the data to create such a visualization, perhaps even by building a graph out of the data it has just for this purpose.

There is a (states-with-deps) function that can help out:

dev=> (require '[mount.tools.graph :as graph])

dev=> (graph/states-with-deps)
({:name "#'app.conf/config", 
  :order 1, 
  :status #{:started}, 
  :deps #{}}
 {:name "#'app.db/conn",
  :order 2,
  :status #{:started},
  :deps #{"#'app.conf/config"}}
 {:name "#'app.www/nyse-app",
  :order 3,
  :status #{:started},
  :deps #{"#'app.conf/config"}}
 {:name "#'app.example/nrepl",
  :order 4,
  :status #{:started},
  :deps #{"#'app.www/nyse-app" "#'app.conf/config"}})

But it does not draw :), and it currently only supports Clojure, not ClojureScript.

conclusion: needs more thinking.