Liking cljdoc? Tell your friends :D

Components

A Component is a data structure to managing the lifecycle, dependencies and data flow of a program. A Component is the smallest unit of execution which is reusable and easy to reason about.

A component is similar in spirit to the definition of an object in Object-Oriented Programming. This does not alter the primacy of pure functions and immutable data structures in Clojure as a language. Most functions are just functions, and most data are just data. Components are intended to help manage stateful resources within a functional paradigm.

Advantages of the Component Model

Large applications often consist of many stateful processes which must be started and stopped in a particular order. The component model makes those relationships explicit and declarative, instead of implicit in imperative code.

Components provide some basic guidance for structuring a HellHound application, with boundaries between different parts of a system. Components offer some encapsulation, in the sense of grouping together related entities. Each component receives references only to the things it needs, avoiding the unnecessary shared state. Instead of reaching through multiple levels of nested maps, a component can have everything it needs at most one map lookup away.

Instead of having mutable state (atoms, refs, etc.) scattered throughout different namespaces, all the stateful parts of an application can be gathered together. In some cases, using components may eliminate the need for mutable references altogether, for example, to store the "current" connection to a resource such as a database. At the same time, having all state reachable via a single [system](./README.md#overview) map makes it easy to reach in and inspect any part of the application from the REPL.

The component dependency model makes it easy to swap in stub or mock implementations of a component for testing purposes, without relying on time-dependent constructs, such as with-redefs or binding, which are often subject to race conditions in multi-threaded code.

Having a coherent way to set up and tear down all the state associated with an application enables rapid development cycles without restarting the JVM. It can also make unit tests faster and more independent, since the cost of creating and starting a system is low enough that every test can create a new instance of the system.

Disadvantages of the Component Model

For small applications, declaring the dependency relationships among components may actually be more work than manually starting all the components in the correct order or even not using component model at all. Everything comes at a price.

The [system](./README.md#overview) map produced by HellHound is a complex map and it is typically too large to inspect visually. But there are enough helper functions in hellhound.system` namespace to help you with it.

You must explicitly specify all the dependency relationships among components: the code cannot discover these relationships automatically.

Finally, HellHound system forbids cyclic dependencies among components. I believe that cyclic dependencies usually indicate architectural flaws and can be eliminated by restructuring the application. In the rare case where a cyclic dependency cannot be avoided, you can use mutable references to manage it, but this is outside the scope of components.

Usage

Components are the main parts of HellHound systems. Basically, each component is an implementation of IComponent protocol. The protocol which defines a component functionality. By default HellHound implements IComponent protocols for hashmaps only. So we can define components in form of maps.

In order to define a component, a map should contain the following keys (All the keys should be namespaced keyword under hellhound.component):

  • name: The name of the component. It should be a namespaced keyword. This key is mandatory.

  • depends-on: This key specifies all the components which are the dependencies of the current component. A collection of components name. This key is optional.

  • start-fn: A function which takes which takes two arguments. The component map and a context map. Returns the component with the necessary keys attached to it. This function is responsible for starting the component. Practically your component code goes into this function. This key is mandatory.

  • stop-fn: A function which takes the component map as the only argument and return the component with the necessary keys attached to it. This function is responsible for stoping the component. This key is mandatory.

  • input-stream-fn: A function which returns a manifold as the input of the component. You rarely need to use this key for a component. This key optional.

  • output-stream-fn: A function which returns a manifold as the output of the component. You rarely need to use this key for a component. This key optional.

So as an example:

A Stupid component which does nothing
(def sample-component
  {:hellhound.component/name :sample.core/component-A (1)
   :hellhound.component/start-fn (fn [component] component) (2)
   :hellhound.component/stop-fn (fn [component] component)  (3)
   :hellhound.component/depends-on [:sample.core/component-B]}) (4)
1Description of the component name which is :sample.code/component-A
2The function which is responsible for starting the component. Which does nothing in this case.
3The function which is responsible for stapping the component. Which does nothing in this case.
4Description of the dependencies of :sample.code/component-A. In this case this component depends on another component called :sample.core/component-B.

As you can see creating a component is fairly simple and nothing special.

Context Map

As you already know, start-fn of a component takes two arguments. The first one it the component map itself and second one is a hashmap called context map.

The purpose of a context map is to provides co-effects for the component, Such as dependency components. For example HellHound inject a key called :dependencies to the context map which is a vector containing those components that this component is depends on. The order of this vector would be exactly the same as :depends-on value in component description map. So you can get all you need from the context map. You can think of it as some sort of dependency injection for you component.

But context map contains more data that you might need them in action. Here is the list of keys of a context map:

  • :dependencies: A vector of running components which the current component is depends on. All the components in this vector already started and the order is the same as the vector provided to :depends-on in component map.

  • :dependencies-map: A hashmap containing all the components of the current component dependencies. keys of this map are the components names and the values are the running components. The purpose of this map is to access dependency components by name.

Can you improve this documentation? These fine people already did:
Sameer Rahmani & yottanami
Edit on GitHub

cljdoc is a website building & hosting documentation for Clojure/Script libraries

× close