Using dry-auto_inject with rails
Around of 2 or 3 months ago, I saw dry-rb at the first time, I thought: “Oh, that’s awesome, I have to experiment”. Today is the day! If you are thinking: “Oh, sorry what is Dry-rb?”. I will explain… Well, Dry-rb is a bunch of tools to simplify and help us to make some code improvements.
Today in this post, I’m going to show how we can use dry-auto_inject with rails. First, There is a simple question, what is dry-auto_inject? According with dry-rb.org, it’s a “Container-agnostic constructor injection mixin”, if you already programmed in languages like Java (Spring @Autowired, says: ‘Hello’), C#, or some language or framework with dependency-injection support you saw the amazing ‘magic’ of Dependency Injection(DI).
If you are thinking: “Oh, my Gosh but I never saw nothing about dependency injection in my life”. Relax, I didn’t forget you. The Internet has a lot of materials about dependency injection. If you read something about SOLID, you already saw this concept. The D in SOLID, represents “Dependency inversion principle”, this principle can be made with an creational pattern, a factory method or a DEPENDENCY INJECTION framework, take a look below.
- Extracted from: https://dzone.com/articles/dependency-injection-0
I won’t explain all details behind this concept, because many people made it as you see below:
If you saw something like the code below, you already had a possibility to use Dry-Auto_Inject:
class CreateArticle
def initialize(repository = ArticleRepository.new)
self.repository = repository
end
def call(article)
repository.call(article)
end
end
In this case the class CreateArticle receives a external dependency and calls the repository method(#call). The class CreateArticle believes that ArticleRepository implements a method #call, it doesn’t have any details about repository. If you have some tool to inject the dependency automatically, this code can be more uncoupled and the responsability to know where was implemented, can be delegated to another part of code.
Starting
We have to install the development environment. We will use current stable rails version 5.0.0.1 and ruby 2.3.0. If you don’t have Ruby and Rails installed, check how install in RVM or Rbenv sites, it’s very simple ;).
~/projects/ruby ruby -v
ruby 2.3.1p112 (2016-04-26 revision 54768) [x86_64-linux]
~/projects/ruby rails -v
Rails 5.0.0.1
After that, we can start! To put it into action, we need a Rails application. To create a new App, type the command below. If you already has a Rails app, you can ignore this step.
~/projects/ruby rails new blog
create
create README.md
create Rakefile
create config.ru
create .gitignore
create Gemfile
create app
...
After create app, we need to install dry-auto_inject. To install, add this line in your Gemfile.
gem 'dry-auto_inject'
And run
bundle install
/projects/ruby/blog bundle list | grep dry
* dry-auto_inject (0.4.1)
* dry-configurable (0.1.7)
* dry-container (0.5.0)
Dry-auto_inject depends of dry-configurable and dry-container. The dry-configurable is “a simple mixin to add thread-safe configuration behaviour to your classes”, you can check here. The dry-container is the core of auto-injection mechanism.
Let’s see how it works.
~/projects/ruby/blog bundle exec rails c
Running via Spring preloader in process 5167
Loading development environment (Rails 5.0.0.1)
2.3.1 :001 >
2.3.1 :002 > container = Dry::Container.new
=> #<Dry::Container:0x000000038bb200 @_container={}>
2.3.1 :003 > container.register(:article_repository) { Class.new }
=> #<Dry::Container:0x000000038bb200 @_container={"article_repository"=>#<Dry::Container::Item:0x000000034d0640 @item=#<Proc:0x000000034d06e0@(irb):43>, @options={:call=>true}>}>
When someone call Dry::Container#register with the identifier(:article_repository), the dependency is stored inside a proc. After that, when someone call Dry::Container#resolve with identifier(:article_repository) the proc stored previosly is executed or returned. The code inside proc can be executed immediately when resolve the reference or no, let’s see how it works with the code below.
2.3.1 :001 > container = Dry::Container.new
=> #<Dry::Container:0x000000042db460 @_container={}>
2.3.1 :002 > container.register(:lazy_hello, -> { puts "Hello Lazy" }, call: false)
=> #<Dry::Container:0x000000042db460 @_container={"lazy_hello"=>#<Dry::Container::Item:0x00000004342a98 @item=#<Proc:0x000000043432e0@(irb):2 (lambda)>, @options={:call=>false}>}>
2.3.1 :003 > container.resolve(:lazy_hello)
=> #<Proc:0x000000043432e0@(irb):2 (lambda)>
2.3.1 :004 > container.resolve(:lazy_hello).call
Hello Lazy
=> nil
2.3.1 :005 > container.register(:hello, -> { puts "Hello" }, call: true)
=> #<Dry::Container:0x000000042db460 @_container={"lazy_hello"=>#<Dry::Container::Item:0x00000004342a98 @item=#<Proc:0x000000043432e0@(irb):2 (lambda)>, @options={:call=>false}>, "hello"=>#<Dry::Container::Item:0x000000043814f0 @item=#<Proc:0x000000043815b8@(irb):5 (lambda)>, @options={:call=>true}>}>
2.3.1 :006 > container.resolve(:hello)
Hello
=> nil
And we can store namespaced identifiers.
2.3.1 :060 > container = Dry::Container.new
=> #<Dry::Container:0x00000003cebc08 @_container={}>
2.3.1 :061 > container.namespace(:services) do
2.3.1 :062 > namespace(:article) do
2.3.1 :063 > register(:repository) { Class.new }
2.3.1 :064?> end
2.3.1 :065?> end
=> #<Dry::Container:0x00000003cebc08 @_container={"services.article.repository"=>#<Dry::Container::Item:0x00000003c898a0 @item=#<Proc:0x00000003c89ad0@(irb):63>, @options={:call=>true}>}>
2.3.1 :066 > container.resolve('services.article.repository')
=> #<Class:0x00000003c66490>
For more details, you could check this documentation.
Now, we need to create something to use dry-auto_inject. We will use scaffold to generate the Article model.
~/projects/ruby/blog rails g scaffold Article name:string description:string
Running via Spring preloader in process 6093
invoke active_record
create db/migrate/20161115123949_create_articles.rb
create app/models/article.rb
invoke test_unit
create test/models/article_test.rb
...
Now, we can see the created routes typing ‘rake routes’.
~/projects/ruby/blog rake routes
Prefix Verb URI Pattern Controller#Action
articles GET /articles(.:format) articles#index
POST /articles(.:format) articles#create
new_article GET /articles/new(.:format) articles#new
edit_article GET /articles/:id/edit(.:format) articles#edit
article GET /articles/:id(.:format) articles#show
PATCH /articles/:id(.:format) articles#update
PUT /articles/:id(.:format) articles#update
DELETE /articles/:id(.:format) articles#destroy
Before run application, we need to create the table to store article entries.
~/projects/ruby/blog rake routes
~/projects/ruby/blog rake db:migrate
== 20161115123949 CreateArticles: migrating ===================================
-- create_table(:articles)
-> 0.0015s
== 20161115123949 CreateArticles: migrated (0.0016s) ==========================
We will change only the article#create route, but be free to modify everything you want. You can create an article to test if everything work as expected, So, open your browser and access http://localhost:3000/articles.
After tests, we can change the code. Take a look at articles_controller.rb.
#app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
...
def create
@article = Article.new(article_params) # we will act only here
...
end
end
I will create a command class only for illustration proposes. The command concept here, is very similar of Trailblazer::Operation. If you don’t like it, you can use Article#create to create directly, don’t worry :).
Now, create the folder /commands/article under lib directory, after that, create file create.rb with lines below.
#lib/commands/article/create.rb
module Commands
module Article
class Create
def call(params)
::Article.create(params)
end
end
end
end
If you run rails console and call Commands::Article::Create#call you will get an error, it’s because we need to add the line responsible to load the created structure at Blog::Application class.
#config/application
module Blog
class Application < Rails::Application
config.eager_load_paths += Dir["#{Rails.root}/lib"] # add this line
Now you can run rails console and call Commands::Article::Create#call with params. An article is created.
~/projects/ruby/blog rails console
Running via Spring preloader in process 10039
Loading development environment (Rails 5.0.0.1)
2.3.1 :001 > Commands::Article::Create.new.call(name: "AutoInject", description: "How can i use DryAutoInject")
(0.1ms) begin transaction
SQL (0.5ms) INSERT INTO "articles" ("name", "description", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["name", "AutoInject"], ["description", "How can i use DryAutoInject"], ["created_at", 2016-11-15 14:03:42 UTC], ["updated_at", 2016-11-15 14:03:42 UTC]]
(7.9ms) commit transaction
=> true
We have to define the container to register all dependencies needed. To reach it, we have to create a file under config/initializers to register dependencies.
#config/initializers/auto_inject.rb
class Blog::Container
extend Dry::Container::Mixin
register('commands.article.create') do
Commands::Article::Create.new
end
end
AutoInject = Dry::AutoInject(Blog::Container)
Inside this file the constant AutoInject was defined, it will be useful inside ArticlesController to inject dependencies.
To use the registered command, we have to include a reference to the registered dependency. The code will look as below:
#app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
# The alias create_article it's a reference
# to 'commands.article.create'
include AutoInject[create_article: 'commands.article.create']
...
def create
@article = create_article.call(article_params)
respond_to do |format|
if @article.persisted?
Finally, you can test again to see DI working. To see how add tests and many cool things please check the site.
All code is available on my github.
Thanks!
References
- http://dry-rb.org/;
- http://trailblazer.to/gems/operation/1.1/;
- http://www.martinfowler.com/articles/injection.html;
- http://solnic.eu/2013/12/17/the-world-needs-another-post-about-dependency-injection-in-ruby.html;
- https://en.wikipedia.org/wiki/Dependency_inversion_principle.