28
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

TISAdvent Calendar 2018

Day 8

筋トレからはじめる💪ドメイン駆動設計

Last updated at Posted at 2018-12-08

はじめに

エリック・エヴァンスのドメイン駆動設計 に出てくるエンティティ・値オブジェクトを筋トレに絡めて解説する今までにない新しい記事がこれです。

読者対象

  • 筋トレしてる人
  • もっと良いコードを書きたいと思っている人
  • もっと良い設計をしたいと思っている人

前提知識

  • Kotlinの基本的な知識(サンプルコードはKotlinです)

この記事で取り上げること

  • エンティティ
  • 値オブジェクト

ドメイン駆動設計

ドメイン駆動設計とは、システム化対象ビジネスの複雑性と戦う道具の一つです。複雑性と戦うために、ドメイン駆動設計では対象ビジネスの概念、考え方をドメインモデルとしてモデル化します。このドメインモデルがとても重要です。ドメイン駆動設計ではドメインモデルを中心に設計開発を進めていくためです。

しかし、学習をしていくうえでドメインモデルを一番最初に学ぶことは難易度が高いように思います。ドメインモデルは対象に応じて様々な形態をとるものであり、説明も概念的になりがちです。これは筋トレに例えると、最初からデッドリフトに挑戦するようなものです。デッドリフトでは正しいフォームが大切であり、初心者が最初から手を出しても効果的なトレーニングが行えません。同じ様に、ドメインモデルを最初から学ぼうとしてもあまりに概念的すぎて学習が効率的、効果的にならない可能性があります。そこで、本記事では実装上の課題をドメイン駆動設計ではどのように解消するのかを中心に説明し、どのような実装になるかを示します。説明の際にはドメインモデルの内容を省きます。

本記事がドメイン駆動設計の学習の入り口(もしくは筋トレに興味を持つきっかけ)として役に立てればと思います。

エンティティ

ジムのユーザ管理システムを構築する場合、ユーザー一人一人を区別して管理する必要があります。例えば田中さん、佐藤さん、鈴木さんといった人がそのジムに登録した場合、システムはこれらの人を区別して管理する必要が出てきます。この場合、それぞれの人をどのように区別するべきでしょうか?

一つの案として筋肉量を比較するというものがあります。この場合、それぞれのユーザの筋肉量が異なれば、それぞれを別の人物として管理することができるでしょう1(田中さん50%、佐藤さん45%、鈴木さん55%)しかし、ここで田中さんが追い上げて筋肉量が55%になってしまうとそれぞれのユーザーを区別することが出来なくなってしまいます。(鈴木さんと同じになってしまう)筋肉量はその人の一生を通じて固定したものではありません。筋肉は鍛えていれば増加します。プロテインを飲めばなおさらです。

このシステムの場合、ユーザーを区別する場合においてはそれぞれの属性に着目することは良くありません。名前、年齢、体重、筋肉力などの属性があったとしても、それらは誰かを表すものではなく、ある時点でのその人の情報でしかありません。年齢は毎年増えますし、名前も変えることが出来ます。

ユーザーの属性が変化しても、同じ属性を持つユーザーがいたとしても、それぞれを区別・追跡する必要があります。属性だけでは区別できない性質は同一性と言われ、同一性を持つものはエンティティと言われます2

エンティティの比較

エンティティ同士はそれらが持つ同一性をもとに比較する操作が必要です。エンティティに対して識別子(ID)を割り振り、それをもとに比較することがよく行われます。

IDは一度割り当てたら再度変更することが出来ないようにします。値自体をイミュータブルにし、JavaやKotlinでSetterをIDのフィールドにつけないようにします。さらにシステムがエンティティをどのような形態(Javaオブジェクト、Kotlinオブジェクト、JSON、DBスキーマ)にしたとしても、IDの表現形態が変わったとしても、IDによる比較が適切に行われるようにします。

data class User(
	val id: Long, // ユーザーのIDを定数で表現する例
	val name: String,
	val years: Int,
	val strength: String
)

val suzuki = User(19082, "Suzuki", 25, "999")
{
  "id" : "19082", // JSON形式になった時のIDの表現例
  "name" : "Suzuki",
  "years" : 25,
  "strength" : "999"
}

ID同士を比較することで同一性を比較出来ます。比較の方法は様々ですが、私がよくやるのは、エンティティを表現するレイヤースーパータイプを用意し、エンティティの比較方法をシステムの中で統一させてしまいます。

// エンティティを表すレイヤースーパータイプ
interface Entity<T> {

	  // 同一性を比較する
	  // 同一性がある場合true、ない場合false
    fun isSameIdentityOf(entity: T): Boolean
}

// 上記のレイヤースーパータイプを実装するクラス
data class User(
        val id: Long,
        val name: String,
        val years: Int,
        val strength: String
) : Entity<User> {

    // 同一性をIDを元に比較する
    override fun isSameIdentityOf(entity: User): Boolean {
        return id == entity.id
    }

}

IDは上記のようにLong型としてエンティティに持たせることもできますが、特に理由がない限り、IDは後述する値オブジェクトを使うことをオススメします。識別子に関する操作を値オブジェクトの中に集約できますし、実際のIDの値が何なのか(StringLongInt...)について気にしなくてよくなります。さらに、IDの表現形式を変更しても、システム全体に影響が及びません。

// ユーザの識別子クラス
data class UserId(val rawId: Long)

data class User(
        val id: UserId, // Long型ではなくUserId型を使用
        val name: String,
        val years: Int,
        val strength: String
) : Entity<User> {

    override fun isSameIdentityOf(entity: User): Boolean {
        // UserIdクラスは data class なので、自動生成された equals を使用できる
        return id == entity.id
    }

エンティティの識別子の生成

筋肉は主にたんぱく質、糖質、ミネラルから生成出来ますが、エンティティの識別子はどのようにして生成するのでしょうか。

考えられる方法としては以下のものがあります。

  • ユーザーが指定する
  • アプリケーションが自動生成する
  • 永続化システムが自動生成する

ユーザーが指定するとは、ジムの入会などでユーザがIDを直接入力し、それをユーザの識別子として利用すると言うことです。この場合、システムが識別子を自動生成する仕組みを持つ必要はありませんが、システム内でユーザが指定してきた識別子に一意性があることを保証する必要が出てきます。でなければ同じユーザIDを持つジム会員が発生してしまいます。(みんな他人の請求書を受け取りたくはないはずです)

アプリケーションが自動生成する方法は様々なものがあります。(ID生成大全)例えば、UUIDを使用して識別子を自動生成する場合以下のように実装できます。

// UserIdを生成するインターフェイスを定める
// インターフェイスを設けることで、UserIdをどのように生成するのかという関心事を分離できる
interface UserIdGenerator {
    
    fun nextId(): UserId
    
}

// UUIDを用いてUserIdを生成するクラス
class UUIDUserIdGenerator : UserIdGenerator {

    override fun nextId() = UserId(UUID.randomUUID().toString())

}


// アプケーションが自動生成したIDを使用してユーザクラスをインスタンス化
val userIdGenerator: UserIdGenerator = UUIDUserIdGenerator()
val sato = User(userIdGenerator.nextId(), "Sato", 30, "8383")

注意すべき点は、アプリケーションが自動生成する識別子は人間にとって読みやすいものではないということです。なので、識別子を直接表示したり、識別子の入力を求めたりすると使いづらいシステムになってしまいます。

最後の永続化システムが自動生成するとはデータベースのシーケンス値といったものを使用するということです。永続化システムにエンティティクラスごとのシーケンスジェネレーターを用意し、アプリケーションからその値を採番することで識別子を生成します。この方法のメリットは採番した値に一意性があることを保証しやすい点です。一方、デメリットは識別子を取得するのに時間がかかることです。エンティティを生成するたびに永続化層にアクセスしに行く必要が出てくるからです。このデメリットの回避策としてシーケンス値をキャッシュする方法が考えられます。しかし、永続化システムが再起動するなどしてキャッシュが破棄されてしまうと、使用されない値が生まれてしまう点に注意してください。

値オブジェクト

ジムでダンベルを上げるときはその重さに注意を払います。自分の負荷が低すぎると当然筋肉が引き締まりません。高すぎると怪我をします。今のコンディションにフィットする重さを選ぶよう注意しましょう。重さが同じであれば、どのダンベルを使っても大丈夫です。

このようなある対象の属性や、何らかの物事を記述することにしか関心がない場合、値オブジェクトとして扱うことが出来ます。値オブジェクトが表現するのは何であるか(ダンベルの重さ、たんぱく質量、消費カロリー)だけを表現し、どれであるか(XX社が2018年に発売した製造番号YYYYYのダンベル)は表現しません。同一性は与えず、比較を行うときには属性のみ対象にするようにします。

値オブジェクトは基本的に以下の特徴があります。

  • システム化対象領域のある概念の、計測したり、定量化したり、説明したりする。
  • 完全に置き換えることができる。
  • 不変なものとして扱うことができる。
  • 等しい値かをオブジェクト同士で比較できる。

例えば、ジムのユーザー管理システムが、それぞれの人がその日使用したダンベルを記録するようになったとしましょう。その時、ダンベルの重さにしか注目する必要がないとします。この場合、ダンベルを以下のうように値オブジェクトとして実装できます。

// レイヤースーパータイプ
interface ValueObject<T> {

    // 同値性を比較する
    // 同値である場合true,同値でない場合false
    fun isSameValueAs(valueObject: T): Boolean
}

// ダンベルを表現する値オブジェクト
data class Dumbbell(
        val weight: Int // ダンベルの重さを表現する
) : ValueObject<Dumbel> {

    override fun isSameValueAs(valueObject: Dumbel): Boolean {
        return weight == valueObject.weight
    }

}

値オブジェクトはイミュータブル

値オブジェクトは状態を変更出来ないようにします。Setterを用意したり、メソッドの中で値の更新などを行なってはいけません。なぜこのようなことをするのでしょうか。

値オブジェクトが変更可能である場合、開発者はその値オブジェクトの状態を常に気をつけていかなくてはなりません。例えば、鈴木さんが50kgのダンベルでアームカールを50回行なったとしましょう。その場合、以下のように書くことが出来ます。

// 属性の変更が可能なダンベルクラス
data class Dumbbell(
        var weight: Int
) : ValueObject<Dumbbell> {

    override fun isSameValueAs(valueObject: Dumbbell): Boolean {
        return weight == valueObject.weight
    }

    // 指定された重さに変更
    fun changeWeight(weight: Int) {
        this.weight = weight
    }
}

// 鈴木さんが50kgのダンベルで50回アームカールを実施
val dumbbell = Dumbbell(50)
suzuki.training(dumbbell = dumbbell, count = 50)

この後、dumbell変数に対して重さの変更を行ってしまったとすると、鈴木さんが100kgのダンベルでアームカールを50回行ったことになってしまいます。筋トレとしては素晴らしい成果ですが、不正な記録です。

// ダンベルの重さを100kgに変更
dumbell.changeWeight(100)

こういった状況はシステムが複雑になればなるほど管理するのが難しくなっていきます。思わぬところでバグを作ってしまったり、システムの理解容易性や拡張性を妨げる原因にも繋がります。

値オブジェクトの状態を変更できないようにすると、上記で挙げたようなことは起こらなくなります。そうすることで、 開発者は値オブジェクトを扱う時、どのインスタンスなのかを気にする必要がなくなります。 その値オブジェクトが別のインスタンスからも同時に参照されていようが、あるいはサブルーチンに参照ごと渡そうが、値オブジェクトの状態は変更されることがありません。このようにすることで、システムをよりシンプルに、より理解しやすくできます。

値オブジェクトの交換可能性

上記で示したように値オブジェクトは不変値である必要があります。しかし、値オブジェクトが示す値を変更したい(ダンベルの重さが50kgから100kgになったとか)場合もあるはずです。その場合、値オブジェクト全体を置き換えることで対応します。

例えば50kgでトレーニングしていると思ったら実は100kgだったなんてことがあったとします。その場合は50kgを表すダンベルの値オブジェクト自体を100kgで置き換えます。

// 50kgのダンベルで50回アームカールを実施
val dumbbell = Dumbbell(50)
suzuki.training(dumbbell = dumbbell, count = 50)

// 100kgに修正
suzuki.changeTrainingLog(dumbell = Dumbell(100))

この時のユーザクラスは以下のようになっています。

// ユーザを表すエンティティクラス
data class User(
        val id: Long,
        val name: String,
        val years: Int,
        val strength: String
) : Entity<User> {

    private var trainingLog: TrainingLog

    override fun isSameIdentityOf(entity: User): Boolean {
        return id == entity.id
    }

    // トレーニングの記録をつける
    public fun training(dumbell: Dumbell, count: Int) {
        trainingLog = TrainingLog(dumbell, count)
    }

    // トレーニング時のダンベルの重さを変更する
    public fun changeTrainingLogDumbell(dumbell: Dumbell) {
        trainingLog = TrainingLog(dumbell, trainingLog.count)
    }

    data class TrainingLog(val dumbell: Dumbell, val count: Int)

}

終わりに

この記事ではドメイン駆動設計のエンティティ・値オブジェクトについて説明しました。ドメイン駆動設計ではこの他にもサービス、リポジトリ、ファクトリ、集約などの実装パターンがあります。これらについては今後少しずつ説明できたらなと思っています。

参考

  1. エリック・エヴァンスのドメイン駆動設計
  2. 実践ドメイン駆動設計
  3. IDDD本から理解するドメイン駆動設計連載一覧:CodeZine(コードジン)
  4. 現場で役立つシステム設計の原則 〜変更を楽で安全にするオブジェクト指向の実践技法
  1. ここで言う筋肉量とは全体重のうち筋肉が占める重量の割合です。

  2. エリック・エヴァンスのドメイン駆動設計、第5章、エンティティを参照

28
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
28
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?