Slide 1

Slide 1 text

Callbacks core_ext Concerns Current Page refreshes Helpers @vars in views Seven deadly Rails anti-patterns Sin City Ruby 2024 Vladimir Dementyev Evil Martians ?

Slide 2

Slide 2 text

palkan_tula palkan 2 Rails anti-patterns Software design patterns and ideas employed and/or encouraged by the framework that make developers hellishly productive (but come with a risk of poor maintainability in the long term) the framework productive poor maintainability Deal with the Devil

Slide 3

Slide 3 text

3

Slide 4

Slide 4 text

palkan_tula palkan Rails Way 4

Slide 5

Slide 5 text

palkan_tula palkan 5 github.com/palkan

Slide 6

Slide 6 text

palkan_tula palkan 6 evilmartians.com

Slide 7

Slide 7 text

palkan_tula palkan 7

Slide 8

Slide 8 text

8

Slide 9

Slide 9 text

+ palkan_tula palkan 9 Callbacks core_ext Concerns Current Page refreshes Helpers @vars in views

Slide 10

Slide 10 text

+ palkan_tula palkan 10 Callbacks core_ext Concerns Current Page refreshes Helpers @vars in views

Slide 11

Slide 11 text

+ palkan_tula palkan 11 Disclaimer: I'm actively using (almost) all seven Confession

Slide 12

Slide 12 text

Callbacks

Slide 13

Slide 13 text

+ palkan_tula palkan X

Slide 14

Slide 14 text

14

Slide 15

Slide 15 text

“The deeper callbacks lie in the architecture layer hierarchy, the more dangerous they can be” –Me @ Layered Rails 15

Slide 16

Slide 16 text

palkan_tula palkan Callbacks 16 Channels Jobs Mailers Models Controllers ⚠

Slide 17

Slide 17 text

palkan_tula palkan 17 class User < ApplicationRecord end Maintainability

Slide 18

Slide 18 text

palkan_tula palkan 18 class User < ApplicationRecord before_validation :normalize_email end Maintainability

Slide 19

Slide 19 text

palkan_tula palkan 19 class User < ApplicationRecord before_validation :normalize_email after_create_commit :send_welcome_email end Maintainability

Slide 20

Slide 20 text

palkan_tula palkan 20 class User < ApplicationRecord before_validation :normalize_email after_create :generate_initial_project after_create_commit :send_welcome_email end Maintainability

Slide 21

Slide 21 text

palkan_tula palkan 21 class User < ApplicationRecord before_validation :slugify, on: :create before_validation :normalize_email after_create :generate_initial_project after_create_commit :send_welcome_email end Maintainability

Slide 22

Slide 22 text

palkan_tula palkan 22 class User < ApplicationRecord before_validation :slugify, on: :create before_validation :normalize_email after_create :generate_initial_project, after_create_commit :send_welcome_email after_create_commit :send_analytics_event after_commit :sync_with_crm end Maintainability

Slide 23

Slide 23 text

palkan_tula palkan 23 class User < ApplicationRecord before_validation :slugify, on: :create before_validation :normalize_email after_create :generate_initial_project, after_create_commit :send_welcome_email after_create_commit :send_analytics_event, if: :tracking_consent? after_commit :sync_with_crm end Maintainability

Slide 24

Slide 24 text

palkan_tula palkan class User < ApplicationRecord before_validation :slugify, on: :create before_validation :normalize_email after_create :generate_initial_project, unless: :admin? after_create_commit :send_welcome_email after_create_commit :send_analytics_event, if: :tracking_consent? after_commit :sync_with_crm end 24 Maintainability

Slide 25

Slide 25 text

+ palkan_tula palkan 25 (De-)Normalization Essential side-effects (audit, etc.) Conditional Peripheral side-effects (third-party integrations, etc.) Model Callbacks

Slide 26

Slide 26 text

Concerns

Slide 27

Slide 27 text

“A concern is a module that you extract to split the implementation of a class in coherent chunks, instead of having one big class body.” –Xavier Noria 27 coherent chunks

Slide 28

Slide 28 text

palkan_tula palkan 28 class Account < ApplicationRecord include Account::Associations include Account::Validations include Account::Scopes include Account::Callbacks end Cohesion Maintainability

Slide 29

Slide 29 text

palkan_tula palkan Coherent Chunks 29 class Identity < ApplicationRecord include Accessor, Authenticable, Boxes, Clearance, Preapproval, Clipper, Creator, Eventable, Examiner, Filer, Member, PasswordReset, Poster, Reaching, Regional, TopicMerger end God-blessed concerns Maintainability

Slide 30

Slide 30 text

+ palkan_tula palkan 30 No isolation (potential hidden/circular dependencies) Naming is hard (modules, methods) Complicated testing Many Modules Maintainability

Slide 31

Slide 31 text

+ palkan_tula palkan Many 32 No isolation (potential hidden/circular dependencies) Naming is hard (modules, methods) Complicated testing Many callbacks Concerns Maintainability

Slide 32

Slide 32 text

Many Objects Maintainability

Slide 33

Slide 33 text

+ palkan_tula palkan 34 Value objects Workflows (state machines) Associated models Store models Extracting Prefer objects over concerns... gem "active_record-associated_object" gem "store_model"

Slide 34

Slide 34 text

35 codewithjason.com

Slide 35

Slide 35 text

+ palkan_tula palkan 36 Associations Active Record extensions (e.g., soft deletion) Gluing associated objects and host models the Rails Way Concerning ...but remember to stay productive

Slide 36

Slide 36 text

palkan_tula palkan 37 module Account::Terminable def terminate purge_or_incinerate if terminable? end private def purge_or_incinerate purgeable? ? purge : incinerate end # def purge # def incinerate # ... end

Slide 37

Slide 37 text

palkan_tula palkan 38 class Account::Terminator def initialize(account) = @account = account def perform purge_or_incinerate if terminable? end private def purge_or_incinerate purgeable? ? purge : incinerate end # def purge # def incinerate # ... end

Slide 38

Slide 38 text

palkan_tula palkan 39 account.terminate #=> Rails-ish! Account::Terminator.new(account) .perform #=> Boooring...

Slide 39

Slide 39 text

palkan_tula palkan 40 class Account::Terminator def initialize(account) = @account = account def perform purge_or_incinerate if terminable? end private def purge_or_incinerate purgeable? ? purge : incinerate end # def purge # def incinerate # ... end

Slide 40

Slide 40 text

palkan_tula palkan 41 class Account class Terminator # ... end module Terminable def terminate = Terminator.new(self).perform end end

Slide 41

Slide 41 text

+ palkan_tula palkan 42 Concerns to sweep complexity under the rug Concerns to provide Rails-ish interface and to integrate with Active Record Objects to truly isolate functionality Concerns

Slide 42

Slide 42 text

Helpers

Slide 43

Slide 43 text

+ palkan_tula palkan 44 Bags of random methods Form a single module shared by all views Helpers # Did you know? config.action_controller.include_all_helpers = false

Slide 44

Slide 44 text

Current

Slide 45

Slide 45 text

46 thevaluable.dev

Slide 46

Slide 46 text

+ palkan_tula palkan 47 Keep the number of current attributes small Always use Current.set(..., &) Write as upper on the abstraction stack as possible Current Don't let currents take you under

Slide 47

Slide 47 text

@vars in views

Slide 48

Slide 48 text

49 thevaluable.dev

Slide 49

Slide 49 text

+ palkan_tula palkan 50 Instance variables in controller action templates are fine (show.html, etc.) Instance variables in partials Strict locals in partials Become friends with erb-lint @vars in views Be strict with them (but not too strict)

Slide 50

Slide 50 text

core_ext

Slide 51

Slide 51 text

52

Slide 52

Slide 52 text

+ palkan_tula palkan X

Slide 53

Slide 53 text

+ palkan_tula palkan 54 Learn them, not fight Adding your own lib/core_ext!* core_ext * Or start using refinements

Slide 54

Slide 54 text

Page refreshes

Slide 55

Slide 55 text

palkan_tula palkan Page refreshes 56 <%# locals: (conversations:, total_count:) -%> <%= turbo_stream.update "counter" do %> <%= render partial: "counter", locals: {total_count:} %> <% end %> <%= turbo_stream.append "items" do %> <%= render partial: "items", locals: {current_conversation: Current.conversation, conversations:} %> <% end %> <%= turbo_stream.update "scroll" do %> <%= render partial: "scroll", locals: {id: :conversations, scroll_url: conversation_action_path(operation: :scroll, page: next_page)} %> <% end %> <%= turbo_stream.update "list" do %> <%= render partial: "list", locals: {conversations:, total_count:, current_conversation: Current.conversation} %> <% end %>

Slide 56

Slide 56 text

palkan_tula palkan Page refreshes 57 broadcast_refresh_to(conversation)

Slide 57

Slide 57 text

+ palkan_tula palkan Rails 58 GEt /converstation/2024

Slide 58

Slide 58 text

+ palkan_tula palkan Rails 62

Slide 59

Slide 59 text

+ palkan_tula palkan 65 class Conversation < ApplicationRecord # Triggers page refresh on EVERY # record update broadcasts_refreshes end Deadly Combo Callbacks meet page refreshes

Slide 60

Slide 60 text

+ palkan_tula palkan 66 class Conversation < ApplicationRecord after_commit :broadcast_refresh_later, if: :should_broadcast? private def should_broadcast? # check changed attributes, # throttle, etc. end end Deadly Combo Be specific, ship Union Pacific selective updates

Slide 61

Slide 61 text

67

Slide 62

Slide 62 text

+ palkan_tula palkan 68 <%= cable_ready_updates_for conversation, only: %w[topic members_count], debounce: 200 do %> <% # ... %> <% end %> Cable Ready? Is Hotwire really that hot?

Slide 63

Slide 63 text

+ palkan_tula palkan 69 Beware of multi-user updates Beware of frequent updates SolidCache is not an accident Page refreshes

Slide 64

Slide 64 text

+ palkan_tula palkan 70 Callbacks Concerns Helpers Core extensions The Seven Caution! Sharp knives, use responsibly Current @vars in views Page refreshes

Slide 65

Slide 65 text

71 dev.37signals.com/series/code-i-like/

Slide 66

Slide 66 text

–Me @ Sin City Ruby 2024 “Hell is full of Rails applications trying to go against the Rails Way.” 72

Slide 67

Slide 67 text

Slides: evilmartians.com/events Twitter: @palkan_tula, @evilmartians Sin City Ruby 2024 Don't go to Hell