概要
前回 Sinatra に DDD を適用してみました 今回は更に Sidekiq を追加して非同期処理に DDD を適用してみます なお以下は前回のアプリを修正しているので修正箇所とポイントのみ紹介しています
環境
- macOS 11.3.1
- Ruby 3.0.0
- sinatra 2.1.0
- sidekiq 6.2.1
前提
Sidekiq をドメインとして抽出してもいいのですがどちらかというとプレゼンテーションレイヤーに近い機能だと思うので今回はプレゼンテーションレイヤーとして定義します 当然プレゼンテーションレイヤー以外でも定義可能なので今回の方法は参考程度に御覧ください
redis を使ってリポジトリを作成
Sidekiq の場合 Sinatra のプロセスとメモリ空間を共有することはできません 前回はインメモリリポジトリでデータを共有できないので redis を使ったリポジトリを作成します
ユーザデータは key/value で key にユーザ名を value にはユーザ情報を文字列の JSON で登録します
- vim redis/user/redis_user_repository.rb
require 'redis'
require 'json'
require './domain/model/user/user'
require './domain/model/user/user_id'
require './domain/model/user/user_name'
require './domain/model/user/user_mail'
class RedisUserRepository
def initialize
@client = Redis.new(
:host => 'localhost',
:port => 6379
)
end
def find(name)
user_value = @client.get(name.value)
return nil if user_value.nil?
user_hash = JSON.parse(user_value)
id = UserId.new(user_hash["id"])
name = UserName.new(user_hash["name"])
mail = UserMail.new(user_hash["mail"])
User.restore(name, mail, id)
end
def save(user)
@client.set(user.name.value, user.to_h.to_json)
end
end
リポジトリが増えてきたらインタフェース的なクラスを一つ作成してそれを継承して実装するほうが良いです またリポジトリのインタフェースは domain/model/user/user_repository_interface.rb という感じでドメイン配下に作成します
ドメインモデルの修正
Redis リポジトリ用に to_h メソッドを追加しました
- vim domain/model/user/user.rb
require 'securerandom'
require './domain/model/user/user_id'
require './domain/model/user/user_name'
require './domain/model/user/user_mail'
class User
def initialize(name, mail, id=nil)
raise Exception.new("A name must be in the UserName class") unless name.is_a?(UserName)
raise Exception.new("A mail must be in the UserMail class") unless mail.is_a?(UserMail)
@id = id || UserId.new(SecureRandom.uuid)
@name = name
@mail = mail
end
def self.restore(name, mail, id)
raise Exception.new("A name must be in the UserName class") unless name.is_a?(UserName)
raise Exception.new("A mail must be in the UserMail class") unless mail.is_a?(UserMail)
raise Exception.new("An id must be in the UserId class") unless id.is_a?(UserId)
@name = name
@mail = mail
@id = id
return User.new(name, mail, id)
end
def change_name(name)
raise Exception.new("A name must be in the UserName class") unless name.is_a?(UserName)
@name = name
end
def to_h
{
"id" => @id.value,
"name" => @name.value,
"mail" => @mail.value,
}
end
attr_reader :id, :name, :mail
end
Sidekiq ワーカーの作成
プレゼンテーションレイヤーにワーカーを作成します Sinatra のルーティングで受け取ったパラメータはプリミティブな型のまま Sidekiq に渡されるのでそれを元にコマンドを作成してアプリケーションサービスの機能を呼び出します
- vim presentation/worker/user_worker.rb
require 'sidekiq'
require 'logger'
require './domain/service/user_service'
require './redis/user/redis_user_repository'
require './application/user/user_application_service'
require './application/user/user_create_command'
class UserWorker
include Sidekiq::Worker
def perform(name, mail)
repository = RedisUserRepository.new
user_service = UserService.new(repository)
app_service = UserApplicationService.new(repository, user_service)
create_command = UserCreateCommand.new(name, mail)
user = app_service.create(create_command)
logger.info(user.to_h)
end
end
Sinatra アプリの修正
今回は非同期用の API を一つ追加しています
- vim presentation/app.rb
require 'sinatra'
require 'sinatra/json'
require 'json'
require './domain/service/user_service'
require './in_memory/user/in_memory_user_repository'
require './redis/user/redis_user_repository'
require './application/user/user_application_service'
require './application/user/user_create_command'
require './application/user/user_get_command'
require './presentation/worker/user_worker'
class DDDWebApp < Sinatra::Base
configure do
# repository = InMemoryUserRepository.new
repository = RedisUserRepository.new
user_service = UserService.new(repository)
app_service = UserApplicationService.new(repository, user_service)
set :repository, repository
set :user_service, user_service
set :app_service, app_service
end
get '/user/:name' do
get_command = UserGetCommand.new(params["name"])
user = settings.app_service.get(get_command)
json user.to_h
end
post '/user', :provides => 'json' do
body = JSON.parse request.body.read
create_command = UserCreateCommand.new(body["name"], body["mail"])
user = settings.app_service.create(create_command)
json user.to_h
end
post '/async_user', :provides => 'json' do
body = JSON.parse request.body.read
result = UserWorker.perform_async(body["name"], body["mail"])
json result
end
end
Sidekiq の設定を rack 起動時に読み込ませる
最後に Sidekiq の設定を行います アプリ起動時に行うのが良いので config.ru を変更します
- vim config/sidekiq_config.rb
require 'sidekiq'
class SidekiqConfig
def self.set_client
Sidekiq.configure_client do |config|
config.redis = { url: "redis://#{ENV['REDIS_HOST']}:#{ENV['REDIS_PORT']}" }
end
end
def self.set_server
Sidekiq.configure_server do |config|
config.redis = { url: "redis://#{ENV['REDIS_HOST']}:#{ENV['REDIS_PORT']}" }
end
end
end
- vim presentation/config.ru
require './presentation/app'
require './config/sidekiq_config'
SidekiqConfig.set_client
SidekiqConfig.set_server
run DDDWebApp
動作確認
アプリとワーカーを起動します
- bundle exec rackup presentation/config.ru
- bundle exec sidekiq -r ./presentation/worker/user_worker.rb
あとは REST にアクセスします 新たに作成した非同期 API を使ってユーザ登録が確認できることと元からあった同期 API を使って登録したユーザが取得できることを確認します
curl -XPOST localhost:9292/async_user -d '{"name":"user02","mail":"user01@mail.domain"}'
curl -XGET localhost:9292/user/user01
最後に
今回は Sidekiq のワーカーをプレゼンテーションレイヤーとして定義してみました 流れとしてはプレゼンテーション (Sinatra) -> プレゼンテーション (Sidekiq) という流れになっています プレゼンテーション -> アプリケーションサービスという流れが自然な感じもするのでアプリケーションサービスとして定義しても良いかなと思います
Sidekiq の仕様としてコンストラクタが持てないのとキューイングする際の perform の引数に ruby のオブジェクトは渡せないので Worker 側でアプリケーションサービスの作成を行っています これは Sidekiq を使う上ではどうしようもない仕様かなともいます 本当はワーカークラスに repository, user_service, app_service を Sinatra 側から引数で渡して持ちたいのですができないので perform 上で生成しています 書き方としてはよろしくない部分になっています (DI にしたほうがいい)
ワーカーの返り値が Sidekiq の jid になっています 本来であればここで返された値を使って進捗を確認することになるので、進捗を確認できる機能も必要になります
0 件のコメント:
コメントを投稿