Movatterモバイル変換


[0]ホーム

URL:


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

[RailsConf 2023] Rails as a piece of cake

Avatar for Vladimir Dementyev Vladimir Dementyev
April 24, 2023

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

Avatar for Vladimir Dementyev

Vladimir Dementyev

April 24, 2023
Tweet

More Decks by Vladimir Dementyev

See All by Vladimir Dementyev

Other Decks in Programming

See All in Programming

Featured

See All Featured
Side Projects
455
42k
The Myth of the Modular Monolith - Day 2 Keynote - Rails World 2024
26
2.9k
Git: the NoSQL Database
430
65k
Music & Morning Musume
46
6.7k
Responsive Adventures: Dirty Tricks From The Dark Corners of Front-End
251
21k

Transcript

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

    CAKE
  2. palkan_tula palkan 2 guides.rubyonrails.org

  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
  4. The model class concerns itself only with the application's state

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

    interface
  6. The controller class is occupied solely with translating user input

    into updates that it passes to the model
  7. palkan_tula palkan 5 Rails vs MVC Model View Controller

  8. palkan_tula palkan 5 Rails vs MVC Model View Controller

  9. palkan_tula palkan 6 Rails vs MVC View Controller Model

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

    of concerns
  11. 7

  12. palkan_tula palkan 8 MVC cake

  13. palkan_tula palkan Mature MVC cake 8

  14. palkan_tula palkan 9 Beyond MVC cake

  15. palkan_tula palkan 10 github.com/palkan

  16. palkan_tula palkan 11 github.com/palkan

  17. 12

  18. 13

  19. evilmartians.com/events

  20. Layers on Rails

  21. palkan_tula palkan 16 Rails Way Request Response

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

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

    Request Response
  24. palkan_tula palkan 19 Maintainability Readability Testability Coupling Cohesion Extensibility Flexibility

    Complexity Reusability
  25. 20

  26. palkan_tula palkan 21 Bad abstractions Good abstractions

  27. Abstractions on Rails Railroad at Murnau, Wassily Kandinsky

  28. 23 Abstraction layer for Rails cake

  29. palkan_tula palkan 24 Abstraction Generalization Encapsulation Loose Coupling Testability Centralization

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

    Centralization Simplification Single Responsibility Reusability Conventions
  31. 26 Abstraction layer for Rails cake 1. Rails conventions Learn

    how Rails work, re-use patterns and building blocks.
  32. palkan_tula palkan 27 Layered Architecture Presentation Application Domain Infrastructure

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

  34. 29

  35. palkan_tula palkan 30 Layered Architecture Presentation Application Domain Infrastructure

  36. palkan_tula palkan 30 Layered Architecture Presentation Application Domain Infrastructure

  37. Controllers Presentation Channels Views Application Jobs Mailers Domain Infrastructure Models

    Adapters (DB, mail) API clients
  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: ?
  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
  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 ???
  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
  42. palkan_tula palkan An object belongs to the highest architecture layer

    among all its inputs and dependencies 36
  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.
  44. palkan_tula palkan How to choose new abstractions? 38

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

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

  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
  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
  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
  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
  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
  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
  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
  55. palkan_tula palkan Should a model be responsible for handling webhooks

    at all? 48
  56. 49

  57. Controllers Presentation Channels Views Application Jobs Mailers Domain Infrastructure Models

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

    Adapters (DB, mail) Service Objects
  59. palkan_tula palkan Service Objects — Pseudo abstraction layer (generalization, consistency)

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

    — "app/services"—bag of random objects 51
  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
  62. palkan_tula palkan 52 Service objects ~ waiting room sms_sender.rb rss_service.rb

    remind_user.rb auth_service.rb post/publish.rb
  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
  64. class GithooksController < ApplicationController def create event = GitHubEvent.parse(request.raw_post) GithubEventHandler.call(event)

    if event head :ok end end 54
  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
  66. 56

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

  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
  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 #
  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
  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
  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
  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 $
  74. palkan_tula palkan Where can we localize the invitation form logic?

    64
  75. Jobs Service Objects Application Mailers Domain Infrastructure Models DB /

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

    Presentation Channels Views
  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
  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
  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
  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
  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
  82. palkan_tula palkan — Type casting, validations — Trigger side actions

    on successful submission — Compatibility with the view layer 71 Form object → Rails abstraction
  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
  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
  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)
  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
  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
  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
  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 %> <form action="/invitations" method="post">
  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
  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
  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
  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
  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)
  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
  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
  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
  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.
  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!
  100. palkan_tula palkan How many layers is enough? 80

  101. Controllers Channels Presentation Views Application Jobs Presenters Form objects Filter

    objects Deliveries Authorization Policies Event Listeners Interactors
  102. Mailers Domain Infrastructure Models Adapters (DB, mail) API clients Deliveries

    Notifiers Interactors Query objects Configuration objects Value objects Service objects
  103. 84

  104. The Book Coming Oct 2023

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


[8]ページ先頭

©2009-2025 Movatter.jp