2021年5月14日金曜日

Sidekiq で DDD を使ってみた

Sidekiq で DDD を使ってみた

概要

前回 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 件のコメント:

コメントを投稿