RailsエンジニアがDDDやクリーンアーキテクチャに触れるためにとりあえずHanamiを始めてみる

# Introduction

これまで Rails をそれなりにやってきて、ちょっとしたWebアプリならそれなりサクッと作れるようにはなりましたが、 Fat Controller だの Fat Model と言われるようにどこかで臨界点が来て、従来のMVCアーキテクチャとは違う別の設計を模索してみたい欲が出てきました :flushed: 

そのとっかりとして、巷で話題の **クリーンアーキテクチャ** に触れるために、 Rails の対抗馬として名乗りを挙げた Ruby 製フレームワーク **Hanami** を始めてみようかと思いました :fire: :fire: :fire:

# Install

まずは Hanami を立ち上げてみます。

```
$ mkdir hanami-tutorial
$ cd hanami-tutorial
$ bundle init
```

Gemfile を書き換えます。

```ruby:Gemfile
# frozen_string_literal: true
  
source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

gem "hanami"
```

ライブラリをインストールして Rails と同様にサーバーを立ち上げます。

```
$ bundle
$ bundle exec hanami new .
$ bundle exec hanami server
```

導入完了！と思いきや、2020年3月20日現在、エラーになりました :cry: 

```
Bundler could not find compatible versions for gem "dry-types":
  In snapshot (Gemfile.lock):
    dry-types (= 0.12.3)

  In Gemfile:
    hanami (~> 1.3) was resolved to 1.3.3, which depends on
      hanami-validations (>= 1.3, < 3) was resolved to 1.3.6, which depends on
        dry-validation (~> 0.11, < 0.12) was resolved to 0.11.2, which depends on
          dry-types (~> 0.12.0)

    hanami-model (~> 1.3) was resolved to 1.3.2, which depends on
      dry-types (~> 0.11.0)

Running `bundle update` will rebuild your snapshot from scratch, using only
the gems in your Gemfile, which may resolve the conflict.
```

指示に従って、もう一度サーバーを立ち上げます。

```
$ bundle update
$ bundle exec hanami server
```

http://localhost:2300/ を開くと...

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97243/7ddb95ea-66dd-62ce-5aba-ca201c705297.png)

:yum: 

# Static Page

Rails チュートリアルと同様に、まずは静的なページを作ってみます。

ルーティングは Rails 感があります。

```ruby:apps/web/config/routes.rb
root to: 'home#index'
```

アクションは Rails と違い独立したクラスを作ります。

```ruby:apps/web/controllers/home/index.rb
module Web
  module Controllers
    module Home
      class Index
        include Web::Action

        def call(params)
        end
      end
    end
  end
end
```

ビューも Rails と違います。 Rails における View は Hanami においては **View** と **Template** に分かれていて、 Rails の Helper などの UI に関わるロジックは View で、 HTML テンプレートは Template が担当します。

```ruby:apps/web/views/home/index.rb
module Web
  module Views
    module Home
      class Index
        include Web::View
      end
    end
  end
end
```

```erb:apps/web/templates/home/index.html.erb
<h1>Bookshelf</h1>
```
http://localhost:2300/ を開くとページが差し替わったはずです。
これで自由にページを作れるようになりました :smile: 

# Template Engine

Hanami のデフォルトのテンプレートエンジンは Rails と同様に Erb ですが、 Slim や Haml で書きたいですよね。
Slim をインストールして、先ほど作ったテンプレートを書き換えます。

```ruby:Gemfile
gem 'slim'
```

```
$ bundle
$ mv apps/web/templates/home/index.html.erb apps/web/templates/home/index.html.slim
```

```ruby:apps/web/templates/home/index.html.slim
h1 Bookshelf
```

これで Slim は導入できました :thumbsup: 

# Pry

Ruby 2.7 以下だとデバッグのために Pry を導入したいですね。これも簡単。

```ruby:Gemfile
gem 'pry'
```

```
$ bundle
$ bundle exec hanami console
```

```irb
[1] pry(main)> 
```

# Ridgepole

意見は分かれますが、自分はプロトタイピング重視で、 Rails のマイグレーションではなく [Ridgepole](https://github.com/winebarrel/ridgepole) を使いたいです。

```ruby:Gemfile
gem 'ridgepole'
```

```
$ bundle
```

Rails の `database.yml` に相当するものが無いので、便宜上新たに作ります。

```yaml:config/database.yml
# ridgepole を使うために用意

development:
  adapter: sqlite3
  database: db/hanami_tutorial_development.sqlite
```

Ridgepole が使う Schemafile を生成します。まだテーブルを定義していないので空ファイルが出力されます。

```
$ bundle exec ridgepole -c config/database.yml -e > Schemafile
```

テーブルを作ってみます。

```ruby:Schemafile
create_table :users, force: :cascade do |t|
  t.string :email, null: false
  t.timestamps null: false
  t.index :email, unique: true
end
```

```
$ bundle exec ridgepole -c config/database.yml -a
Apply `Schemafile`
-- create_table("users", {})
   -> 0.0052s
-- add_index("users", ["email"], {:unique=>true})
   -> 0.0035s
```

テーブルが作られたのでモデルを作ります。 Rails におけるモデルは Hanami においては **Repository** と **Entity** に分かれます。クリーンアーキテクチャが見えてきましたね :point_up:

```ruby:lib/hanami_tutorial/repositories/user_repository.rb
class UserRepository < Hanami::Repository
end
```

```ruby:lib/hanami_tutorial/entities/user.rb
class User < Hanami::Entity
end
```

コンソールで動作確認してみます。

```irb
[1] pry(main)> UserRepository.new.create(email: 'test@example.com')
[2] pry(main)> UserRepository.new.find(1)
=> #<User:0x0000000000000000 ...
[3] pry(main)> UserRepository.new.users.where(email: 'test@example.com')
=> #<ROM::Relation::Composite name=users dataset= ...
```

Hanami は ActiveRecord ではなく [Rom](https://github.com/rom-rb/rom) という ORM を使っていますが、 Rails エンジニアなら雰囲気で使えそうな気がしますよね？ :grin: 

# Job Queue

Rails の ActiveJob に相当する非同期処理モジュールが Hanami にはありませんが、自分の経験上 Job Queue ミドルウェアを移行することはそんなに無いので、ここでは Sidekiq を入れてみましょう。

```ruby:Gemfile
gem 'sidekiq'
```


```ruby:config/sidekiq.rb
Sidekiq.configure_server do |config|
  config.redis = { url: ENV['REDIS_URL'] }
end

Sidekiq.configure_client do |config|
  config.redis = { url: ENV['REDIS_URL'] }
end
```

ワーカーを作ります。引数で与えられた秒数だけ待機して **foo!** と叫ぶだけのやつです。

```ruby:lib/hanami_tutorial/workers/sleepy_echo_worker.rb
class SleepyEchoWorker
  include Sidekiq::Worker

  def perform(time)
    sleep time
    puts 'foo!'
  end
end
```
Sidekiq を立ち上げます。

```
$ REDIS_URL=redis://localhost:6379 bundle exec sidekiq -r ./config/boot.rb 
```

コンソールでワーカーを呼び出します。

```
[1] pry(main)> SleepyEchoWorker.perform_async(3)
```

Sidekiq を立ち上げたターミナルで、3秒間待機した後に **foo!** とログに出ることが確認できましたね :sunglasses: 


# Conclusion

Rails エンジニアが Hanami でWebアプリを開発するための初期導入についてまとめてみました。今後 Hanami で開発する際にハマったことがあればここに追記していこうかと思います。

ありがとうございました :bow: