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

[RailsConf 2023] Rails as a piece of cake

[RailsConf 2023] Rails as a piece of cake

Ruby on Rails as a framework follows the Model-View-Controller design pattern. Three core elements, like the number of layers in a traditional birthday cake, are enough to “cook” web applications. However, on the long haul, the Rails cake often resembles a crumble cake with the layers smeared and crumb-bugs all around the kitchen-codebase.

Similarly to birthday cakes, adding new layers is easier to do and maintain as the application grows than increasing the existing layers in size.

How to extract from or add new layers to a Rails application? What considerations should be taken into account? Why is rainbow cake the king of layered cakes? Join my talk to learn about the layering Rails approach to keep applications healthy and maintainable.

Vladimir Dementyev

April 24, 2023
Tweet

More Decks by Vladimir Dementyev

Other Decks in Programming

Transcript

  1. Vladimir Dementyev
    Evil Martians
    RAILS AS A PIECE
    OF BIRTHDAY CAKE

    View Slide

  2. palkan_tula
    palkan
    2
    guides.rubyonrails.org

    View Slide

  3. palkan_tula
    palkan
    –Application Programming in
    Smalltalk-80 (TM), Steve Burbeck, 1987
    “In the MVC paradigm the user input, the
    modeling of the external world, and the visual
    feedback to the user are explicitly separated
    and handled by three types of object, each
    specialized for its task.”
    3

    View Slide

  4. The model class
    concerns itself only
    with the application's
    state and logic

    View Slide

  5. The view class
    concerns itself only
    with creating the user
    interface

    View Slide

  6. The controller class is
    occupied solely with
    translating user input
    into updates that it
    passes to the model

    View Slide

  7. palkan_tula
    palkan
    5
    Rails vs MVC
    Model
    View
    Controller

    View Slide

  8. palkan_tula
    palkan
    5
    Rails vs MVC
    Model
    View
    Controller

    View Slide

  9. palkan_tula
    palkan
    6
    Rails vs MVC
    View
    Controller
    Model

    View Slide

  10. palkan_tula
    palkan
    6
    Rails vs MVC
    View
    Controller
    Model
    Un-separation of
    concerns

    View Slide

  11. 7

    View Slide

  12. palkan_tula
    palkan
    8
    MVC cake

    View Slide

  13. palkan_tula
    palkan
    Mature MVC cake
    8

    View Slide

  14. palkan_tula
    palkan
    9
    Beyond MVC cake

    View Slide

  15. palkan_tula
    palkan
    10
    github.com/palkan

    View Slide

  16. palkan_tula
    palkan
    11
    github.com/palkan

    View Slide

  17. 12

    View Slide

  18. 13

    View Slide

  19. evilmartians.com/events

    View Slide

  20. Layers on Rails

    View Slide

  21. palkan_tula
    palkan
    16
    Rails Way
    Request
    Response

    View Slide

  22. palkan_tula
    palkan
    17
    Rails Way
    Model
    Controller View
    Request
    Response

    View Slide

  23. palkan_tula
    palkan
    18
    Extended Rails Way
    Model
    Controller View
    ?
    Request
    Response

    View Slide

  24. palkan_tula
    palkan
    19
    Maintainability
    Readability
    Testability
    Coupling
    Cohesion
    Extensibility
    Flexibility Complexity
    Reusability

    View Slide

  25. 20

    View Slide

  26. palkan_tula
    palkan
    21
    Bad abstractions Good abstractions

    View Slide

  27. Abstractions
    on Rails
    Railroad at Murnau, Wassily Kandinsky

    View Slide

  28. 23
    Abstraction layer for Rails cake

    View Slide

  29. palkan_tula
    palkan
    24
    Abstraction
    Generalization
    Encapsulation
    Loose Coupling
    Testability Centralization
    Simplification Single Responsibility
    Reusability

    View Slide

  30. palkan_tula
    palkan
    25
    Rails Abstraction
    Generalization
    Encapsulation
    Loose Coupling
    Testability Centralization
    Simplification
    Single Responsibility
    Reusability
    Conventions

    View Slide

  31. 26
    Abstraction layer for Rails cake
    1. Rails conventions Learn how Rails work, re-use
    patterns and building blocks.

    View Slide

  32. palkan_tula
    palkan
    27
    Layered Architecture
    Presentation
    Application
    Domain
    Infrastructure

    View Slide

  33. palkan_tula
    palkan
    28
    Layered Architecture
    Presentation
    Application
    Domain
    Infrastructure
    !

    View Slide

  34. 29

    View Slide

  35. palkan_tula
    palkan
    30
    Layered Architecture
    Presentation
    Application
    Domain
    Infrastructure

    View Slide

  36. palkan_tula
    palkan
    30
    Layered Architecture
    Presentation
    Application
    Domain
    Infrastructure

    View Slide

  37. Controllers
    Presentation
    Channels
    Views
    Application Jobs
    Mailers
    Domain
    Infrastructure
    Models
    Adapters (DB, mail)
    API clients

    View Slide

  38. palkan_tula
    palkan
    32
    class Authenticator
    def call(request)
    auth_header = request.headers["Authorization"]
    raise "Missing auth header" unless auth_header
    token = auth_header.split(" ").last
    raise "No token found" unless token
    JWT.decode(
    token,
    Rails.application.secrets.secret_key_base
    ).then do
    User.find(_1["user_id"]) if _1["user_id"]
    end
    end
    end
    Layer: ?

    View Slide

  39. palkan_tula
    palkan
    33
    class Authenticator
    def call(request)
    auth_header = request.headers["Authorization"]
    raise "Missing auth header" unless auth_header
    token = auth_header.split(" ").last
    raise "No token found" unless token
    JWT.decode(
    token,
    Rails.application.secrets.secret_key_base
    ).then do
    User.find(_1["user_id"]) if _1["user_id"]
    end
    end
    end
    Presentation
    Layer: Presentation

    View Slide

  40. palkan_tula
    palkan
    34
    class Authenticator
    def call(request)
    auth_header = request.headers["Authorization"]
    raise "Missing auth header" unless auth_header
    token = auth_header.split(" ").last
    raise "No token found" unless token
    JWT.decode(
    token,
    Rails.application.secrets.secret_key_base
    ).then do
    User.find(_1["user_id"]) if _1["user_id"]
    end
    end
    end
    Presentation
    Infrastructure
    Layer: Presentation ???

    View Slide

  41. palkan_tula
    palkan
    35
    class Authenticator
    def call(token)
    raise "No token found" unless token
    JWT.decode(
    token,
    Rails.application.secrets.secret_key_base
    ).then do
    User.find(_1["user_id"]) if _1["user_id"]
    end
    end
    end
    Layer: Application

    View Slide

  42. palkan_tula
    palkan
    An object belongs to the highest architecture
    layer among all its inputs and dependencies
    36

    View Slide

  43. 37
    Abstraction layer for Rails cake
    1. Rails conventions Learn how Rails work, re-use
    patterns and building blocks.
    2. Layered architecture ideas Identify the target arch. layer;
    avoid mixing too many layers.

    View Slide

  44. palkan_tula
    palkan
    How to choose new abstractions?
    38

    View Slide

  45. palkan_tula
    palkan
    How to choose new extract abstractions?
    39

    View Slide

  46. 40
    Abstraction layer for Rails cake
    1. Rails conventions Learn how Rails work, re-use
    patterns and building blocks.
    2. Layered architecture ideas Identify the target arch. layer;
    avoid mixing too many layers.
    3. Сodebase extracts, no
    artificial concepts
    Perform complexity analysis,
    analyze and extract abstractions.

    View Slide

  47. Extraction
    Time

    View Slide

  48. class GithooksController < ApplicationController
    def create
    event = JSON.parse(request.raw_post, symbolize_names: true)
    login = event.dig(:issue, :user, :login) ||
    event.dig(:pull_request, :user, :login)
    User.find_by(gh_id: login)
    &.handle_github_event(event) if login
    head :ok
    end
    end
    42
    Case #1: Models vs. webhooks

    View Slide

  49. class User < ApplicationRecord
    def handle_github_event(event)
    case event
    in type: "issue", action: "opened",
    issue: {user: {login:}, title:, body:}
    issues.create!(title:, body:)
    in type: "pull_request", action: "opened",
    pull_request: {
    user: {login:}, base: {label:}, title:, body:
    }
    pull_requests.create!(title:, body:, branch:)
    end
    end
    end
    43

    View Slide

  50. class User < ApplicationRecord
    def handle_github_event(event)
    case event
    in type: "issue", action: "opened",
    issue: {user: {login:}, title:, body:}
    issues.create!(title:, body:)
    in type: "pull_request", action: "opened",
    pull_request: {
    user: {login:}, base: {label:}, title:, body:
    }
    pull_requests.create!(title:, body:, branch:)
    end
    end
    end
    Hash originated in
    the outer world
    43

    View Slide

  51. class GitHubEvent
    def self.parse(raw_event)
    parsed = JSON.parse(raw_event, symbolize_names: true)
    case parsed[:type]
    when "issue"
    Issue.new(
    # ...
    )
    when "pull_request"
    PR.new(
    # ...
    )
    end
    rescue JSON::ParserError
    nil
    end
    Issue = Data.define(:user_id, :action, :title, :body)
    PR = Data.define(:user_id, :action, :title, :body, :branch)
    end
    44

    View Slide

  52. class GithooksController < ApplicationController
    def create
    event = GitHubEvent.parse(request.raw_post)
    User.find_by(gh_id: event.user_id)
    &.handle_github_event(event) if event
    head :ok
    end
    end
    45

    View Slide

  53. class User < ApplicationRecord
    def handle_github_event(event)
    case event
    in GitHubEvent::Issue[action: "opened", title:, body:]
    issues.create!(title:, body:)
    in GitHubEvent::PR[
    action: "opened", title:, body:, branch:
    ]
    pull_requests.create!(title:, body:, branch:)
    end
    end
    end
    46

    View Slide

  54. palkan_tula
    palkan
    Maintainability "
    — Controller: not ad-hoc hacks, less churn
    — Model: no knowledge of the outer world
    — Webhook payload access is encapsulated
    and localized
    47

    View Slide

  55. palkan_tula
    palkan
    Should a model be responsible for handling
    webhooks at all?
    48

    View Slide

  56. 49

    View Slide

  57. Controllers
    Presentation
    Channels
    Views
    Application Jobs
    Mailers
    Domain
    Infrastructure
    Models
    Adapters (DB, mail)
    API clients

    View Slide

  58. Controllers
    Presentation
    Channels
    Views
    Application Jobs
    Mailers
    Domain
    Infrastructure
    Models
    Adapters (DB, mail)
    Service Objects

    View Slide

  59. palkan_tula
    palkan
    Service Objects
    — Pseudo abstraction layer (generalization,
    consistency)
    51

    View Slide

  60. palkan_tula
    palkan
    Service Objects
    — Pseudo abstraction layer (generalization,
    consistency)
    — "app/services"—bag of random objects
    51

    View Slide

  61. palkan_tula
    palkan
    Service Objects
    — Pseudo abstraction layer (generalization,
    consistency)
    — "app/services"—bag of random objects
    —Intermediate stage until the final
    abstraction emerges
    51

    View Slide

  62. palkan_tula
    palkan
    52
    Service objects ~ waiting room
    sms_sender.rb
    rss_service.rb
    remind_user.rb
    auth_service.rb post/publish.rb

    View Slide

  63. palkan_tula
    palkan
    To "app/services" or not to
    "app/services"?
    — Don't start early with abstractions →
    better generalization requires a bit of
    aging
    — Don't overcrowd "app/services"
    53

    View Slide

  64. class GithooksController < ApplicationController
    def create
    event = GitHubEvent.parse(request.raw_post)
    GithubEventHandler.call(event) if event
    head :ok
    end
    end
    54

    View Slide

  65. class GithubEventHandler
    def self.call(event)
    user = User.find_by(gh_id: event.user_id)
    return false unless user
    case event
    in GitHubEvent::Issue[action: "opened", title:, body:]
    user.issues.create!(title:, body:)
    in GitHubEvent::PR[
    action: "opened", title:, body:, branch:
    ]
    user.pull_requests.create!(title:, body:, branch:)
    else
    # ignore unknown events
    end
    true
    end
    end
    55

    View Slide

  66. 56

    View Slide

  67. palkan_tula
    palkan
    57
    Case #2: Models vs. forms

    View Slide

  68. 58
    class User < ApplicationRecord
    attribute :should_send_invitation, :boolean
    after_commit :send_invitation, if: :should_send_invitation,
    on: :create
    def send_invitation
    UserMailer.invite(self).deliver_later
    end
    end

    View Slide

  69. 59
    class User < ApplicationRecord
    attribute :should_send_invitation, :boolean
    after_commit :send_invitation, if: :should_send_invitation,
    on: :create
    def send_invitation
    UserMailer.invite(self).deliver_later
    end
    end
    Layered Arch: Sending emails from models? Is it even legal #

    View Slide

  70. 60
    class User < ApplicationRecord
    attribute :should_send_invitation, :boolean
    after_commit :send_invitation, if: :should_send_invitation,
    on: :create
    def send_invitation
    UserMailer.invite(self).deliver_later
    end
    end
    Layered Arch: Sending emails from models? Is it even legal #
    Context-specific information now a part of the domain model

    View Slide

  71. 61
    class User < ApplicationRecord
    attribute :should_send_invitation, :boolean
    after_commit :send_invitation, if: :should_send_invitation,
    on: :create
    def send_invitation
    UserMailer.invite(self).deliver_later
    end
    end
    >= Application Layer

    View Slide

  72. class InvitationsController < ApplicationController
    def create
    @user = User.new(params.require(:user).permit(:email))
    @user.should_send_invitation = true
    if @user.save
    if params[:send_copy] == "1"
    UserMailer.invite_copy(current_user, @user)
    .deliver_later
    end
    redirect_to root_path, notice: "Invited!"
    else
    render :new
    end
    end
    end
    62

    View Slide

  73. class InvitationsController < ApplicationController
    def create
    @user = User.new(params.require(:user).permit(:email))
    @user.should_send_invitation = true
    if @user.save
    if params[:send_copy] == "1"
    UserMailer.invite_copy(current_user, @user)
    .deliver_later
    end
    redirect_to root_path, notice: "Invited!"
    else
    render :new
    end
    end
    end
    63
    Leaking abstraction $

    View Slide

  74. palkan_tula
    palkan
    Where can we localize
    the invitation form logic?
    64

    View Slide

  75. Jobs
    Service Objects
    Application
    Mailers
    Domain
    Infrastructure
    Models
    DB / API / etc
    Controllers
    Presentation
    Channels
    Views

    View Slide

  76. Jobs
    Service Objects
    Application
    Mailers
    Domain Models
    Form objects
    Controllers
    Presentation
    Channels
    Views

    View Slide

  77. class UserInvitationForm
    attr_reader :user, :send_copy, :sender
    def initialize(email, send_copy: false, sender: nil)
    @user = User.new(email:)
    @send_copy = send_copy.in?(%w[1 t true])
    @sender = sender
    end
    def save
    return false unless user.valid?
    user.save!
    deliver_notifications!
    true
    end
    def deliver_notifications!
    UserMailer.invite(user).deliver_later
    if send_copy
    UserMailer.invite_copy(sender, user).deliver_later
    end
    end
    end

    View Slide

  78. class UserInvitationForm
    attr_reader :user, :send_copy, :sender
    def initialize(email, send_copy: false, sender: nil)
    @user = User.new(email:)
    @send_copy = send_copy.in?(%w[1 t true])
    @sender = sender
    end
    def save
    return false unless user.valid?
    user.save!
    deliver_notifications!
    true
    end
    def deliver_notifications!
    UserMailer.invite(user).deliver_later
    if send_copy
    UserMailer.invite_copy(sender, user).deliver_later
    end
    end
    end
    Manual type-casting

    View Slide

  79. palkan_tula
    palkan
    class InvitationsController < ApplicationController
    def create
    form = UserInvitationForm.new(
    params.require(:user).permit(:email)[:email],
    params[:send_copy],
    current_user
    )
    if form.save
    redirect_to root_path, notice: "Invited!"
    else
    @user = form.user
    render :new
    end
    end
    end
    68

    View Slide

  80. class InvitationsController < ApplicationController
    def create
    form = UserInvitationForm.new(
    params.require(:user).permit(:email)[:email],
    params[:send_copy],
    current_user
    )
    if form.save
    redirect_to root_path, notice: "Invited!"
    else
    @user = form.user
    render :new
    end
    end
    end
    69
    Two sets of params
    Hack to re-use templates +
    leaking internals

    View Slide

  81. class InvitationsController < ApplicationController
    def create
    form = UserInvitationForm.new(
    params.require(:user).permit(:email)[:email],
    params[:send_copy],
    current_user
    )
    if form.save
    redirect_to root_path, notice: "Invited!"
    else
    @user = form.user
    render :new
    end
    end
    end
    70
    Two sets of params
    Hack to re-use templates +
    leaking internals

    View Slide

  82. palkan_tula
    palkan
    — Type casting, validations
    — Trigger side actions on successful
    submission
    — Compatibility with the view layer
    71
    Form object → Rails abstraction

    View Slide

  83. palkan_tula
    palkan
    Form object → Rails abstraction
    — Type casting, validations → ActiveModel::API
    + ActiveModel::Attributes
    — Trigger side actions on successful
    submission → ActiveSupport::Callbacks
    — Compatibility with the view layer →
    ActiveModel::Name + conventions
    72

    View Slide

  84. class InvitationForm < ApplicationForm
    attribute :email
    attribute :send_copy, :boolean
    attr_accessor :sender
    validates :email, presence: true
    after_commit :deliver_invitation
    after_commit :deliver_invitation_copy, if: :send_copy
    def submit!
    @user = User.new(email:)
    @user.save!
    end
    def deliver_invitation
    UserMailer.invite(@user).deliver_later
    end
    def deliver_invitation_copy
    UserMailer.invite_copy(sender, @user).deliver_later if sender
    end
    end

    View Slide

  85. class InvitationForm < ApplicationForm
    attribute :email
    attribute :send_copy, :boolean
    attr_accessor :sender
    validates :email, presence: true
    after_commit :deliver_invitation
    after_commit :deliver_invitation_copy, if: :send_copy
    def submit!
    @user = User.new(email:)
    @user.save!
    end
    def deliver_invitation
    UserMailer.invite(@user).deliver_later
    end
    def deliver_invitation_copy
    UserMailer.invite_copy(sender, @user).deliver_later if sender
    end
    end
    Form fields (w/types)

    View Slide

  86. class InvitationForm < ApplicationForm
    attribute :email
    attribute :send_copy, :boolean
    attr_accessor :sender
    validates :email, presence: true
    after_commit :deliver_invitation
    after_commit :deliver_invitation_copy, if: :send_copy
    def submit!
    @user = User.new(email:)
    @user.save!
    end
    def deliver_invitation
    UserMailer.invite(@user).deliver_later
    end
    def deliver_invitation_copy
    UserMailer.invite_copy(sender, @user).deliver_later if sender
    end
    end
    Form fields (w/types)
    Core logic

    View Slide

  87. class InvitationForm < ApplicationForm
    attribute :email
    attribute :send_copy, :boolean
    attr_accessor :sender
    validates :email, presence: true
    after_commit :deliver_invitation
    after_commit :deliver_invitation_copy, if: :send_copy
    def submit!
    @user = User.new(email:)
    @user.save!
    end
    def deliver_invitation
    UserMailer.invite(@user).deliver_later
    end
    def deliver_invitation_copy
    UserMailer.invite_copy(sender, @user).deliver_later if sender
    end
    end
    Form fields (w/types)
    Core logic
    Trigger actions

    View Slide

  88. palkan_tula
    palkan
    74
    class InvitationsController < ApplicationController
    def new
    @invitation_form = InvitationForm.new
    end
    def create
    @invitation_form = InvitationForm.new(
    params.require(:invitation).permit(:email, :send_copy)
    )
    @invitation_form.sender = current_user
    if @invitation_form.save
    redirect_to root_path
    else
    render :new, status: :unprocessable_entity
    end
    end
    end

    View Slide

  89. palkan_tula
    palkan
    75
    <%= form_for(@invitation_form) do |form| %>
    <%= form.label :email %>
    <%= form.text_field :email %>
    <%= form.label :send_copy, "Send me the copy" %>
    <%= form.check_box :send_copy %>
    <%= form.submit "Invite" %>
    <% end %>

    View Slide

  90. class ApplicationForm
    include ActiveModel::API
    include ActiveModel::Attributes
    define_callbacks :save, only: :after
    define_callbacks :commit, only: :after
    def save
    return false unless valid?
    with_transaction do
    AfterCommitEverywhere.after_commit { run_callbacks(:commit) }
    run_callbacks(:save) { submit! }
    end
    end
    def model_name
    ActiveModel::Name.new(nil, nil, self.class.name.sub(/Form$/, ""))
    end
    private
    def with_transaction(&) = ApplicationRecord.transaction(&)
    def submit!
    raise NotImplementedError
    end
    end

    View Slide

  91. class ApplicationForm
    include ActiveModel::API
    include ActiveModel::Attributes
    define_callbacks :save, only: :after
    define_callbacks :commit, only: :after
    def save
    return false unless valid?
    with_transaction do
    AfterCommitEverywhere.after_commit { run_callbacks(:commit) }
    run_callbacks(:save) { submit! }
    end
    end
    def model_name
    ActiveModel::Name.new(nil, nil, self.class.name.sub(/Form$/, ""))
    end
    private
    def with_transaction(&) = ApplicationRecord.transaction(&)
    def submit!
    raise NotImplementedError
    end
    end
    Types and validations

    View Slide

  92. class ApplicationForm
    include ActiveModel::API
    include ActiveModel::Attributes
    define_callbacks :save, only: :after
    define_callbacks :commit, only: :after
    def save
    return false unless valid?
    with_transaction do
    AfterCommitEverywhere.after_commit { run_callbacks(:commit) }
    run_callbacks(:save) { submit! }
    end
    end
    def model_name
    ActiveModel::Name.new(nil, nil, self.class.name.sub(/Form$/, ""))
    end
    private
    def with_transaction(&) = ApplicationRecord.transaction(&)
    def submit!
    raise NotImplementedError
    end
    end
    Types and validations
    Callbacks

    View Slide

  93. class ApplicationForm
    include ActiveModel::API
    include ActiveModel::Attributes
    define_callbacks :save, only: :after
    define_callbacks :commit, only: :after
    def save
    return false unless valid?
    with_transaction do
    AfterCommitEverywhere.after_commit { run_callbacks(:commit) }
    run_callbacks(:save) { submit! }
    end
    end
    def model_name
    ActiveModel::Name.new(nil, nil, self.class.name.sub(/Form$/, ""))
    end
    private
    def with_transaction(&) = ApplicationRecord.transaction(&)
    def submit!
    raise NotImplementedError
    end
    end
    Types and validations
    Callbacks
    Transaction-awareness

    View Slide

  94. class ApplicationForm
    include ActiveModel::API
    include ActiveModel::Attributes
    define_callbacks :save, only: :after
    define_callbacks :commit, only: :after
    def save
    return false unless valid?
    with_transaction do
    AfterCommitEverywhere.after_commit { run_callbacks(:commit) }
    run_callbacks(:save) { submit! }
    end
    end
    def model_name
    ActiveModel::Name.new(nil, nil, self.class.name.sub(/Form$/, ""))
    end
    private
    def with_transaction(&) = ApplicationRecord.transaction(&)
    def submit!
    raise NotImplementedError
    end
    end
    Types and validations
    Callbacks
    Transaction-awareness
    Action View compatibility
    (InvitationForm → /invitations)

    View Slide

  95. class ApplicationForm
    include ActiveModel::API
    include ActiveModel::Attributes
    define_callbacks :save, only: :after
    define_callbacks :commit, only: :after
    def save
    return false unless valid?
    with_transaction do
    AfterCommitEverywhere.after_commit { run_callbacks(:commit) }
    run_callbacks(:save) { submit! }
    end
    end
    def model_name
    ActiveModel::Name.new(nil, nil, self.class.name.sub(/Form$/, ""))
    end
    private
    def with_transaction(&) = ApplicationRecord.transaction(&)
    def submit!
    raise NotImplementedError
    end
    end
    Types and validations
    Callbacks
    Transaction-awareness
    Action View compatibility
    (InvitationForm → /invitations)
    Interface

    View Slide

  96. class ApplicationForm
    include ActiveModel::API
    include ActiveModel::Attributes
    define_callbacks :save, only: :after
    define_callbacks :commit, only: :after
    def save
    return false unless valid?
    with_transaction do
    AfterCommitEverywhere.after_commit { run_callbacks(:commit) }
    run_callbacks(:save) { submit! }
    end
    end
    def model_name
    ActiveModel::Name.new(nil, nil, self.class.name.sub(/Form$/, ""))
    end
    private
    def with_transaction(&) = ApplicationRecord.transaction(&)
    def submit!
    raise NotImplementedError
    end
    end
    Types and validations
    Callbacks
    Transaction-awareness
    Action View compatibility
    Interface

    View Slide

  97. palkan_tula
    palkan
    — Type casting, validations ✅
    — Trigger side actions on successful submission ✅
    — Compatibility with the view layer ✅
    — Strong parameters compatibility
    — DX (test matchers, generators)
    78
    Form object → Rails abstraction

    View Slide

  98. 79
    Abstraction layer for Rails cake
    1. Rails conventions Learn how Rails work, re-use
    patterns and building blocks.
    2. Layered architecture ideas Identify the target arch. layer;
    avoid mixing too many layers.
    3. Сodebase extracts, no
    artificial concepts
    Perform complexity analysis,
    analyze and extract abstractions.

    View Slide

  99. 79
    Abstraction layer for Rails cake
    1. Rails conventions Learn how Rails work, re-use
    patterns and building blocks.
    2. Layered architecture ideas Identify the target arch. layer;
    avoid mixing too many layers.
    3. Сodebase extracts, no
    artificial concepts
    Perform complexity analysis,
    analyze and extract abstractions.
    Feel free to experiment and add ingredients from other
    paradigms and ecosystems!

    View Slide

  100. palkan_tula
    palkan
    How many layers is enough?
    80

    View Slide

  101. Controllers
    Channels
    Presentation
    Views
    Application
    Jobs
    Presenters
    Form objects
    Filter objects
    Deliveries
    Authorization Policies
    Event Listeners
    Interactors

    View Slide

  102. Mailers
    Domain
    Infrastructure
    Models
    Adapters (DB, mail)
    API clients
    Deliveries
    Notifiers
    Interactors
    Query objects
    Configuration objects
    Value objects
    Service objects

    View Slide

  103. 84

    View Slide

  104. The Book
    Coming Oct 2023

    View Slide

  105. @palkan
    @palkan_tula
    evilmartians.com
    @evilmartians
    Thanks!

    View Slide