# 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 ```