頑張るときはいつも今

自称エンジニアのゴリラです。日々精進。

【読書】ドメイン駆動設計入門を読み始めた# 3章

DDDについて、本を読みながら勉強中です。

今回はエンティティについて学んだことの忘備録になります。

エンティティとは

  • ドメインモデルを実装したドメインオブジェクト
    • 値オブジェクトとの違いは、同一性によって識別されるか
  • 同一性が担保されるオブジェクトは属性が変更されても、オブジェクト自体は変更されない
    • 例えばユーザ情報
      • ユーザの属性(体重、メールアドレス)が変更されても、別のユーザオブジェクトにはならない
      • 属性ではなく、同一性(idみたいな物?)によって識別される

値オブジェクトは属性が変更されたら、全く異なるものになる。

(例えば、ユーザ名オブジェクトは、属性が変更されたら異なるものとして認識される。)

エンティティの性質

エンティティは、値オブジェクトとはま反対の下記の性質を持つ。

  • 可変である(属性の変更ができる)
    • 値オブジェクトは、オブジェクト自体の入れ替えによって変更を実現していた
  • 同じ属性であっても、区別される
  • 同一性によって区別される

可変であることをちょっと詳しく

ユーザ名を持つUserオブジェクトをエンティティとして実装したのが下のコード。

class User
    attr_reader :name

    def initialize(name)
        change_name(name)
    end

    # ユーザ名を変更するメソッド(パブリック)
    def change_name(name)
        raise Exception.new("ユーザ名が指定されていません") unless name
        raise Exception.new("ユーザ名は3文字以上入力してください") unless name.length < 3
        
        @name = name
    end
end

Userオブジェクトが持つnameは、change_nameメソッドを通じて、

直接、属性を変更することできる。

第2章の値オブジェクトを変更する時のコードと比較するとわかりやすい。

class FullName
    attr_accessor :first_name, :last_name
    def initialize(first_name, last_name)
        @first_name = first_name
        @last_name = last_name
    end
end

full_name = FullName.new('masaru', 'hogehoge')

# 代入によって値を交換する
full_name = FullName.new('masaru', 'fugafuga')

「同じ属性であっても、区別される」をちょっと詳しく

前回の値オブジェクトの比較する処理の場合は、属性を比較していた。

class FullName
    attr_accessor :first_name, :last_name
    def initialize(first_name, last_name)
        @first_name = first_name
        @last_name = last_name
    end

    def equals(full_name)
        return false full_name.null? || !full_name.is_a(FullName)

        return first_name.eql?(full_name.first_name) && last_name.eql?(full_name.last_name)
    end
end

Userモデルを考えると、同様に属性で比較した場合、同姓同名を持つUserは同じという判定になってしまう。

エンティティ同士の比較には、識別子(id)が利用される。

class UserId
    attr_reader :value

    def initialize(value)
        raise Exception.new('ユーザidが指定されていません') unless value

        @value = value
    end
end

class User
    attr_reader :user_id, :name

    def initialize(user_id, name)
        raise Exception.new('ユーザidが不正です') unless user_id.is_a(UserId)

        @user_id = user_id
        change_name(name)
    end

    # ユーザ名を変更するメソッド(パブリック)
    def change_name(name)
        raise Exception.new("ユーザ名が指定されていません") unless name
        raise Exception.new("ユーザ名は3文字以上入力してください") unless name.length < 3
        
        @name = name
    end

    # 比較メソッドのオーバライド
    def eql?(user_object)
        return false if user_object.nil? || !user_object.is_a(User)

        return user_id == user_object.user_id
    end
end

change_nameでユーザ名が変更されたとしても、識別子(UserId)が変更されていないので同一性が担保される。

エンティティと値オブジェクトどっちを選択するべきか

エンティティと値オブジェクトは共に、ドメインの概念を表現するオブジェクトとして似ている。

判断基準は、オブジェクトにライフサイクルが存在するかどうか。

ライフサイクルが存在する場合は、エンティティとして表現する。

ライフサイクルが存在しない場合(もしくは表現することが無意味な場合)は、値オブジェクトとして表現する。

例えば、Userという概念の場合、

  • システム開始時に利用者として作成される
  • システム利用中に、属性(メールアドレスやユーザ名)が変更されることもある
  • 利用者がシステムの利用をやめる時に、削除される

という感じで、ライフサイクル(生成〜削除)が存在する。

ドメインオブジェクトを定義するメリット

  • 値オブジェクトもエンティティもドメインオブジェクト

  • ドメインオブジェクトを定義するメリット

    • コードのドキュメント性が高くなる
      • ドメインオブジェクトにバリデーションのルールを組むと、属性がどういう値を持たないといけないのか明白になる
        • 文字数、フォーマット
      • DDDはドメインについて学び、ドメインオブジェクトを作り上げるところから始まる
    • 変更をコードに伝えやすくなる
      • ドメインの概念が変更(例えばパスワードを3文字以上から6文字以上に変更する)が入った際の変更範囲をドメインオブジェクトに限定できる
      • ドメインオブジェクトにルールや振る舞いを集約する

まとめ

  • エンティティも値オブジェクトもドメインモデル
  • エンティティはライフサイクルをもち、識別子(id)によって等価判定される
    • 属性は可変