Hanami 2.0 Ideas

General Principles for 2.0

Before to dive into single features, I want to discuss about general principle to follow for 2.0.

Remove Global State

Global state is cause of major pain for Hanami development and at the same time prevented to ship some features.

For global state in the context of Hanami, I mean global configurations attached to class methods or constants. For instance: Hanami::Model.configuration.

This is the thing that I regret the most in Hanami architecture.


Let me explain why this is a problem both for us and for developers.

Within a Hanami project we are able to use one or more Hanami applications. Let’s name them Web and Admin. Each application has its own set of actions, views, etc…

All the actions belonging to Admin need to follow its configuration. For instance, the session secret should be the one from Admin, while the one from Web must not interfere.

Because hanami-controller is designed with global state in mind, it holds the configuration in Hanami::Controller.configuration. Now, if both Admin and Web try to read and write settings from there, then we’ll have a problem.

That’s why I invented a technique named framework duplication. Before to read or write settings duplicate the framework under a different namespace and copy the configuration. So Hanami::Controller.configuration gets copied under Admin::Controller.configuration and only now I can read/write settings.

That’s why we include Admin::Action, instead of Hanami::Action.

This sucks.

It’s source of confusion for new developers and a PITA for us to test Hanami::Controller.configuration both in hanami and hanami-controller.

That class method is a singleton, and singletons (by definition) prevent to work with multiple instances of a same class.


Wouldn’t be better if we can get rid of this insanity?

Imagine if behind the scenes we could do:

configuration = Hanami::Controller::Configuration.new do
  # ... configure here
end

We can store this configuration object somewhere (see next point) and read/write from it. This would eliminate at all Hanami::Controller.configuration, in favor of isolated configuration instances.

Also imagine if instead of having Hanami::Model.connection that targets one database, we could have multiple connections to target multiple databases.

Components

Luckily, as v0.9 we introduced a solution for this problem, but we didn’t fully applied it yet because it required changes in hanami-* gems too.

At the boot time, we store some configurations in a registry called Hanami::Components. The goal is to have all the configurations stored here.


What are the advantages of this registry?

First of all, it ensures components are stored once (at the boot time) and never changed. It’s thread-safe, and it’s able provide fine-grained control over components.

When we start the server (via hanami server), we need to load the entire project. But there are cases where this isn’t necessary: migrating a database doesn’t require to load the actions, precompiling the assets doesn’t require to connect to the database, etc…

With this system we are already able (as of v0.9) to pick single components where needed.

If we use this system for everything, we can say goodbye to framework duplication: no more Web::Action, Admin::Action, but just include Hanami::Action and let the components to pick the right configuration for a given action.

This will eliminate all the singletons Hanami::View.configuration et all, in favor of Hanami::Components['apps.web.view'] and Hanami::Components['apps.admin.view'].

IoC/Dependency Injection

We should favor dependency injection (DI) everywhere.

There are components that don’t play well with this technique: I’m looking at you, interactor.

To ease DI, we should change the signature of some methods. For instance:

class UserSignup
  include Hanami::Interactor
  
  def initialize(data)
    @data = data
  end
  
  def call
    # ...
  end
end

What’s the problem here?

If I want to use DI in an action unit test, I have hard times because the initializer needs some arguments that I’m not always able to provide at the init time. See:

module Web::Controllers::Signup
  class Create
    include Web::Action

    # This is impossible
    def initialize(interactor: UserSignup.new(params))
      @interactor = interactor
    end
    
    def call(params)
      @interactor.call
    end
  end
end

What we need to do is to have more callable objects.

Imagine this version of the interactor:

class UserSignup
  include Hanami::Interactor
    
  def call(data)
    # ...
  end
end

At this point we can easily use DI:

module Web::Controllers::Signup
  class Create
    include Web::Action

    def initialize(interactor: UserSignup.new)
      @interactor = interactor
    end
    
    def call(params)
      @interactor.call(params)
    end
  end
end

In this way, we can easily pass a double in the unit test:

interactor = double('signup', call: ...)
action = Web::Controllers::Signup::Create.new(interactor: interactor)

With DI in place, we can provide advanced features for dependencies management.

Think if we can have a convention to auto-register dependencies and to inject them.

class BookRepository < Hanami::Repository
  def on_sale_books
    books.where(on_sale: true)
  end
end

Then we could reference a repository like this:

module Web::Controllers::Books
  class Show
    expose :on_sale_books

    def call(params)
      @on_sale_books = books.on_sale_books
    end
  end
end

This is an idea inspired by dry-auto_inject.

Immutability

To allow these features to work properly, we need to make sure repositories, actions, views, won’t change their state. They must hold only the minimum state as possible.

Because of mutability, for each HTTP request we need to instantiate at least an action, and a view. If there are other objects involved (eg a repository) it will be instantiated too.

If we combine DI/IoC with immutability we can save a lot of memory allocations per request.

Imagine if at the runtime we can have the only instance of Web::Controllers::Books::Index registered as Hanami::Components['web.controllers.books.index']. If this instance is immutable we can keep invoking #call for each request, without instantiating it anymore.


This requires design changes for these objects. Action.expose must be removed in favor of this:

module Web::Controllers::Books
  class Show
    include Web::Action
    
    def call(req, res)
      res[:books] = BookRepository.new.find(req.params[:id])
    end
  end
end

Also Action#body=, Action#headers should be replaced with:

require 'json'

module Web::Controllers::Books
  class Index
    include Web::Action
    
    def call(req, res)
      books = ...
      res.headers['Content-Type'] = "application/json"
      res.body = JSON.dump(books)
    end
  end
end

In this case, it’s res that mutates, not the action itself.

Converge Apps Settings to Project Settings

If we look at the configuration of each single Hanami application in a project, there are a lot of settings.

These are overwhelming for newcomers and it doesn’t make sense to be duplicated across the project.

Logger is a good example. Until 1.0 we had one logger per app: Web.logger, Admin.logger. This had the drawback to maintain the relative settings and to operate a project with several loggers.

With 1.0, we converged all of them in Hanami.logger.

The settings are centralized into config/environment.rb instead of apps/web/application.rb and apps/admin/application.rb.

This means less overhead for Hanami and less maintenance for developers.


For the future we can think to unify:

  • routes (with a single router, instead of two tier system)
  • exception handling
  • scheme/host/port settings
  • Body parsers
  • Common Security features
  • Assets compressors
  • Assets subresource integrity

Be More Opinionated

There are settings that can be removed from application configurations, by providing an implicit default that can be changed at a developer’s convenience.

Think of the following settings:

module Web
  class Application < Hanami::Application
    configure do
      root __dir__
      routes 'config/routes'
    end
  end
end

Who ever changed root? Who ever placed routes under another path?

We’re creating noise for the 99% of the developers who don’t need these settings.

The goal is to reduce the boilerplate of these settings, not generating them for new apps.

So a new generated app will have less settings, that can be changed in case a developer needs them. Eg. load_paths can be removed by letting the framework to pick a default for us (controllers and views dirs). In case we need to customize it, we can type:

module Web
  class Application < Hanami::Application
    configure do
      # ...
      load_paths << 'presenters'
    end
  end
end

Extendibility

We want keeping encouraging third-party developers to write integration gems for Hanami.

One goal is to allow to register custom commands or subcommands:

hanami foo

or

hanami generate authentication user

Performance

We’re faster than Rails, but slower than Sinatra. Wouldn’t be nice if we can offer the best of the two worlds? A framework that is both complete and fast.

To do so we need to:

  • Have a single tier router. Right now we have Hanami.app that is a proxy router to dispatch requests to mounted apps. Each app has it’s own router. So for /admin/books request, we get the main router to dispatch to Web and Admin. Then Admin router dispatch the request to the internal route. This two tier system is slow.
  • Replace http_router with roda for hanami-router
  • Embrace immutability and components (see above). We can save a lot of instantiated objects per request. This is a relief for Ruby garbage collector and so for performance.
  • Profile existing projects to find bottlenecks, useless allocations etc…
6 Likes