$30 off During Our Annual Pro Sale. View Details »

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. 蒼時弦也
    看網站開發框架

    隠藏的細節
    從Domain-Driven

    Design

    View Slide

  2. CONSULTANT × SPEAKER
    蒼時弦也
    YouTube Coding Live Blog https://blog.aotoki.me Discord Discuss about Ruby
    Introduce About Me

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  6. Opportunity Why Domain-Driven Design
    The Opportunity
    When I try to improve the code base

    View Slide

  7. Framework and Design Why Domain-Driven Design
    Design
    Architecture
    Framework

    View Slide

  8. Escape Why Domain-Driven Design
    Framework
    Ruby / PHP
    Framework
    Node.js / Golang
    Replace

    View Slide

  9. Modeling Why Domain-Driven Design
    The Modeling
    From data to information

    View Slide

  10. Modeling Why Domain-Driven Design
    Data State Action
    Modeling Change
    Object

    View Slide

  11. Layered Architecture The Layers
    Layered Architecture
    Define the role of your "Class"

    View Slide

  12. Presentation Layer The Layers
    Presentation Layer
    Transform between "data" and "object"

    View Slide

  13. Presentation Layer The Layers
    Object
    @product.name
    Data
    Product
    Render
    Output (View)
    Rails View

    View Slide

  14. Presentation Layer The Layers
    Object
    @product.name
    Data
    {"name": "Product"}
    Serialize
    Output (View)
    Rails View

    View Slide

  15. Presentation Layer The Layers
    Data
    {"name":"Product"}
    Object
    params[:name]
    Deserialize
    Input (Param)
    Rails Controller

    View Slide

  16. Application Layer The Layers
    Application Layer
    Define the flow of "action"

    View Slide

  17. Application Layer The Layers
    Input
    User Flow
    Order.create!
    Payment.create!
    OrderMailer.confirmation

    View Slide

  18. Application Layer The Layers
    Input
    User Flow
    Order.create!
    Payment.create!
    OrderMailer.confirmation
    Exception
    PaymentGatewayUnavailable
    Raise

    View Slide

  19. 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

    View Slide

  20. Domain Layer The Layers
    Domain Layer
    The "logic" related business

    View Slide

  21. Domain Layer The Layers
    Period
    begin=2022-09-01, end=2022-10-31
    Call in?
    2022-09-30
    Return true
    Value Object

    View Slide

  22. Domain Layer The Layers
    Data State Action
    Modeling Change
    Entity

    View Slide

  23. Domain Layer The Layers
    ShippingService#create_ticket!(order)
    if response.success?
    True
    order.shipping_id = response.id
    False
    raise ShippingProviderUnavailable
    Service Object

    View Slide

  24. Infrastructure Layer The Layers
    Infrastructure Layer
    The way to interact with "data"

    View Slide

  25. Infrastructure Layer The Layers
    PostgresOrderRepository
    implement IOrderRepository
    Write Read
    Orders Table
    Repository

    View Slide

  26. Infrastructure Layer The Layers
    ShopifyOrderRepository
    implement IOrderRepository
    Call
    Shopify Order API
    Write Read
    Orders Table
    Repository

    View Slide

  27. Testing The Layers
    Add Tests
    The reasonable way to test

    View Slide

  28. Testing The Layers
    shipped.json.jbuilder
    ShippingController
    Order
    OrderRepository UserRepository
    Shipping
    ShippingRepository TicketRepository
    DHL::Client
    E2E Testing

    View Slide

  29. Testing The Layers
    shipped.json.jbuilder
    ShippingController
    Order
    OrderRepository UserRepository
    Shipping
    ShippingRepository TicketRepository
    DHL::Client
    E2E Testing

    View Slide

  30. Testing The Layers
    shipped.json.jbuilder
    ShippingController
    Order
    OrderRepository UserRepository
    Shipping
    ShippingRepository TicketRepository
    DHL::Client
    Unit Test

    View Slide

  31. Testing The Layers
    shipped.json.jbuilder
    ShippingController
    Order
    OrderRepository UserRepository
    Shipping
    ShippingRepository TicketRepository
    DHL::Client
    Unit Test

    View Slide

  32. Testing The Layers
    shipped.json.jbuilder
    ShippingController
    Order
    OrderRepository UserRepository
    Shipping
    ShippingRepository TicketRepository
    DHL::Client
    Workable Feature

    View Slide

  33. Input Object Case Study
    Input Object
    How to "validate" the inputs

    View Slide

  34. Input Object Case Study
    Input
    Profile
    @user.assign_attributes(params)
    @user.valid!
    @user.save!

    View Slide

  35. Input Object Case Study
    Input
    Register
    @user = User.new(params)
    @user.valid!
    @user.save!

    View Slide

  36. Input Object Case Study
    Input
    Register
    @form.valid!
    @user = User.new(@form.attributes)
    @user.save!

    View Slide

  37. 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

    View Slide

  38. 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

    View Slide

  39. 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);

    }

    View Slide

  40. 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);

    }

    View Slide

  41. Input Object Case Study
    Input
    Steps
    Parse Request
    Validate DTO
    Dispatch Controller

    View Slide

  42. 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)

    }

    View Slide

  43. Error Handling Case Study
    Error Handling
    Where to fix the error?

    View Slide

  44. Error Handling Case Study
    Input
    Order.create!
    Payment.create!
    OrderMailer.confirmation
    Exception
    PaymentGatewayUnavailable
    Raise

    View Slide

  45. 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

    View Slide

  46. 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

    # ...

    View Slide

  47. 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

    View Slide

  48. 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

    View Slide

  49. 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)

    View Slide

  50. Error Handling Case Study
    ActiveSupport::Rescuable
    ActionController ActionMailer ActionCable ActiveJob

    View Slide

  51. Side Effect Case Study
    Side Effect
    Who changes my data?

    View Slide

  52. Side Effect Case Study
    # Ruby on Rails

    class ShippingService

    # ...

    def refresh_shipping_state!(order, ticket)

    end

    end
    order.update(

    shipping_state: ticket.shipping_state,

    # ...

    )

    View Slide

  53. 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

    View Slide

  54. 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

    View Slide

  55. 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

    # ...


    View Slide

  56. 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)


    View Slide

  57. 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!

    View Slide

  58. Value Object Case Study
    Value Object
    Manage logic in the "Domain Model"

    View Slide

  59. Value Object Case Study
    # Ruby on Rails

    class Wallets::DepositController < ApplicationController

    def create

    # ...

    @wallet.save!

    end

    end
    @wallet.deposit(@form[‘amount’], @form[‘currency’])

    View Slide

  60. 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


    View Slide

  61. Value Object Case Study
    # Ruby on Rails

    class Wallet < ApplicationRecord

    # ...

    def deposit(amount)

    end

    end
    sig {params(amount: Money).returns(Money)}

    self.balance += amount

    View Slide

  62. 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)

    View Slide

  63. 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

    View Slide

  64. Value Object Case Study
    # Ruby on Rails

    class Wallets::DepositController < ApplicationController

    def create

    # ...

    @wallet.save!

    end

    end
    @wallet.deposit(@form.amount)

    View Slide

  65. Value Object Case Study
    # Ruby on Rails

    class Wallets::DepositController < ApplicationController

    def create

    # ...

    @wallet.save!

    end

    end
    @wallet.balance += @form.amount

    View Slide

  66. Survey Feedback
    Survey https://www.surveycake.com/s/lboZ9

    View Slide