[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,并通用化到不同的外部介面。


<<:  Day 11 : PHP - 如何将HTML的内容传送到PHP?POST和GET又该如何选择?

>>:  与不同阶段的使用者对话

Day 20-state inspection-更改 state 有其风险,State manipulation 有赚有赔,更改前应详阅官方文件说明书之二

更改 state 有其风险,State manipulation 有赚有赔,更改前应详阅官方文件说明...

【从零开始的Swift开发心路历程-Day18】简易订单系统Part2

昨天我们已经成功建立资料库了,今天要做的是将资料存进资料库并且让TableView能马上更新资料库里...

Day11 HTML 切版介绍(下)

以下将介绍几个实用的切版学习资源与工具 e Odin Project(完整规划和复合式学习) 这是一...

30天学会C语言: Day 27-指标当参数

函式没办法使用其他函式中的变数 #include <stdio.h> #include ...