iT邦幫忙

2021 iThome 鐵人賽

DAY 10
0
Software Development

在 Ruby on Rails 中導入 Domain-Driven Design 是不是搞錯了什麼?系列 第 10

[Day10] Boxenn 實作 Aggregate 和 Aggregate Root

原則

對於 domain 內的 aggregate,有以下的原則我們會遵守

  • 一個 domain 內只有一組 agrregate
  • 所有對資料的更動必須要整個物件一起透過 repository 處理
  • 需選擇其中一個 entity 作為 aggregate root ,上層要取用其他 entity 只能透過 aggregate root 的 repository
  • entity 可以有自己的業務邏輯
  • entity 的名字屬於共通語言,不只工程師要了解,領域專家也需要知道

範例 code

假設現在的 domain 是一個班級,一個班級底下會有一個老師和很多學生

設計出來的 aggregate 大致上會長這樣

# 班級 class.rb
class Class < Boxenn::Entity
  def self.primary_keys
    %i[grade name]
  end
  # 年級
  attribute :grade,                 Types::Coercible::Integer
  # 班級名字
  attribute :name,                  Types::String.enum('甲', '乙', '丙', '丁')
  # 老師
  attribute :teacher,               Entities::Teacher.optional.default(nil)
  # 學生
  attribute :students,              Types::Array.of(Entities::Student).default([].freeze)

  def class_people_count
    teacher.present? ? students.size + 1 : students.size
  end
end

# 老師 teacher.rb
class Teacher < Boxenn::Entity
  def self.primary_keys
    [:id_number]
  end
  # 身分證字號
  attribute :id_number,             Types::Coercible::String
  # 個人資料
  attribute :profile,               Entities::PersonProfile
end

# 學生 student.rb
class Student < Boxenn::Entity
  def self.primary_keys
    [:id_number]
  end
  # 身分證字號
  attribute :id_number,             Types::Coercible::String
  # 學生編號
  attribute :student_number,        Types::Coercible::String.optional.default(nil)
  # 個人資料
  attribute :profile,               Entities::PersonProfile
end

# 個人資訊 (value object) person_profile.rb
class PersonProfile < Dry::Struct
  # 名字
  attribute :name,                  Types::Coercible::String
  # 性別
  attribute :gender,                Types::Symbol.enum(:male, :female, :other).optional.default(nil)
  # 生日
  attribute :birthday,              Types::Date.optional.default(nil)

  def age
    today = Time.zone.today
    gap = today.year - birthday.year
    gap = gap - 1 if (
      birthday.month >  today.month or 
        (birthday.month >= today.month and birthday.day > today.day)
    )
    gap
  end
end

補充上一篇關於 dry-struct 的用法:

  1. dry-struct 新建的物件為 immutable,因此如果需要修改屬性必須使用 new 並重新賦值
profile = PersonProfile.new(name: 'Tom')
profile = profile.new(gender: :male)
  1. 在這之中有一個特例,如果型別為 Array ,那個屬性是 mutable 的,如果想讓所有屬性都保持 immutable,則需要給予 default 值並 freeze
 attribute :numbers, Types::Array.of(Types::Integer).default([].freeze)

小結

Aggregate 是我們在 DLL 中主要操作的物件,因此它的設計會直接影響到 code base 的依賴關係。
下一篇會來介紹 Boxenn 是如何實作 repository pattern,並通用化到不同的外部介面。


上一篇
[DAY9] Boxenn 實作 Entity 與 Value Object
下一篇
[DAY11] Data Access Layer 設計概念
系列文
在 Ruby on Rails 中導入 Domain-Driven Design 是不是搞錯了什麼?30

尚未有邦友留言

立即登入留言