建議搭配之前的 sequence diagram 一起服用!
在進到 Record Mapper 和 Factory 之前,先來介紹一下 dry-initializer,他替我們省略掉 constructor (在 ruby 中是 initialize
)中賦值給實例變數(instance variable)的工作,在 spec 中這樣的特性讓我們可以簡單的替換掉跟外部依賴的 class。
require 'dry/initializer'
class User
extend Dry::Initializer
param :name, default: proc { nil }
option :age, default: proc { nil }
end
user = User.new 'Tom', age: 23
user.name # => 'Tom'
user.age # => 23
我們放在 initialize 的參數都是屬於工具或模組化的 class,通常在 spec 中都會把這些 class 抽換掉,而 method 傳入的才是那個 method 有用到的參數。
Record Mapper的職責是把 aggregate
的 hash
轉成對應 db schema key 的 hash
,選擇轉成 hash
的原因是 hash
是 ruby 的 base type,如此一來便可以讓 mapper 通用化到不同的外部資源(ORM、Database、其他 gem 等等)
class Order < Boxenn::Repositories::Mapper
def build(hash)
# key 是對應到 DB schema columns name,而 value 則是 entity 的 attibutes name
{
custom_serial_number: hash[:serial_number],
status: hash[:status],
created_at: hash[:puchased_at],
comment: hash[:comment],
}
end
end
Factory 的職責跟 Record Mapper 剛好恰恰相反,是把 source
轉成 aggregate
,而在Boxenn::Repositories::Factory
中定義的介面也是 build,除此之外還需要使用 dry-initializer 指定 entity。
這邊的 Factory 不是在 DDD 或 clean architecture 中的 factory,單純只是一個轉換器,名字是為了與 record mapper 作區別。
class Order < Boxenn::Repositories::Factory
# 指定 entity 讓 factory 和 repository 使用
param :entity, default: -> { Entities::Order }
def build(source_object)
entity.new(
serial_number: source_object.custom_serial_number,
status: source_object.status,
puchased_at: source_object.created_at,
comment: source_object.comment,
)
end
end
下一篇會進一步擴充 record mapper,使每次在建立基礎設施時更加快速。