# Domain-Driven Design
## Domain Model ~= Businss Model aka Business Logic
| Type | State | Method |
|------|-------|--------|
| Entity/Aggregate/Value | Yes | Yes |
| Service | No | Yes |
* Aggregate -> ID (Unique)
* Boundary ~= Module -(組成)--> System
* Aggreate ~= Moudle ===> 找 Aggregate Root == 找邊界
* State
* Order -> sub-total ...
* OrderItems -> [OrderItem<id: 1>]
* Method
* Change State
* Change Aggregated Entity State -> `order.update_amount(product_id, amount)`
* `items.select { |i| i.pid == product_id }.update_amount(amount)`
* When Save, all aggregate entities are saved
* `has_many :items autosave: true`
* Entity -> ID (Unique)
* State
* OrderItem -> unit_price ...
* Method
* `update_amount() => self.amount = amount`
* Value Object -> No ID -> Not Unique
* Immutable
* Money -> Composite([100, TWD])
* Service Object
* Order 想 update_amount 時,商品數量必須足夠 <== 條件
* `OrderItemAmountUpdater` ->
* Service 協調
* `prodsuct.stock_available?(amount)`
* `order.update_amount(product.id, amount)`
```ruby=
# Product (Context)
# * Context - Store Manager
# * Context - Customer
class Money
attr_reader :value, :currency
def initialize(value, currency)
@value = value
@currency = currency
end
def +(other)
# check currency
new(value + other.value, currency)
end
end
module Store
class Product
belongs_to :store
compose_of :price, class_name: 'Money', mapping: [%i(price value), %i(currency currency)]
end
end
class Order
# ---> product.xxxx
# product_id (Value != Association) --> Link
# product_name
# product_unit_price
end
```
## Flow
-> View
-> Controller -- User Flow
-> A Case -> Reseller -> No stock check
-> B Case -> Customer -> Check stock
-> Model -- Business Logic
```ruby=
class CheckoutController
def create
# Step 1
cart.items.each do |item|
# ... check
item.product_id
end
# Step 2
Order.create_from(cart)
# 拆 Service Object
end
end
# ...
class CheckoutController
rescue_from ...., :on_product_not_available
# ....
def create
# Step 1
ProductAvailableService.new.call(cart)
# raise Error
# Step 2
@order = BuildOrderService.new.call(cart)
@order.save!
# render
end
end
class Reseller::CheckoutController
rescue_from ...., :on_product_not_available
# ....
def create
# Step 1
# ProductAvailableService.new.call(cart)
# raise Error
# Step 2
@order = BuildOrderService.new.call(cart)
@order.save!
# render
end
end
```
```ruby=
# Service Object -> No State
# --> Multiple Steps
# --> Cross Domain Interaction
def create
# Product Domain
@products = Product.where(id: @cart.items.pluck(:product_id))
# ...
# --> ProductAvailableService
verify_product_available!
# A Scenario
# ...
@order = Order.new(user: @cart.user)
@order = BuildOrderService.call(@order, @cart.item)
@order.save!
end
def verify_product_available!
cart_items.each do |item|
product = products.find { |p| p.pid = item.product_id] }
verify_data = {
today: Date.today,
amount: item.amount,
# ...
}
ProductAvailableService.call(product, verify_data)
ProductProvideService.call(product, verify_data)
# ...
end
end
class ProductProvideService
def call(product, today)
raise NotProvidedError unless product.provide?(today)
end
end
class ProductAvailabelService
def call(product, item)
raise NotAvailableError unless product.available?(item.amount)
end
end
class BuildOrderService
def call(order, items)
items.each do |item|
order.add_item(convert_to_order_item(item))
end
end
private
def convert_to_order_item(item)
{
amount: item.amount,
price: item.amount * item.unit_price
}
end
end
```
```ruby=
# Service Object -> No State
def create
CreditCard.new.perform
rescue CreditInsuffientError => e
res = HandleCreditInsuffientService.new.perform
# ...
logger.log(res.internal_error)
render json: { reason: res.message }
end
def logger
Rails.logger.tag(...)
end
```
1. feature
migration
feature code
2. feature code
3. until migration
4. unlock feature code
5.
```ruby=
class Item
self.ignore_columns = []
end
def update
attrs = {
# ...
notes: ...
}
# notes
@item.update(attributes)
end
```