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 toWeb
andAdmin
. ThenAdmin
router dispatch the request to the internal route. This two tier system is slow. - Replace
http_router
withroda
forhanami-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…