はじめに
新しい職場に来てもう少しで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
コマンドが用意されています。細かいところで違いはあるものの、ほとんどrails
がhanami
になっただけです。
プロジェクトの作成
bundle exec hanami new . --database=mysql
Hanami console
bundle exec hanami console
ディレクトリ構造
上記のコマンドを実行した後のディレクトリ構造はこんな感じです。
今回、環境にdocker-composeを利用しています。dbディレクトリとDocker関係のファイルは気にしないで下さい。
apps配下にRailsでも馴染みがあるcontrollers
、templates
、views
が存在します。
そして、Railsと大きく違う点は、lib配下にDDDらしさが伺えるentities
とrepositories
が存在する点です。簡単に説明すると、この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と同じ感覚でこのあたりさえ覚えておけば、ほとんど使えてしまいそうです。
get '/', to: "users#show"
post '/new', to: "users"
または
resources :users, only: [:show, :create]
Model
まずは、hanamiコマンドでModelを作成します。以下のコマンドを実行すると、entities
、repositories
が作成されます。
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とそれほど変わりません。
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を発行するのもここが担います。
create
、update
、delete
、all
、find
などの基本メソッドは、Hanami::Repository
を継承するだけで予めサポートされます。他に必要なメソッドは以下のように自作する必要があります。
また、ここに複雑なロジックを噛ませることも可能です。しかし、ここにトリッキーなメソッドを定義するのは自殺行為なのでやめた方がいいです。ここでバグを仕込むとDBに保存されるデータが汚染され、恐ろしいことになるので、ロジックは出来るだけ別階層に書くべきです。遊びで試したところ、思い切ったクエリも実行出来るので下手なことをすると地獄を見そうで怖いです。
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クラスが用意されている点です。
module Web
module Views
module Users
class Show
include Web::View
def title
'あるユーザーの情報'
end
end
end
end
end
あとはerbから呼び出すだけで使用できます。Rails経験者は勘が働くと思いますが、<h2>
タグで呼び出されているコードはControllerで説明します。
<h1><%= title %></h1>
<h2><%= user.name %></h2>
<h2><%= user.email %></h2>
どうやら、Viewクラスが存在することで、パーシャル化を行う際に非常に重宝するとの解説が見られますが、業務であまりいじらない箇所なので詳細な知見までは把握出来ていません。
Controller
Generaterの章で少し触れますが、HanamiのContorollerはAction毎に別ファイルに記載します。RailsだとControllerクラス配下のメソッドで定義されていたものが、Actionクラス毎に定義されるのだと理解すると分かりやすいと思います。
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のロジックを移設するとこんな感じです。
require 'hanami/interactor'
module UserInteractor
class Show
include Hanami::Interactor
expose :user
def call(id)
@user = UserRepository.new.find(id)
end
end
end
そして、Controllerはこのようになります。
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