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

Video: https://www.youtube.com/watch?v=fANjY7Hn_ig

---

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 full-size slide

  2. palkan_tula
    palkan
    2
    guides.rubyonrails.org

    View full-size 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  7. palkan_tula
    palkan
    5
    Rails vs MVC
    Model
    View
    Controller

    View full-size slide

  8. palkan_tula
    palkan
    5
    Rails vs MVC
    Model
    View
    Controller

    View full-size slide

  9. palkan_tula
    palkan
    6
    Rails vs MVC
    View
    Controller
    Model

    View full-size slide

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

    View full-size slide

  11. palkan_tula
    palkan
    8
    MVC cake

    View full-size slide

  12. palkan_tula
    palkan
    Mature MVC cake
    8

    View full-size slide

  13. palkan_tula
    palkan
    9
    Beyond MVC cake

    View full-size slide

  14. palkan_tula
    palkan
    10
    github.com/palkan

    View full-size slide

  15. palkan_tula
    palkan
    11
    github.com/palkan

    View full-size slide

  16. evilmartians.com/events

    View full-size slide

  17. Layers on Rails

    View full-size slide

  18. palkan_tula
    palkan
    16
    Rails Way
    Request
    Response

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  22. palkan_tula
    palkan
    21
    Bad abstractions Good abstractions

    View full-size slide

  23. Abstractions
    on Rails
    Railroad at Murnau, Wassily Kandinsky

    View full-size slide

  24. 23
    Abstraction layer for Rails cake

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  28. palkan_tula
    palkan
    27
    Layered Architecture
    Presentation
    Application
    Domain
    Infrastructure

    View full-size slide

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

    View full-size slide

  30. palkan_tula
    palkan
    30
    Layered Architecture
    Presentation
    Application
    Domain
    Infrastructure

    View full-size slide

  31. palkan_tula
    palkan
    30
    Layered Architecture
    Presentation
    Application
    Domain
    Infrastructure

    View full-size slide

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

    View full-size slide

  33. 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 full-size slide

  34. 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 full-size slide

  35. 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 full-size slide

  36. 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 full-size slide

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

    View full-size slide

  38. 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 full-size slide

  39. palkan_tula
    palkan
    How to choose new abstractions?
    38

    View full-size slide

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

    View full-size slide

  41. 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 full-size slide

  42. Extraction
    Time

    View full-size slide

  43. 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 full-size slide

  44. 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 full-size slide

  45. 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 full-size slide

  46. 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 full-size slide

  47. 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 full-size slide

  48. 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 full-size slide

  49. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  55. 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 full-size slide

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

    View full-size slide

  57. 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 full-size slide

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

    View full-size slide

  59. 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 full-size slide

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

    View full-size slide

  61. 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 full-size slide

  62. 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 full-size slide

  63. 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 full-size slide

  64. 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 full-size slide

  65. 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 full-size slide

  66. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  70. 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 full-size slide

  71. 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 full-size slide

  72. 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 full-size slide

  73. 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 full-size slide

  74. 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 full-size slide

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

    View full-size slide

  76. 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 full-size slide

  77. 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 full-size slide

  78. 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 full-size slide

  79. 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 full-size slide

  80. 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 full-size slide

  81. 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 full-size slide

  82. 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 full-size slide

  83. 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 full-size slide

  84. 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 full-size slide

  85. 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 full-size slide

  86. 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 full-size slide

  87. 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 full-size slide

  88. 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 full-size slide

  89. 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 full-size slide

  90. 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 full-size slide

  91. 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 full-size slide

  92. 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 full-size slide

  93. palkan_tula
    palkan
    How many layers is enough?
    80

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  96. The Book
    Coming Oct 2023

    View full-size slide

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

    View full-size slide