前回に引き続き、Modelの設計の勉強として「ドメイン駆動設計入門」を読んでいます。サンプルコードはC#で書かれていますが、iOSを勉強している人にもオススメの一冊です。
今回は、13章に出てきた仕様パターンが面白かったので、Swiftのコードを使ってまとめます。
仕様パターンとは
仕様(Specification)は、オブジェクトの評価を行うオブジェクトです。
主に以下の2つの用途で使われます。
- 検証(バリデーション)
- 選択(フィルタリング)
それでは、仕様パターンを使わない方法から見ていきましょう。
元となるModel
例えば、ToDoアプリのドメインモデルとして、以下のようなModelがあるとします。
struct ToDo {
private let id: ID<Self>
private(set) var title: ToDoTitle
private(set) var detail: ToDoDetail
static func create(title: ToDoTitle, detail: ToDoDetail) -> ToDo {
let id = ID<Self>(UUID().uuidString)
return ToDo(id: id, title: title, detail: detail)
}
init(id: ID<Self>, title: ToDoTitle, detail: ToDoDetail) {
self.id = id
self.title = title
self.detail = detail
}
}
IDとToDoTitleとToDoDetailの定義は以下の通りです。
struct ID<T>: Equatable {
let value: String
init(_ value: String) {
self.value = value
}
}
struct ToDoTitle {
let value: String
init(_ value: String) throws {
guard case 1...30 = value.count else { /* エラーを投げる */ }
self.value = value
}
}
struct ToDoDetail {
let value: String
init(_ value: String) {
self.value = value
}
}
ToDoTitleには1〜30文字という制約をつけています。
それでは、ToDoモデルに以下の制約を追加します。
詳細(detail)が書かれているToDoのタイトル(title)の長さは20文字まで
このバリデーションロジックはどこに記述されるべきですか?
(1) ユースケースに記述する
これは最悪の選択肢です。ToDoのdetailやtitleの操作を行うすべてのユースケースに、このバリデーションロジックが点在することになるでしょう。
(2) モデルに記述する
ToDoモデル内にバリデーションロジックを持たせて、ユースケースから呼び出す方法です。
struct ToDo {
private let id: ID<Self>
private(set) var title: ToDoTitle
private(set) var detail: ToDoDetail
static func create(title: ToDoTitle, detail: ToDoDetail) -> ToDo {
let id = ID<Self>(UUID().uuidString)
return ToDo(id: id, title: title, detail: detail)
}
init(id: ID<Self>, title: ToDoTitle, detail: ToDoDetail) {
self.id = id
self.title = title
self.detail = detail
}
+ func isTitleLengthProper() -> Bool {
+ return detail.value.isEmpty || title.value.count < 20
+ }
}
一見良さそうに見えますが、この制約に変更が加わるたびに、ToDo内部を書き換えなければいけないというのは、良い設計ではなさそうです。
この評価ロジックは、別のオブジェクトに切り出す必要があります。
(3) 仕様(Specification)に記述する
ここで登場するのが仕様パターンです。
struct ToDoProperTitleLengthSpecification {
func isSatisfied(by todo: ToDo) -> Bool {
return todo.detail.value.isEmpty || todo.title.value.count < 20
}
}
このように複雑な評価のロジックを切り出すことで、ドメインルールを分離し、ドメインモデルであるToDo内部の複雑化を回避できます。
リポジトリと仕様でフィルタリング
仕様パターンはバリデーションの他にも、リポジトリパターンと組み合わせてフィルタリングを行うこともできます。isSatisfiedがtrueなModelだけを抽出して取得するようにします。
口コミアプリでお店を検索する例を考えてみましょう。
protocol Specification {
associatedtype T
func isSatisfied(by shop: T) -> Bool
}
protocol ShopRepositoryProtocol {
func find<Spec: Specification>(specification: Spec) -> [Shop] where Spec.T == Shop
}
具体的には、人気なお店の情報だけを取得したいとします。人気なお店とは、星3.5以上かつ口コミ数が100以上の店であるとします。
struct PopularShopSpecification: Specification {
typealias T = Shop
func isSatisfied(by shop: Shop) -> Bool {
shop.rating > 3.5 && shop.reviews.count > 100
}
}
struct ShopRepository: ShopRepositoryProtocol {
func find<S>(specification: S) -> [Shop] where S: Specification, S.T == Shop {
let shops = /* フェッチ */
return shops.filter { PopularShopSpecification().isSatisfied(by: $0) }
}
}
これによって、「人気の店」というドメインルールに従ってフィルタリングを行うことができるようになります。
let repository = ShopRepository()
let specification = PopularShopSpecification()
let shops = repository.find(specification: specification)
全件フェッチしてModelに変換してフィルタリングを行うので、パフォーマンスについては考える必要がありますが、設計上は綺麗になり、コードは読みやすくなります。
まとめ
ルールをオブジェクトとして切り出すこのデザインパターンは、ドメイン駆動設計の考え方にとてもよくフィットしていて面白いと思いました。複雑な評価ロジックは仕様パターンでカプセル化しましょう。
参考