aroemers / mount-lite

mount, but different and light
https://cljdoc.org/d/functionalbytes/mount-lite/
Eclipse Public License 1.0
102 stars 8 forks source link

Bindings (dependency injection), good idea? #5

Closed aroemers closed 8 years ago

aroemers commented 8 years ago

From the README:

Bindings

It is generally best to define the defstates in application namespaces, not in the more general (library) namespaces. This is because the :start and :stop expressions are tightly coupled to their environment, including references to other states. This is fine though, as you as the application writer have full control over your states, and resources should be at the periphery of the application anyway.

Yet, in the rare situations where you need a looser coupling between the :start/:stop expressions and their environment, mount-lite has a unique feature called bindings. When defining a defstate, one can optionally supply a :bindings vector, next to the :start and :stop expressions. This vector declares the bindings that can be used by the :start/:stop expressions, and their defaults. For example:

(defstate incrementer
  :start    (fn [n] (+ n i))
  :stop     (println "stopping incrementer of" i)
  :bindings [i 10])

When the incrementer state is started normally, it will become a function that increments the argument by 10. However, one can start the incrementer with different bindings, like so:

(mount/start (bindings #'incrementer {'i 20})
;=> (#'incrementer)

(incrementer 5)
;=> 25

(stop)
;>> stopping 20 incrementer
;=> (#'incrementer)

As can be seen, the bindings that were used when starting the state are also used when stopping the state.

This bindings feature can be used as a kind of dependency injection or for passing configuration parameters. Yet, at the current time of writing, my opinion is to use this feature sparingly. Substitutions are normally sufficient and using bindings a lot might hint towards a design flaw.

My opinion as of the time of this writing:

For passing in some configuration values (e.g. command line arguments), I think this is a very nice solution: no need for thread-local dynamic vars (which will break in parallel mode) or other fragile and rigid solutions. Bindings in that sense offer an easy, cleanly scoped and semantically clear way of passing values to states, ensuring the same values on stop as when a state was started.

Yet, the number of use cases currently seem very slim. In my opinion it should not be used to pass one state/resource to another state as some form of dependency injection. States can simply refer to those states/resources directly; which is the ease that mount offers. And those referred states can be substituted. Most configuration of states would be by referring to some config state. So, only this initial config state might benefit from the bindings feature.

So, powerful in theory (as it removes the last "limitation" of mount - tight coupling), but not many use cases in practice? and maybe too powerful leading to "wrong" usage of mount? The tight coupling limitation is a good thing, because states should be in fully controlled application namespaces anyway?

Time will tell. But anyone reading this, know that this feature exists (which was only 12 lines of real code anyway), and please share your use case or opinion on the matter.

vizanto commented 8 years ago
  :start    (fn [n] (+ n i))

looks odd to me, as you're defining an anonymous function, yet the i is not declared in the lines above, but in bindings.

What happens if you do not provide i in bindings? Do bindings behave like with-redefs?

aroemers commented 8 years ago

@vizanto If i is not in the bindings (and also not in the environment of the defstate), the standard "symbol not found" exception will occur when loading the source code.

What behaviour of with-redefs are you referring to? They are not thread-local, if that's what you mean (it actually avoids it), and they are also not visible outside of the defstate's scope.

About the order, you are right. Of course, you could put the :bindings above the :start expression. I also thought if this syntax:

(defstate incrementer [i 10]
  :start #(+ % i))

Would that improve things?

aroemers commented 8 years ago

Above syntax idea is now supported.

(defstate incrementer [i 10]
  :start #(+ % i))

When started, a :mount.lite/current-bindings var meta key is set to the actual binding values, for debugging purposes.

(mount/start)
(-> #'incrementer meta :mount.lite/current-bindings)
;=> [i 10]

(stop)

(mount/start (bindings #'incrementer '[i 20])
(-> #'incrementer meta :mount.lite/current-bindings)
;=> [i 20]

Note that the bindings option now takes a vector, instead of a map.

vizanto commented 8 years ago

That is more explicit, yes. The more explicit syntax in the lasts commits seems cleaner to me. :+1:

I meant (with-redefs [#'i 10] ...) redefines a var named i, which was not declared in the original example. So standard symbol not found error kinda makes sense.