The components within that system are the objects you create to get things done within your application. For example, a HTTP action for responding to requests, a validation contract for verifying data, an operation for writing to the database, or a client that calls an external API.

Ideally, each component in your application has a single responsibility. Very often, one component will need to use other components to achieve its work. When this happens, we call the latter components dependencies.

Hanami is designed to make it easy to create applications that are systems of well-formed components with clear dependencies.

Let’s take a look at how this works in practice!

Imagine we want our Bookshelf application to send welcome emails to new users. Assuming that we’re already handling user sign ups, our task is now to create an operation for sending the welcome email. We’re going to use an external mail delivery service, while sending email in both html and plain text.

To acheive this, we first add two new components to our application: a send welcome email operation, and a welcome email renderer.

On the file system, this looks like:

Sketching out a send welcome email operation component:

  1. # app/operations/send_welcome_email.rb
  2. module Bookshelf
  3. module Operations
  4. class SendWelcomeEmail
  5. def call(name:, email_address:)
  6. # Send a welcome email to the user here...
  7. end
  8. end
  9. end
  10. end

And a welcome email renderer component:

  1. # app/renderers/welcome_email.rb
  2. module Bookshelf
  3. module Renderers
  4. class WelcomeEmail
  5. def render_html(name:)
  6. "<p>Welcome to Bookshelf #{name}!</p>"
  7. end
  8. def render_text(name:)
  9. "Welcome to Bookshelf #{name}!"
  10. end
  11. end
  12. end
  13. end

When our application boots, Hanami will automatically register these classes as components in its app container, each under a key based on their Ruby class name.

This means that an instance of the Bookshelf::Operations::SendWelcomeEmail class is available in the container under the key "operations.send_welcome_email", while an instance of Bookshelf::Renderers::WelcomeEmail is available under the key "renderers.welcome_email".

We can see this in the Hanami console if we boot our application and ask what keys are registered with the app container:

  1. bundle exec hanami console
  2. bookshelf[development]> Hanami.app.boot
  3. => Bookshelf::App
  4. bookshelf[development]> Hanami.app.keys
  5. => ["notifications",
  6. "settings",
  7. "routes",
  8. "inflector",
  9. "logger",
  10. "rack.monitor",
  11. "operations.send_welcome_email",
  12. "renderers.welcome_email"]

To fetch our welcome email send operation from the container, we can ask for it by its "operations.send_welcome_email" key:

  1. bookshelf[development]> Hanami.app["operations.send_welcome_email"]
  2. => #<Bookshelf::Operations::SendWelcomeEmail:0x00000001055dadd0>

Similarly we can fetch and call the renderer via the "renderers.welcome_email" key:

  1. bookshelf[development]> Hanami.app["renderers.welcome_email"]
  2. bookshelf[development]> Hanami.app["renderers.welcome_email"].render_html(name: "Ada")
  3. => "<p>Welcome to Bookshelf Ada!</p>"

Most of the time however, you won’t work with components directly through the container via Hanami.app. Instead, you’ll work with components through the convenient dependency injection system that having your components in a container supports. Let’s see how that works!

Dependency injection is a software pattern where, rather than a component knowing how to instantiate its dependencies, those dependencies are instead provided to it. This means the dependencies can be abstract rather than hard coded, making the component more flexible, reusable and easier to test.

This component has two dependencies, each of which is a “hard coded” reference to a concrete Ruby class:

  • AcmeEmail::Client, used to send an email via the third party Acme Email service.
  • Renderers::WelcomeEmail, used to render text and html versions of the welcome email.

To make this send welcome email operation more resuable and easier to test, we could instead inject its dependencies when we initialize it:

  1. # app/operations/send_welcome_email.rb
  2. require "acme_email/client"
  3. module Bookshelf
  4. module Operations
  5. attr_reader :email_client
  6. attr_reader :email_renderer
  7. def initialize(email_client:, email_renderer:)
  8. @email_client = email_client
  9. @email_renderer = email_renderer
  10. end
  11. def call(name:, email_address:)
  12. email_client.deliver(
  13. to: email_address,
  14. subject: "Welcome!",
  15. text_body: email_renderer.render_text(name: name),
  16. html_body: email_renderer.render_html(name: name)
  17. )
  18. end
  19. end
  20. end
  21. end

As a result of injection, this component no longer has rigid dependencies - it’s able to use any email client and email renderer it’s provided.

Hanami makes this style of dependency injection simple through its Deps mixin. Built into the component management system, and invoked through the use of include Deps["key"], the Deps mixin allows a component to use any other component in its container as a dependency, while removing the need for any attr_reader or initializer boilerplate:

  1. # app/operations/send_welcome_email.rb
  2. module Bookshelf
  3. module Operations
  4. class SendWelcomeEmail
  5. include Deps[
  6. "email_client",
  7. "renderers.welcome_email"
  8. ]
  9. def call(name:, email_address:)
  10. email_client.deliver(
  11. to: email_address,
  12. subject: "Welcome!",
  13. text_body: welcome_email.render_text(name: name),
  14. html_body: welcome_email.render_html(name: name)
  15. )
  16. end
  17. end
  18. end
  19. end

Injecting dependencies via Deps

In the above example, the Deps mixin takes each given key and makes the relevant component from the app container available within the current component via an instance method.

i.e. this code:

  1. include Deps[
  2. "email_client",
  3. "renderers.welcome_email"

makes the "email_client" component from the container available via an #email_client method, and the "renderers.welcome_email" component available via #welcome_email.

By default, dependencies are made available under a method named after the last segment of their key. So include Deps["renderers.welcome_email"] allows us to call #welcome_email anywhere in our SendWelcomeEmail class access the welcome email renderer.

We can see Deps in action in the console if we instantiate an instance of our send welcome email operation:

  1. bookshelf[development]> Bookshelf::Operations::SendWelcomeEmail.new
  2. => #<Bookshelf::Operations::SendWelcomeEmail:0x0000000112a93090
  3. @email_client=#<AcmeEmail::Client:0x0000000112aa82d8>,
  4. @welcome_email=#<Bookshelf::Renderers::WelcomeEmail:0x0000000112a931d0>>

We can choose to provide different dependencies during initialization:

  1. bookshelf[development]> Bookshelf::Operations::SendWelcomeEmail.new(email_client: "another client")
  2. => #<Bookshelf::Operations::SendWelcomeEmail:0x0000000112aba8c0
  3. @email_client="another client",
  4. @welcome_email=#<Bookshelf::Renderers::WelcomeEmail:0x0000000112aba9b0>>

This behaviour is particularly useful when testing, as you can substitute one or more components to test behaviour.

In this unit test, we substitute each of the operation’s dependencies in order to unit test its behaviour:

Exactly which dependency to stub using RSpec mocks is up to you - if a depenency is left out of the constructor within the spec, then the real dependency is resolved from the container. This means that every test can decide exactly which dependencies to replace.

Sometimes you want to use a dependency under another name, either because two dependencies end with the same suffix, or just because it makes things clearer in a different context.

This can be done by using the Deps mixin like so:

  1. module Bookshelf
  2. class SendWelcomeEmail
  3. include Deps[
  4. "email_client",
  5. email_renderer: "renderers.welcome_email"
  6. ]
  7. def call(name:, email_address:)
  8. email_client.deliver(
  9. to: email_address,
  10. subject: "Welcome!",
  11. text_body: email_renderer.render_text(name: name),
  12. html_body: email_renderer.render_html(name: name)
  13. )
  14. end
  15. end
  16. end
  17. end

Above, the welcome email renderer is now available via the #email_renderer method, rather than via #welcome_email. When testing, the renderer can now be substituted by providing email_renderer to the constructor:

  1. subject(:send_welcome_email) {
  2. described_class.new(email_client: mock_email_client, email_renderer: mock_email_renderer)
  3. }

Opting out of the container

For once-off exclusions like this Hanami supports a magic comment: # auto_register: false

  1. # auto_register: false
  2. require "hanami/action"
  3. module Bookshelf
  4. class Action < Hanami::Action
  5. end
  6. end

If you have a whole class of objects that shouldn’t be placed in your container, you can configure your Hanami application to exclude an entire directory from auto registration by adjusting its no_auto_register_paths configuration.

Here for example, the app/structs directory is excluded, meaning nothing in the app/structs directory will be registered with the container:

  1. # config/app.rb
  2. require "hanami"
  3. module Bookshelf
  4. class App < Hanami::App
  5. config.no_auto_register_paths << "structs"
  6. end
  7. end

A third alternative for classes you do not want to be registered in your container is to place them in the lib directory at the root of your project.

For example, this SlackNotifier class can be used anywhere in your application, and is not registered in the container:

  1. # lib/bookshelf/slack_notifier.rb
  2. module Bookshelf
  3. class SlackNotifier
  4. def self.notify(message)
  5. # ...
  6. end
  7. end
  8. end

Zeitwerk autoloading is in place for code you put in lib/<app_name>, meaning that you do not need to use a require statement before using it.

Code that you place in other directories under lib needs to be explicitly required before use.

Hanami applications support a prepared state and a booted state.

Whether your app is prepared or booted determines whether components in your app container are lazily loaded on demand, or eagerly loaded up front.

Hanami.prepare

When you call Hanami.prepare (or use require "hanami/prepare") Hanami will make its app available, but components within the app container will be lazily loaded.

This is useful for minimizing load time. It’s the default mode in the Hanami console and when running tests.

When you call Hanami.boot (or use require "hanami/boot") Hanami will go one step further and eagerly load all components up front.

This is useful in contexts where you want to incur initialization costs at boot time, such as when preparing your application to serve web requests. It’s the default when running via Hanami’s puma setup (see config.ru).

Standard components

Hanami provides several standard app components for you to use.

"settings"

These are your settings defined in config/settings.rb. See the settings guide for more detail.

The app’s standard logger. See the logger guide for more detail.

"inflector"

An object providing URL helpers for your named routes. See the for more detail.