Hanami で小さな開発をやってみた。

hanami でブックマークマネージャを作った - ユユユユユ (https://jnsato.hateblo.jp/entry/2020/06/15/230000)

 単機能のウェブページに過ぎず、 Rails であれば1日でできただろうと思う。 hanami の機能や API を追いかけながらの実装だったので、やや時間はかかった。とはいえ、それでもファースト・コミットから最初のデプロイまでは3日で済んでいる。感触としてはもっとかかった印象があったため、これは意外にも思う。

車輪の再発明

 認証やページネーション、ヘルパーのアルゴリズムも自分で書いた。時間を要したのはむしろこれが大きかったかもしれない。なぜわざわざ車輪の再発明に舵をとったか? それは、少なくない数の人気 gem が、そもそも Rails の拡張として定義されており、 Rails ではないアプリケーションから利用することそのものが、ただちに面倒に片足を突っ込むことのように思われたからである。

 過去の自分であれば、「車輪の再発明」を頭ごなしに否定して、すぐに gem に飛びついていただろう。依存先の gem に合わせてアプリの形を変えさえもしたかもしれない。それは否めない。

 しかし hanami のようにシンプルなフレームワークを使っていると、むしろ外部に余計な依存先を持ってしまうことの方がリスクに感じられた。例えば、タイムスタンプを「○日前」と文字列に変換するだけのために、 ActiveSupport を丸ごと取り込むのは、理にかなった判断とは言えまい。明らかに自分で書ける範疇の機能であったから自分で書いたし、車輪の再発明は大いに勉強になるという思いは改めて持った。

アーキテクチャとしての Hanami

Hanami architecture is heavily inspired by Clean Architecture. 1

 Hanami はクリーン・アーキテクチャに強く影響されたフレームワークである、と言明されている。これは具体的にどういうことを意味するか?

 Hanami のパラダイムにおいては、単一責任の原則にのっとって、基本的には #initialize#call しかパブリックメソッドを持たないクラスをいくつも作ることになる。そしてそれらのクラスは依存性の注入によって巧妙に切り離されている。

 例えば、 公式ガイド から次の例を引用する2

# lib/bookshelf/interactors/add_book.rb

require 'hanami/interactor'

class AddBook
  include Hanami::Interactor

  expose :book

  def initialize(repository: BookRepository.new)
    @repository = repository
  end

  def call(book_attributes)
    @book = @repository.create(book_attributes)
  end
end

 AddBookクラスはBookRepositoryを経由してデータベースに依存していることになる。ただし、ここでは#initializeの引数としてrepositoryインスタンスへの依存を注入することができるように設計されているため、依存を差し替えて単体テストを行うことは容易である。

# spec/bookshelf/interactors/add_book_spec.rb

RSpec.describe AddBook do
  let(:attributes) { Hash[author: "Toni Morrison", title: "Beloved"] }
  let(:repository) { instance_double("BookRepository") }

  it "invokes repository#create to persist data" do
    expect(repository).to receive(:create)
    AddBook.new(repository: repository).call(attributes)
  end
end

 Ruby にはインターフェイスの概念が存在しないがゆえに、厳密にはクラス間の結合は切り離されていないと指摘する余地はある。しかし少なくともこのデザインによって、コンポーネントからデータベースへの直接の依存は解消されて、単独でテスト可能なクラスとして整合性を保っている。これはなかなか目から鱗のデザインパターンであった。

「ビジネスロジック」への嗅覚

なぜ Interactor がそんなにも特権的な位置づけになるのだろう? それは、ビジネスルールを含んでいるからだ。 Interactor は、アプリケーションの最上位レベルの方針を含んでいる。そのほかのコンポーネントは、周辺にある関心事を処理している。3

 これは『クリーン・アーキテクチャ』の「オープン・クローズドの原則」からの引用である。「ビジネスロジックを依存ヒエラルキーの最上位におく」という有名な思想が最初に言及される箇所だ。

 この考え方に依拠して「ビジネスロジック」について語るまねをした経験もある。しかし実をいうと「Interactor」という概念については具体的なイメージを持てていなかった。反省しつつ、 Hanami による開発を通して、 interactor の概念を具体的な実装のパターンとして理解できたと思うし、ひいては「ビジネスロジック」への嗅覚も鋭くなったように思う。

 Interactor は、データの永続化を司る repository クラスと、リクエストを捌く controller クラスの間に立って仲介するクラスである。要するに、データの入出力をコントローラに手続き的に書くのではなく、クラスとして定義することで宣言的な呼び出しを可能にしている、というのが僕の解釈。

 Rails でいうところのサービス層にあたるものをイメージすればよい。そして、 Rails プロジェクトにおけるサービス層の導入が物議をかもすのとは対照的に、 hanami はフレームワークとしてデザインパターンを用意してくれている。これはありがたい。もっとも、プロジェクトの規模が大きくなるになるにつれて何かしらの歪みは生じるのであろうが、それはまた別の話とする。

 極論にはなるが、サービス層を導入するにしても、クラスの設計としては initialize と call 以外は許容しない、という制約があればどうだろうか? などと考えた。粒度の異なるクラスに、粒度の異なるパブリックメソッドが乱立してしまい、プロジェクト全体にカオスが招き入れられてしまう、というシナリオは回避できるのではないかと思うのだが、どうだろう?

おわりに

 などなど、 Hanami による開発の経験は、多くの示唆を与えられる機会となった。一般的に言っても、 Hanami の思想に立って Rails を省みることで、新しい視点が得られるということは少なからずあると思う。与えられた問題意識については、遅かれ早かれじっくりと考え直す機会が訪れるだろうと思うから、その時のアイデアとして取っておくことにする。

  1. https://guides.hanamirb.org/introduction/getting-started/ 

  2. https://guides.hanamirb.org/architecture/interactors/ 

  3. Robert C. Martin, 角 征典, 高木 正弘. Clean Architecture 達人に学ぶソフトウェアの構造と設計 (アスキードワンゴ)[Kindle 版]. ASIN: B07FSBHS2V. 位置1301.