🧿

Rubyの小さなライブラリ群によるアプリでクリーンアーキテクチャを理解する

2022/11/01に公開約3,400字

クリーンアーキテクチャですが、hanami/hanami: The web, with simplicity. と出会ってから5周回くらいして、ようやく腹落ちしつつあるのでまとめてみます。

フレームワークを利用しつつ理解するのは難しい

フレームワークを利用した場合、例の図のUsecaseの外堀をグルッと埋める形になります。MVCならController、View、Modelです。Hanamiの場合はRepositoryが来ます。個人的にはこの内側から3層目が全て一体化していると、どうしてもそれらが中心であるように感じてしまうんですよね。

そこでアプリケーションのそれぞれの責務を別のライブラリを利用して実装することを試してみました。

これです。 cc-kawakami/clean-architecture-minimal-app: A minimal Clean Architecture app

利用したライブラリはこれらです。

「Entityあれ。」

アーキテクチャの中心から攻めていきます。まずはEntity。

class User
  attr_reader :id, :name, :email

  def initialize(id:, name:, email:)
    @id = id
    @name = name
    @email = email
  end
end

普通のクラスですね。ビジネス上でどんな情報を扱うかを定義しています。今回はユーザーを管理することを想定してUser entitiyを用意しました。

次にアプリのビジネスロジック

このアプリではユーザーをIDで検索できるようにします。

class FindUser
  include Hanami::Interactor

  expose :user

  def initialize(repository:, serializer:)
    @repository = repository
    @serializer = serializer
  end

  def call(id)
    begin
      #ユーザーを探す
    rescue => e
      error!(e.class.name)
    end
  end
end

こんな感じのUsecaseを用意しました。これがアプリがビジネス上のどんな目的を果たすかを定義できました。Repository, SerializerはEntityをとってくるところと出力するところのAdapterですね。これをUsecaseが利用します。

あとはAdapterを拵えていく

Repository。

class UserRepository < ROM::Repository[:users]
  commands :create

  def find(id)
    users.by_pk(id).map_to(User).one
  end
end

Controller。

get "/users/:id" do
  find = FindUser.new.call(params["id"])

  if find.success?
    if find.user
      status 200
      body = find.user
    else
      status 404
      body = { error: "Not found!" }
    end
  else
    status 500
    body = { error: find.error }
  end

  json body
end

Serializerです。

class UserSerializer < Blueprinter::Base
  identifier :id

  fields :name, :email
end

これで揃いました。

あとは、さっき出てきたUsecaseの詳細な記述をしていきます。

class FindUser
  include Hanami::Interactor

  expose :user

  def initialize(
    repository: UserRepository.new(App.new.rom),
    serializer: UserSerializer
  )
    @repository = repository
    @serializer = serializer
  end

  def call(id)
    begin
      user = @repository.find(id)
      @user = @serializer.render_as_hash(user)
    rescue => e
      error!(e.class.name)
    end
  end
end

実行する

$ curl http://127.0.0.1:9292/users/1
{"id":1,"email":"smith@exmaple.com","name":"Smith"}

OK。ここまでの流れで、HTTPリクエスト・レスポンスは、FindUserのビジネスロジックの入出力の詳細の一つに過ぎないことが体感できましたでしょうか。
同じ様に、DBはUserを生成するための実装の一つでしかありません。DBやHTTPは詳細なのです。

責務を混ぜることが難しい = 責務が混ざらない

この設計なら、ControllerにEntityの検索が記述されることは、よほど工夫しない限り起こらないということが分かるでしょう。DBやHTTP、HTML、JSONの詳細はUsecaseを起点として、円のそれぞれの方向へ分散される、という設計に自然と誘導されます。

以上です。ありがとうございました。

Discussion

ログインするとコメントできます