Upgrade to Pro — share decks privately, control downloads, hide ads and more …

MOPCON 2022 - 從 Domain-Driven Design 看網站開發框架隱藏

MOPCON 2022 - 從 Domain-Driven Design 看網站開發框架隱藏

Domain-Driven Design 在近年是經常被討論的一種系統設計的方式,在學習思考方式的過程中,意外的發現在實作上有許多地方可以解釋我們在使用網站開發框架時無法清晰解釋的細節。

以 Ruby on Rails 經常被濫用的 Service Object 為例子,從 Domain-Driven Design 角度的思考,實際上就是 Domain Service 的角色,該在怎樣的時機使用、該封裝怎樣的內容馬上就變得明確。

讓我們試著以 Domain-Driven Design 的戰術(程式實作)來分析網站開發框架將哪些細節簡化使我們得以容易入門,又開如何重新細化來對應不斷發展成更複雜的系統。

蒼時弦や

October 15, 2022
Tweet

More Decks by 蒼時弦や

Other Decks in Programming

Transcript

  1. What we talk about Overview The Layers The case study

    Why Domain-Driven Design Why Domain-Driven Design
  2. What we talk about Overview The Layers The Layers The

    case study Why Domain-Driven Design
  3. What we talk about Overview The Layers The case study

    The case study Why Domain-Driven Design
  4. Application Layer The Layers Input User Flow Order.create! Payment.create! OrderMailer.confirmation

    Input User Flow Order.find_or_create_by! Payment.create! OrderMailer.confirmation Idempotent
  5. Input Object Case Study # Ruby on Rails validates :email,

    :password, presence: true validates :email, format: /\A[^@\s]+@[^@\s]+\z/ class RegisterForm include ActiveModel::API include ActiveModel::Validation attribute :email, :string attribute :password, :string end
  6. Input Object Case Study # Ruby on Rails validates :email,

    :password, presence: true validates :email, format: /\A[^@\s]+@[^@\s]+\z/ class RegisterForm include ActiveModel::API include ActiveModel::Validation attribute :email, :string attribute :password, :string end validate :email_uniqueness
  7. Input Object Case Study // NestJS class RegisterDto { email:

    string; password: string; } @IsEmail() @IsNotEmpty() @Post() @UsePipes(new ValidationPipe({ transform: true })) async create(@Body() registerDto: RegisterDto) { this.registrationService.create(registerDto); }
  8. Input Object Case Study // NestJS @UsePipes(new ValidationPipe({ transform: true

    })) class RegisterDto { @IsEmail() email: string; @IsNotEmpty() password: string; } @Post() async create(@Body() registerDto: RegisterDto) { this.registrationService.create(registerDto); }
  9. Input Object Case Study // Pseudo Code function dispatch(request: HttpRequest)

    { controller, action = this.router.lookup(request.path) input = meta.inputs[action].build(request.params) controller[action].call(input) } meta = Reflect.getMeta(controller) validator = meta.validator[action] if (validator) { validator.validate(input) }
  10. Error Handling Case Study # Ruby on Rails rescue PaymentGateWayUnavailable

    render :new class OrderController < ApplicationController def create @order = Order.create!(params) @payment = Payment.create!(order: @order) OrderMailer.confirmation(@order) end end
  11. Error Handling Case Study # Ruby on Rails rescue PaymentGateWayUnavailable

    render :new class OrderController < ApplicationController def create @order = Order.create!(params) @payment = Payment.create!(order: @order) OrderMailer.confirmation(@order) end end rescue SomeErrorA # ... rescue SomeErrorB # ...
  12. Error Handling Case Study # Ruby on Rails rescue_from ActiveRecord::ActiveRecordError,

    with: :record_error rescue_from PaymentService::PaymentError, with: :payment_error class OrderController < ApplicationController def create @order = Order.create!(params) @payment = Payment.create!(order: @order) OrderMailer.confirmation(@order) end end
  13. Error Handling Case Study # Ruby on Rails @form =

    CreaetOrderForm.new(params) def payment_error(exc) @form.errors.add(:base, exc.message) render :new end class OrderController < ApplicationController rescue_from ActiveRecord::ActiveRecordError, with: :record_error rescue_from PaymentService::PaymentError, with: :payment_error def create # ... end end
  14. Error Handling Case Study # Ruby on Rails module WithPaymentFlow

    # ... included do rescue_from PaymentService::PaymentError, with: :payment_flow_error end def payment_flow_error(exc) @form.errors.add(:base, exc.message) end end # response_with(@form)
  15. Side Effect Case Study # Ruby on Rails class ShippingService

    # ... def refresh_shipping_state!(order, ticket) end end order.update( shipping_state: ticket.shipping_state, # ... )
  16. Side Effect Case Study # Ruby on Rails class Merchant::ShippingController

    < ApplicationController # ... def update ticket = ShippingTicket.from_order!(@order) shipping_service.refresh_shipping_state!(@order, ticket) # ... @order.save! end end tracking_service.attach_event!(@order) #=> TrackingUnavailableError
  17. Side Effect Case Study # Ruby on Rails class ShippingService

    # ... end def refresh_shipping_state(order, ticket) order.assign_attributes( shipping_state: ticket.shipping_state, # ... ) end
  18. Side Effect Case Study # Ruby on Rails class Merchant::ShippingController

    < ApplicationController # ... def update # User Flow ticket = ShippingTicket.from_order!(@order) shipping_service.refresh_shipping_state(@order, ticket) tracking_service.attach_event!(@order) # Commit @order.save! end end # Input # ...
  19. Side Effect Case Study # Ruby on Rails class Merchant::ShippingController

    < ApplicationController # ... def update # Input # ... # Commit @order.save! end end # User Flow ticket = ShippingTicket.from_order!(@order) shipping_service.refresh_shipping_state(@order, ticket) tracking_service.attach_event!(@order)
  20. Side Effect Case Study # Ruby on Rails class Merchant::ShippingController

    < ApplicationController # ... def update # Input # ... # User Flow ticket = ShippingTicket.from_order!(@order) shipping_service.refresh_shipping_state(@order, ticket) tracking_service.attach_event!(@order) end end # Commit @order.save!
  21. Value Object Case Study # Ruby on Rails class Wallets::DepositController

    < ApplicationController def create # ... @wallet.save! end end @wallet.deposit(@form[‘amount’], @form[‘currency’])
  22. Value Object Case Study # Ruby on Rails class Wallet

    < ApplicationRecord # ... def deposit(amount, currency) self.balance += amount end end raise CurrencyMismatch unless self.currency == currency
  23. Value Object Case Study # Ruby on Rails class Wallet

    < ApplicationRecord # ... def deposit(amount) end end sig {params(amount: Money).returns(Money)} self.balance += amount
  24. Value Object Case Study # Ruby on Rails class Money

    # ... end def +(other) raise ArgumentError, ‘...’ unless other.currency == currency Money.new(amount + other.amount, currency) end raise ArgumentError, ‘...’ unless other.is_a?(Money)
  25. Value Object Case Study # Ruby on Rails class Wallet

    < ApplicationRecord # ... sig {params(amount: Money).returns(Money)} def deposit(amount) end end composed_of :balance, class_name: ‘Money’, mapping: [%i[balance amount], %i[currency currency]] self.balance += amount
  26. Value Object Case Study # Ruby on Rails class Wallets::DepositController

    < ApplicationController def create # ... @wallet.save! end end @wallet.deposit(@form.amount)
  27. Value Object Case Study # Ruby on Rails class Wallets::DepositController

    < ApplicationController def create # ... @wallet.save! end end @wallet.balance += @form.amount