Help us understand the problem. What is going on with this article?

Railsエンジニアの視点でHanamiを解説してみた

はじめに

新しい職場に来てもう少しで2ヶ月が経過します。現在の業務でHanamiを書き始め、何となく慣れてきたので、アドベンドカレンダーの機会にHanamiについて書いてみたいと思います。しかし、Hanamiはまだまだマイナーなフレームワークであるため、Railsエンジニアの視点で分かりやすいように解説していきます。

Hanamiとは

2017年4月にバージョン 1.0.0 がリリースされたばかりの比較的新しいRubyのフレームワークです。Railsとの違いで代表的なものはこんなところです。

長期的なメンテナンスに向いたフレームワーク

Rails はMVCやActiveRecordに仕様の大部分が依存したフレームワークになっています。 一方でHanami は DDD (ドメイン駆動設計) をベースにしつつ、ある程度柔軟性を残した状態で開発出来るフレームワークとなっています。 もう少し具体的に言うと、Rails はサービスを素早くローンチすることに向いている一方で Hanami は長期的な保守を念頭においた開発に向いているといった感じです。

ただし、Railsと同様にMVC全体がサポートされたフルスタックなフレームワークなので、必ずしも初期ローンチを犠牲にしている訳ではありません。最近では、マイクロサービスを念頭に置いた開発を行うようになってきたこともあり、初期ローンチとサービス成長の両方を見据えた選択肢としては良さそうなフレームワークです(ここはあくまでも2ヶ月間書いてきた人間の初感ですが。。)。

マジックが少ない

マジックと言うと少し分かりにくいですが、Rubyはメタプログラミングを上手く活用することで、プログラムを書くプログラムを書くことが可能です(初学者を混乱させてしまう説明かも。。)。もう少し具体的に言うと、実行時に与えられた引数に合わせて処理を柔軟に変化させるクラスやメソッドが書けるといった感じです(さらに分らなかったらごめんなさい。。)。メタプログラミングで書かれたクラスやメソッドは内部処理を知らなくても柔軟に使用出来てしまうことから、このような言われ方がされます。

特に、Railsは、ActiveRecordやActiveSupport系などの強力なGemによりマジックが生み出され、開発者が詳細を意識しなくても書けてしまうフレームワークになっています。このあたりがRailsは書けるけど、Rubyは書けないみたいなエンジニアが生まれてしまう原因だったり。。しかし、ビジネス的な視点で見ると、早くローンチ出来た方がビジネスでのトライ&エラーが出来るため、Railsが広まった大きな理由でもあり、問題というよりは利点だと思われます。

一方、Hanamiの場合は、一部で特定のGemに依存している部分は見られるものの、マジックが少なく、 ほぼPure Ruby に近いフレームワークとなっています。ちなみに、ActiveSupportが使えないため、一番最初に困るのが、present?blank?メソッドが使えなくなる点です。オブジェクトの種類を意識しないと無意識にNoMethodErrorを引き起こすコードを書きます。

あとは、長くなりそうなので、技術的な特徴などは公式ホームページをご覧下さい。

公式ホームページはこちら

hanamiコマンド

Hanamiでは、Railsと同様にhanamiコマンドが用意されています。細かいところで違いはあるものの、ほとんどrailshanamiになっただけです。

プロジェクトの作成
bundle exec hanami new . --database=mysql
Hanami console
bundle exec hanami console

ディレクトリ構造

上記のコマンドを実行した後のディレクトリ構造はこんな感じです。

今回、環境にdocker-composeを利用しています。dbディレクトリとDocker関係のファイルは気にしないで下さい。

apps配下にRailsでも馴染みがあるcontrollerstemplatesviewsが存在します。

そして、Railsと大きく違う点は、lib配下にDDDらしさが伺えるentitiesrepositoriesが存在する点です。簡単に説明すると、この2つがModelの役割を果たします。

.
|-- Dockerfile
|-- Gemfile
|-- Gemfile.lock
|-- README.md
|-- Rakefile
|-- apps
|   `-- web
|       |-- application.rb
|       |-- assets
|       |   |-- favicon.ico
|       |   |-- images
|       |   |-- javascripts
|       |   `-- stylesheets
|       |-- config
|       |   `-- routes.rb
|       |-- controllers
|       |-- templates
|       |   `-- application.html.erb
|       `-- views
|           `-- application_layout.rb
|-- config
|   |-- boot.rb
|   |-- environment.rb
|   `-- initializers
|-- config.ru
|-- db
|   |-- migrations
|   |-- mysql
|   |   `-- volumes
|   `-- schema.sql
|-- docker-compose.yml
|-- lib
|   |-- app
|   |   |-- entities
|   |   |-- mailers
|   |   |   `-- templates
|   |   `-- repositories
|   `-- app.rb
|-- public
`-- spec
    |-- app
    |   |-- entities
    |   |-- mailers
    |   `-- repositories
    |-- features_helper.rb
    |-- spec_helper.rb
    |-- support
    |   `-- capybara.rb
    `-- web
        |-- controllers
        |-- features
        `-- views
            `-- application_layout_spec.rb

Generater

これは、users#showというアクションを作成するコマンドです。もう、なんとなく分かると思いますが、RailsでいうところのControllerのActionです。

Hanamiの大きな特徴として、ControllerはAction毎にファイルを作ります。詳しくは、Controllerの章で解説します。

# bundle exec hanami generate action web users#show
      create  apps/web/controllers/users/show.rb
      create  apps/web/views/users/show.rb
      create  apps/web/templates/users/show.html.erb
      create  spec/web/controllers/users/show_spec.rb
      create  spec/web/views/users/show_spec.rb
      insert  apps/web/config/routes.rb

Routing

Railsと全く同じに書けます。詳細に調べて行くと、セッションの有無に応じてルーティングを行うような高度なルーティングも出来ますが、Railsと同じ感覚でこのあたりさえ覚えておけば、ほとんど使えてしまいそうです。

apps/web/config/route.rb
get '/',  to: "users#show"
post '/new', to: "users"

または

apps/web/config/route.rb
resources :users, only: [:show, :create]

Model

まずは、hanamiコマンドでModelを作成します。以下のコマンドを実行すると、entitiesrepositoriesが作成されます。

bundle exec hanami generate model user
      create  lib/src/entities/user.rb
      create  lib/src/repositories/user_repository.rb
      create  db/migrations/20191208133857_create_users.rb
      create  spec/src/entities/user_spec.rb
      create  spec/src/repositories/user_repository_spec.rb

Migration

そして、マイグレーションファイルもRailsとそれほど変わりません。

db/migrations/20191207110038_create_users.rb
Hanami::Model.migration do
  change do
    create_table :users do
      primary_key :id

      column :name,  String, null: false
      column :email, String, null: false

      column :created_at, DateTime, null: false
      column :updated_at, DateTime, null: false
    end
  end
end

マイグレーションを実行するコマンドはこんな感じです。

bundle exec hanami db prepare

Entity

特殊な要件がなければ、これだけで使用出来ます。RailsのModelと同じように、Hanami::Entityを継承しているだけですが、面白い違いがあります。

class User < Hanami::Entity
end

Hanamiのconsoleで試して見ると、違いが分かります。Userクラスをインスタンス化する時に値は入れられるものの、後から代入しようとすると値が入りません。

つまり、Entityはイミュータブルな設計になっており、initializeの段階でしか値を入力することが出来ません。

つまり、Entityはデータをオブジェクトとして扱うためのただの入れ物です。

user = User.new(email: 'test@gmail.com')
user.email
#=> "test@gmail.com"
#=>
user.email = 'hoge@gmail.com'
#=> NoMethodError: undefined method `email='

Repository

それでは、どうやって値を代入するかですが、その役割はRepositoryが担います。つまり、DBへの更新はRepositoryの役割であり、SQLを発行するのもここが担います。

createupdatedeleteallfindなどの基本メソッドは、Hanami::Repositoryを継承するだけで予めサポートされます。他に必要なメソッドは以下のように自作する必要があります。

また、ここに複雑なロジックを噛ませることも可能です。しかし、ここにトリッキーなメソッドを定義するのは自殺行為なのでやめた方がいいです。ここでバグを仕込むとDBに保存されるデータが汚染され、恐ろしいことになるので、ロジックは出来るだけ別階層に書くべきです。遊びで試したところ、思い切ったクエリも実行出来るので下手なことをすると地獄を見そうで怖いです。

lib/src/repositories/user_repository.rb
class UserRepository < Hanami::Repository
  def find_by_email(email)
    users.where(email: email).first
  end
end

ちなみに使うときはこんな感じです。

user_repository = UserRepository.new
@user = user_repositor.find_by_email("test@gmail.com")

View

ViewはRailsとほとんど同じです。デフォルトのTemplateに、eRuby(.erb)が使用されています。
大きな違いはViewクラスが用意されている点です。

apps/web/views/users/show.rb
module Web
  module Views
    module Users
      class Show
        include Web::View

        def title
          'あるユーザーの情報'
        end
      end
    end
  end
end

あとはerbから呼び出すだけで使用できます。Rails経験者は勘が働くと思いますが、<h2>タグで呼び出されているコードはControllerで説明します。

apps/web/templates/users/show.html.erb
<h1><%= title %></h1>
<h2><%= user.name %></h2>
<h2><%= user.email %></h2>

どうやら、Viewクラスが存在することで、パーシャル化を行う際に非常に重宝するとの解説が見られますが、業務であまりいじらない箇所なので詳細な知見までは把握出来ていません。

Controller

Generaterの章で少し触れますが、HanamiのContorollerはAction毎に別ファイルに記載します。RailsだとControllerクラス配下のメソッドで定義されていたものが、Actionクラス毎に定義されるのだと理解すると分かりやすいと思います。

apps/web/controllers/users/show.rb
module Web::Controllers::Users
  class Show
    include Web::Action

    params do
      required(:id).filled(:str?)
    end

    expose :user

    def call(params)
      if params.valid?
        redirect_to '/'
      else
        self.status = 422
        return
      end

      @user = UserRepository.new.find(params(:id))
    end
  end
end

フォームから送信された値がcallメソッドに引数として渡され、処理が行われます。このとき、params.valid?で以下のように定義されたValidationが実行されます。

今回は文字列かどうかのチェックだけが行われていますが、オリジナルのメソッドを定義すれば、有効なEmailアドレスの文字列が不正ではないかなどのチェックもここで可能です。

params do
  required(:id).filled(:str?)
end

そして、Viewの章で少し触れていますが、インスタンスメソッドを以下のように定義することで、View側に渡すことが出来ます。そして、Railsと同様にテンプレート内で使用が可能です。

ちなみに、Railsと違い、ここで定義しないとインスタンス変数は外に出ませんので注意が必要です。

expose :user

Interactor

最後に、一番馴染みがない名前が登場します。この用語の解説を始めると、ドメイン駆動設計の話やクリーンアーキテクチャの話に広がってしまうため、今回は省略します(私も勉強中のため、説明出来るほど理解が進んでいない。。)。

そこで、ここでは、RepositoryとControllerの間に入る層だと思えば、少し理解しやすいと思います。

また、Repositoryの章で複雑なロジックは、Repositoryクラスに書かないように注意を促しましたが、HanamiのビジネスロジックはこのInteractorに書くことになります。

例えば、Controllerの章で例にしたActionのロジックを移設するとこんな感じです。

lib/bookshelf/interactors/user/show.rb
require 'hanami/interactor'

module UserInteractor
  class Show
    include Hanami::Interactor

    expose :user

    def call(id)
      @user = UserRepository.new.find(id)
    end
  end
end

そして、Controllerはこのようになります。

apps/web/controllers/users/show.rb
module Web::Controllers::Users
  class Show
    include Web::Action
    include Hanami::Interactor

    params do
      required(:id).filled(:str?)
    end

    expose :user

    def call(params)
      if params.valid?
        redirect_to '/'
      else
        self.status = 422
        return
      end

      @user = UserInteractor::Show.new.call(params[:id])
    end
  end
end

業務上の経験でしかありませんが、基本的にビジネスロジックはInteractorに詰め込んでいくため、ちょっとした仕様変更だとここを弄るだけで済んでしまうことがあり、個人的にはHanamiの良さだと思っています。

最後に

長い解説になってしまいすみません。。もし、少しでも興味を持っていただけるのなら、Hanamiに触れてみてください。

また、今回は、Advent Calendar に間に合わせるために、サンプルを用意出来ませんでしたが、検証用のdocker-composeとサンプルリポジトリをあとで追加したいと思います。

以上です。

参考文献

HanamiはRubyの救世主(メシア)となるか、愚かな星と散るのか
Hanami Getting Started guide

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした