はじめに
- この資料は社内勉強会用に用意したスライドです
- ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本第6章の内容を参考にしています
もくじ
- アプリケーションサービスとは
- ユースケースを組み立てる
- ドメインルールの流出
- アプリケーションサービスと凝集度
- アプリケーションサービスのインターフェース
- サービスとは何か
アプリケーションサービスとは
- アプリケーションサービス=ユースケースを実現するオブジェクト
- アプリケーションとは?
- 利用者の目的に応じたプログラムのこと
- 利用者の目的はユースケースとして定義される
- つまりアプリケーション=ユースケースの集合
- アプリケーションサービスは、アプリケーションを構築するユースケースを実現する
ユースケースを組み立てる
- ユースケースはドメインオブジェクトを用いて実現する
- ユースケース=ドメインオブジェクトを用いて行う手続きの定義
例 ユーザーの登録処理を実装する
user.rb
class User
attr_reaeder :name, :id
def initialize(id: nil, name:)
@name = url_name
@id = id
@id ||= UserId.new(value: SecureRandom.uuid)
end
def change_name(name:)
raise Error if name.nil?
@name = name
end
end
user_id.rb
class UserId
attr_reader :value
def initialize(value:)
raise Error unless value.is_a?(String)
@value = value
end
end
user_service.rb
class UserService
def initialize(user_repository:)
@user_repository = user_repository
end
def exists?(user:)
!@user_repository.find(id: user.id).nil?
end
end
user_repositable.rb
module UserRepositable
def find(id:)
raise NotImprementedError
end
def save(name:)
raise NotImprementedError
end
end
user_repository.rb
class UserRepository
include UserRepositable
@@db = {}
def find(id:)
@@db[id.vallue]
end
def save(user:)
@@db[user.id.value] = user
end
end
user_application_service.rb
class UserApplicationService
attr_reader :user_repository, :user_service
def initialize(user_repository:, user_service:)
@user_repository = user_repository
@user_service = user_service
end
def register(name:)
user = User.new(name: name)
user_repository.save(user: user) unless user_service.exists?(user: user)
end
end
ユースケースを組み立てる2
- 前ページの実装では、
UserApplicationService#register
がUser
のインスタンスを返している - ドメインオブジェクトを返すかどうかは重要な判断
- ドメインオブジェクトを返す=受け取った先でドメインオブジェクトへアクセスできてしまう
- ドメインオブジェクトの操作をアプリケーションサービス外で許容している
- ドメインオブジェクトを返すと判断したときは、以下のようなことができてしまうことを知っていないといけない
# アプリケーションサービスを読んでいるどこかのコード
repositroy = UserRepository.new
application_service = UserApplicationService.new(user_repository: repository, user_service: UserService.new(user_repository: repository))
user = application_service.register(name: 'hoge')
user.change_name(name: 'fuga') # アプリケーションサービス外でドメインオブジェクトの操作をしている
ユースケースを組み立てる3
- アプリケーションサービスの外へのドメインオブジェクトの露出を防ぐ方法にDTO(Data Transfer Object)という考え方がある
- ドメインオブジェクトのプロパティのみを含んだオブジェクト
- DTOを用いることで、ドメインオブジェクトの操作はアプリケーションサービスに閉じることを強制することができる
user.rb
class User
attr_reaeder :name, :id
def initialize(id: null, name:)
@name = name
@id = id
@id ||= UserId.new(value: SecureRandom.uuid)
end
def change_name(name)
raise Error if name.nil?
@name = name
end
class Data
attr_reader :id, :name
def initialize(user)
@id = user.id
@name = user.name
end
end
end
user_application_service.rb
class UserApplicationService
attr_reader :user_repository, :user_service
def initialize(user_repository:, user_service:)
@user_repository = user_repository
@user_service = user_service
end
def register(name:)
user = User.new(name: name)
raise Error unless user_service.exists?(user: user)
if user_repository.save(user: user)
return User::Data.new(user)
end
end
def update_user(user:, name:)
user.change_name(name)
user_repository.save(user: user)
end
end
ユースケースを組み立てる4
- Update処理を考える
- 更新が行えるプロパティは複数指定できる(name, emailなど)
- 変更が行えるプロパティが増えたらアプリケーションサービスも変更する?
- Entityにアプリケーションサービスのインターフェースが依存する
- コマンドオブジェクトを用いることでインターフェースの依存を逆転させる→Rubyではハッシュ引数で解決できる
user_application_service.rb
def update_user(attributes)
user.change_name(attributes[:name]) if attributes[:name]
user_repository.save(user: user)
end
ドメインルールの流出
- アプリケーションサービスはドメインオブジェクトではない
- ドメインオブジェクトを用いてユースケースを実現する
- アプリケーションサービスにドメインの知識が記述されると、メンテナンス性が低下する
- アプリケーションサービスではドメインオブジェクトのメッセージパッシングでのみ処理を行う
アプリケーションサービスと凝集度
- 凝集度=モジュールの責任範囲がどれだけ集中しているか
- 凝集度が高い=堅牢性・信頼性・再利用性・可読性が高い
- LCOM=凝集度を測る方法の1つ
- メソッド内で使われているインスタンス変数の分割可能性が高いほど凝集度が低い
例
class Hoge
attr_reader :value1, :value2, :value3, :value4
def method1
value1 + value2
end
def method2
value3 + valu4
end
end
# => (value1,value2)と(value3, value4)は同じメソッドから参照されていないので分割できる
アプリケーションサービスと凝集度2
- 疑問1: Entityの場合はこれはよく起こることでは?
- Entityのプロパティを同じメソッドで参照することの方が珍しい
- 疑問2: アプリケーションサービスにおいてはパブリックインターフェースを1つだけにするのが理想なのでは?
- そもそも1つのアプリケーションサービスで複数のユースケースを実現しようとすることに違和感を感じる
- 1つのパブリックインターフェースを用意するのが、LCOM的には凝集度が必ず高くなると思う
アプリケーションサービスのインターフェース
- アプリケーションサービスのパブリックインターフェースを定義する
- アプリケーションサービスが開発をブロックしない
- インターフェースが同じであればモックも用意できる
class UserReidserService
attr_reader :user, :repository
def initialize(repository:, user:)
@user = user
@repository = repository
end
def call
if repository.save(user)
return User::Data.new(user: user)
end
end
end
サービスとは何か
- ドメインオブジェクトは、自己の振る舞いを持っている
- あるドメインにある実体をマッピングしたもの
サービスとは何か2
- ドメインサービスとは何か
- ドメインサービスにもドメインとしては存在する
- 例: ユーザーの重複チェック→実世界にも重複チェックをしている誰か(何か)はあるはず
- しかしその実体はドメインモデルにおいて表現する必要がない、または表現ができないものが、ドメインサービスとなる
- つまり、ドメインサービス=実世界のドメインからドメインモデルへマッピングされるときに実体を落とした振る舞いの総称
- だからドメインサービスは行動に焦点が当たる(誰がするか、ではなく何をするか)
サービスとは何か3
- アプリケーションサービスとは何か
- アプリケーションとは、実世界には関係ないプロダクトとして存在するもの
- アプリケーションとは1つの存在として自己の振る舞いを持っている
- ユーザー登録、ユーザー削除など
- しかしアプリケーションも実体としての存在はない(アプリケーションそのものが実体)
- アプリケーションはユースケースの集合体→ユースケースとはいわばアプリケーションが行う行動
- つまり、アプリケーションサービス=アプリケーションというものの振る舞いの総称
サービスとは何か4
- 本質的にドメインサービスとアプリケーションサービスには差がない
- 何を対象にしているかだけが異なる
- ドメインサービスはドメインを対象にした振る舞い
- アプリケーションサービスはアプリケーションを扱うユーザーを対象にした振るまい
- 何を対象にしているかだけが異なる
- サービスは状態を持たない
- 状態をもてない。自己の振る舞いを持たない=自己の状態をもてない