クリーンアーキテクチャですが、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
利用したライブラリはこれらです。
- Usecase: utils/interactor.rb at v1.3.8 · hanami/utils
- Controller: sinatra/sinatra: Classy web-development dressed in a DSL (official / canonical repo)
- Serializer: procore/blueprinter: Simple, Fast, and Declarative Serialization Library for Ruby
- Object Mapper: rom-rb/rom: Data mapping and persistence toolkit for Ruby
- DB: sparklemotion/sqlite3-ruby: Ruby bindings for the SQLite3 embedded database
「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を起点として、円のそれぞれの方向へ分散される、という設計に自然と誘導されます。
以上です。ありがとうございました。