|(require component)||package: component-lib|
This library helps you manage the lifecycle of stateful objects in your application. It ensures that objects are started, linked together and stopped in the correct order.
By writing programs in this style, you trade some flexibility for clarity around how and when your objects are initialized and your code becomes easier to test since swapping out real implementations of components for stubs is trivial.
Let’s assume that you’re writing a web application that emails users when they sign up. Your components are probably going to be:
the database (no dependencies),
the mailer (no dependencies),
the user manager (depends on the database) and
the http frontend (depends on each of the above).
Assuming that each of the identified components is a struct that implements the gen:component interface, your system might look something like this:
(define-system prod [db make-database] [mailer make-mailer] [user-manager (db) (lambda (db) (make-user-manager db))] [http (db mailer user-manager) (lambda (db mailer user-manager) (make-http db mailer user-manager))]) (system-start prod-system) (system-stop prod-system)
The system specification is made up of a list of component specifications. Each component specification is made up of the unique name of a component in the system, an optional list of dependencies (other component names) and a function that can be used to construct that component from its dependencies. There are no constraints on the names of the components in the system and you can have multiple components of the same type.
The define-system form builds the system struct and its internal dependency graph but does not start the system.
The call to system-start starts the db and the mailer first (one or the other may be first since neither has any dependencies), then the user-manager and finally the http server.
Finally, the call to system-stop stops all the components in the system in the reverse order that they were started in.
Components that have no dependencies and no dependents are never started.
When a component fails during startup or shutdown (i.e. raises an exception), systems don’t attempt to perform any sort of cleanup. This isn’t really a limitation, but something to be aware of.
Components are the basic building blocks of this library. Every component needs to know how to start and stop itself.
(struct mailer (started) #:methods gen:component [(define (component-start a-mailer) (struct-copy mailer a-mailer [started #t])) (define (component-stop a-mailer) (struct-copy mailer a-mailer [started #f]))])
Systems group components together according to a declarative specification.
When a system is started, its components are each started in dependency order (if a depends on b which depends on c then c is started first, then b then a) and injected into their dependents’ factory functions (c is passed to b which is finally passed to a).
When a system is stopped, its components are stopped in the reverse order that they were started in.
(define-system name component ...+)
name = id component = [component-name factory] | [component-name (dependency-name ...) factory] dependency-name = component-name component-name = id factory = expr
(define-system prod [db make-db] [app (db) make-app])
defines a system called prod-system.
This library draws inspiration from Stuart Sierra’s "component" library for Clojure.