對於 domain 內的 aggregate,有以下的原則我們會遵守
假設現在的 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 的用法:
profile = PersonProfile.new(name: 'Tom')
profile = profile.new(gender: :male)
Array
,那個屬性是 mutable 的,如果想讓所有屬性都保持 immutable,則需要給予 default 值並 freeze attribute :numbers, Types::Array.of(Types::Integer).default([].freeze)
Aggregate 是我們在 DLL 中主要操作的物件,因此它的設計會直接影響到 code base 的依賴關係。
下一篇會來介紹 Boxenn 是如何實作 repository pattern,並通用化到不同的外部介面。