tolitius / mount

managing Clojure and ClojureScript app state since (reset)
Eclipse Public License 1.0
1.22k stars 88 forks source link

multi-project-mount-samples #91

Open mattiasw2 opened 6 years ago

mattiasw2 commented 6 years ago

A simple example would be a simple project where I use defstate for managing env (like in luminus). Now I want to write XXX component which uses defstate to manage a db. If I put all of these into the some clojure project, i.e. same jar file, the XXX component will be able to access env as a "global" variable.

However, if I decide to put the XXX component into a separate project, I will have to inject the env somehow, i.e. code will look different. (I would already have to inject it for the unit tests.) Can you point me to any multi-project-mount-samples? I do not want to make these kinds of decisions up-front.

tolitius commented 6 years ago

can you give an example of a "multi project" you have in mind, where you keep env and why is it not collocated with other stateful components?

usually a project has a single entry point: i.e. the main function. other components that are used in the project are either project's modules (namespaces) or libraries (jars that brought in).

when creating state, it is best to do it around the entry point.

i.e. if a project P has several namespaces Ns and several libraries Ls, it would create/keep stateful components in M or Ns. Libraries are best to not have stateful components. They could have factories or builders that return state, but the scope of this state should still belong to P and Ns.

mattiasw2 commented 6 years ago

You are right if the statement "Libraries are best to not have stateful components." is the goal. However, I do not agree. That means that you upfront needs to decide if something is a component or a library.

Erlang/OTP has shown, that it is possible to make composable systems with stateful components. Erlang does this by late-binding by name. If you follow the OTP templates, you will always end up in writing stateful components. (I am not talking about objects here, since there will typically only be one state per component, and a lot of pure functions.)

Stateful components are in many cases better than stateless libraries, since you can compose bigger parts.

So, I should rephrase my question to "How to write stateful components in Clojure?"

But that is a bigger question than mount. mount does what it is espected to do in a very good way!

tolitius commented 6 years ago

That means that you upfront needs to decide if something is a component or a library

can you give an example when this is not the case, i.e. you are building something that is either stateful component or a library or both?

How to write stateful components in Clojure?

The way I do it is by keeping all the state on the edges of an application:

in a "frameworks and drivers" layer from this image.

I like to see libraries as stateless utilities, in which case they are universally useful, where the universe is bound by problems that fit library APIs.

libraries that "have to do" with state would have builders / factories where given configuration they would "build" a stateful component and transfer its ownership / return it to the caller.

take a datasource library for example. it does not keep connections to the database, it takes configuration parameters, builds connection objects, and returns you a datasource that has pointers to these connections. So you become an owner of the state, but not the library.

If you share a concrete example of what it is you are building, it would be easier to discuss the approach, otherwise, I would recommend to keep state close to the application entry point rather than scatter it around.

mattiasw2 commented 6 years ago

One example is a database-component. It typically starts out as a rather stateless library, whose only input is environment variables and database-connection. After a while more state is added, for example caching, using read-only slaves for reading etc, and suddenly, it is a stateful component. So the library becomes a stateful component as it matures.

Your clean architecture picture is too clean. There a a number of these circles, one set of entities for handling creditcard and paypal payment, another for handling subscription and reminding users to pay again, one for sending these emails, one for the actual service they use. The interface adapters and use-cases are sometimes inside these circles, sometimes covers several of them.

I used to do a lot of C# and F# programming, and then, a big part of the design is to get the core classes right for the different entities. I hope to do it differently in Clojure.

tolitius commented 6 years ago

and suddenly, it is a stateful component. So the library becomes a stateful component as it matures

but it does not have to. you pass configuration to this library, it connects to the database, creates all the caching layers, configures read only slaves, and simply returns all these stateful components to the caller.

This caller then could have a "state", in mount speak, that would call these builders in its start function:

(defstate db :start (library/build-and-start-all-these-things config)
             :stop (library/stop-db db))

or it can be composed by library functions:

(defstate db :start (-> (library/connect config)
                        library/add-cache
                        library/wrap-it-in-something-else)
             :stop (library/stop db))

There is a clear distinction between:

I prefer the latter: i.e. stateless libraries. But I also don't believe in dogma, and if you believe your use case will benefit from using stateful libraries, you could certainly do it. Clojure is a general purpose language (so as C# and F# that you have experience with), hence it won't stop you.

leblowl commented 5 years ago

So stateless libraries are nice, but I like stateful libraries as well. Take HornetQ for example, you can just embed it in your code and call init or similar. And HornetQ has a fair amount of state and maintains it's own internal database, which I don't think it really shares except through it's APIs. I like the simplicity of embedding multiple servers and other stateful things from different libraries in the same process. My issue with mount in multiple projects, where one project depends on others, is mount.core/start's side-effect of starting state in libraries that have been required. I wouldn't want HornetQ to start up unexpectedly by calling mount.core/start. I think this side-effect is just due to the way mount resolves dependency states. If mount isn't intended to be used in libraries, then I guess I will just use another state library or simply plain functions.

See the HornetQ documentation for an example of embedding it in your project: https://docs.jboss.org/hornetq/2.2.5.Final/user-manual/en/html/embedding-hornetq.html

Isn't in-process embedding a desirable and handy way of using services like HornetQ? Or would you consider this way of designing/using components an anti-pattern? I feel like many components/services need state and a simple way to use a separate component/service is with a library in the same process...

tolitius commented 5 years ago

I will just use another state library or simply plain functions

"simply plain functions" that return state is definitely the right thing to use in libraries vs. hiding state creation within a library. This is true for libraries you'd like other people/projects/libraries to use.

hornetQ server, or an embedded version of it, does not start itself, instead it has functions that create the state:

EmbeddedHornetQ embedded = new EmbeddedHornetQ();
embedded.start();

hornetQ client is a library that also does not start itself, but has functions that enable you to create state: i.e. connectionFactory, session, etc..

mount or other state management libraries are not meant to be used inside a reusable library, they just don't belong there, since a good library will have API to create state rather than creating state magically. It would need to be called to create state: hence functions are the best in this case.

mount would come later in the assemble and run step.

leblowl commented 5 years ago

Thanks for the insight.