管理者がセール情報を登録する | ドメイン駆動設計ハンズオン
Prev: ユーザーが商品を検索する
フィーチャーツリーとユースケース
フィーチャーツリーは以下の通りでした。
- 決済機能
- ユーザー情報変更
- 商品カタログ
- (実装済み)商品の検索
- (実装済み)商品管理
- 注文
- ショッピングカート
- 注文受付機能
- 配送状態確認
ユースケースは以下の通りです。
- (実装済み)管理者が商品を登録する
- (実装済み)ユーザーが商品を検索する
- ユーザーがショッピングカートに商品を追加する
- ユーザーがショッピングカートの中身を確認する
割り込みの機能追加
ここで、プロダクトオーナーが1つ機能を忘れていたと言い出しました(そういうことにしてください)。「このECサイトではセールを行うことがあり、セールの情報を登録できる必要がある」とのことで、商品カタロググループにセール管理機能を追加し、ユースケースに「管理者がセールを登録する」を追加するとのことでした。新しいフィーチャーツリーとユースケース配下の通りです。
- 決済機能
- ユーザー情報変更
- 商品カタログ
- (実装済み)商品の検索
- (実装済み)商品管理
- セール管理
- 注文
- ショッピングカート
- 注文受付機能
- 配送状態確認
ユースケースは以下の通り。
- (実装済み)管理者が商品を登録する
- (実装済み)ユーザーが商品を検索する
- 管理者がセールを登録する
- ユーザーがショッピングカートに商品を追加する
- ユーザーがショッピングカートの中身を確認する
機能要求としては
- セールの開始日と終了日が指定できる
- セールの種類は以下の2種類のいずれか
- 10%OFFのような一律の割引
- 5000円以上の商品500円OFFのような条件付き値引き
- 値引き後価格が0円以下にならないようにする
- セール期間が重複した場合は値引き金額が一番大きいセールだけが適用される
- 商品一覧に表示する価格はセール適用前と適用後の両方を併記する
というものでした。
商品一覧の価格の適用はともかくとして、まずは「管理者がセールを登録する」を実装していきましょう。
データモデリング
セールは「10%OFFのような一律の割引」というパーセント割引、「5000円以上の商品500円OFFのような条件付き値引き」という固定額値引が排他で登録されます。データモデルもこれに習って、パーセント割引と固定額値引で分けることにしましょう。
Aggregateの実装
ER図の通りに実装します。
# セール Aggregateルート
class ProductCatalog::SaleManagement::Sale
attr_reader :from, :to, :percentage_discount, :fixed_amount_discount
def initialize(from, to, percentage: nil,
target_price: nil, fixed_amount: nil)
@from = from
@to = to
if percentage.present?
@percentage_discount =
ProductCatalog::SaleManagement::PercentageDiscount.new(percentage)
return
end
@fixed_amount_discount =
ProductCatalog::SaleManagement::FixedAmountDiscount.new(
target_price, fixed_amount
)
end
end
# パーセント割引
class ProductCatalog::SaleManagement::PercentageDiscount
attr_reader :percentage
def initialize(percentage)
@percentage = percentage
end
end
# 固定額値引
class ProductCatalog::SaleManagement::FixedAmountDiscount
attr_reader :target_price, :fixed_amount
def initialize(target_price, fixed_amount)
@target_price = target_price
@fixed_amount = fixed_amount
end
end
ここでバリデーションについて考えます。固定額値引の場合は「値引き後価格が0円以下にならないようにする」という機能要求があるのでこのバリデーションが必要になります。問題はこのバリデーションをどこで実装して、どうチェックするかです。やり方はいろいろ考えられますが、ここではバリデーションの実行に必要な情報を持つEntityでバリデーションを実行し、親のEntityで子供のEntityのバリデーションメソッドの結果を集約するという方法を取ります。
# セール Aggregateルート
class ProductCatalog::SaleManagement::Sale
attr_reader :from, :to, :percentage_discount, :fixed_amount_discount
# パーセント割引も固定額値引も指定されていないケースを考慮していないが
# ハンズオンなので省略
def initialize(from, to, percentage: nil,
target_price: nil, fixed_amount: nil)
@from = from
@to = to
if percentage.present?
@percentage_discount =
ProductCatalog::SaleManagement::PercentageDiscount.new(percentage)
return
end
@fixed_amount_discount =
ProductCatalog::SaleManagement::FixedAmountDiscount.new(
target_price, fixed_amount
)
end
def valid?
return @percentage_discount.valid? if @percentage_discount.present?
@fixed_amount_discount.valid?
end
end
# パーセント割引
class ProductCatalog::SaleManagement::PercentageDiscount
attr_reader :percentage
def initialize(percentage)
@percentage = percentage
end
# パーセンテージの指定がされているかというチェックもできるが
# ハンズオンなので省略
def valid?
true
end
end
# 固定額値引
class ProductCatalog::SaleManagement::FixedAmountDiscount
attr_reader :target_price, :fixed_amount
def initialize(target_price, fixed_amount)
@target_price = target_price
@fixed_amount = fixed_amount
end
def valid?
return false if @target_price <= @fixed_amount
true
end
end
Repositoryの実装
Repositoryの実装は特に難しいところはありません。単にAggregateからテーブルに詰め替えるだけです。ただ、テーブルは1つのテーブルにパーセント割引と固定額値引の区別なく入れることにします(※特に理由はありません。テーブル設計は本題ではないしハンズオンなのでいい加減に決めています)。
class ProductCatalog::SaleManagement::SaleRepository
# 引数は ProductCatalog::SaleManagement::Sale のインスタンス
# percentage_discount&.percentage などの&.は
# percentage_discountがnil(null)だった場合に
# percentageメソッドを呼び出さずにnilを返すメソッド呼び出し方法
def save(sale)
percentage_discount = sale.percentage_discount
fixed_amount_discount = sale.fixed_amount_discount
sale = Sale.new(from: sale.from,
to: sale.to,
percentage: percentage_discount&.percentage,
target_price: fixed_amount_discount&.target_price,
fixed_amount: fixed_amount_discount&.fixed_amount)
sale.save!
end
end
ApplicationServiceの実装
ApplicationServiceの実装も特に難しいところはありません。「管理者がセールを登録する」ユースケースなのでAdministratorRegisterSale
とします。
# 管理者がセールを登録するユースケース
class AdministratorRegisterSale
def perform(from, to, percentage: nil,
target_price: nil, fixed_amount: nil)
sale = ProductCatalog::SaleManagement::Sale.new(
from, to, percentage: percentage,
target_price: target_price, fixed_amount: fixed_amount
)
return false if !sale.valid?
repository = ProductCatalog::SaleManagement::SaleRepository.new
result = false
ActiveRecord::Base.transaction do
repository.save(sale)
result = true
end
result
end
end
Next: 「ユーザーが商品を検索する」を再モデリング