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

[Kaigi on Rails 2024] Rails Way, or the highway

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.
Avatar for Vladimir Dementyev Vladimir Dementyev
October 25, 2024
28k

[Kaigi on Rails 2024] Rails Way, or the highway

Avatar for Vladimir Dementyev

Vladimir Dementyev

October 25, 2024

Transcript

  1. Agenda What's the Rails Way? Challenges & temptations How to

    not get lost 8 次の退屈な30分は英語です RAILS WAY ずは䜕ですか? 挑戊ず誘惑 迷⌊にならない✅法
  2. For many developers, the Rails Way is just the title

    of the book 倚くの開発者にずっお、 RAILS WAY は単なる本 のタむトルに過ぎない
  3. It took me years to realize the actual meaning goes

    far beyond code コヌドを超えた本圓の意味に気づくたで に䜕幎もかかった
  4. Rails Way is a methodology of building Rails applications 12

    RAILS WAYはRAILSアプリケヌションを構築する✅法論です
  5. Rails Way is a philosophy of building Rails applications 13

    RAILS WAYはRAILSアプリケヌションを構築する哲孊です
  6. Rails Way Optimize for productivity & happiness Freedom from decision

    making (Omakase) Design for scalability 14 ✣産性ず幞犏のための最適化 意思決定からの解攟おたかせ スケヌラビリティを考慮した蚭蚈 スケヌラビリティずは「必芁に応じお、必芁なリ゜ヌスで、必芁な速さで実⟏するこず」
  7. 15 Ruby MVC Convention over configuration Complexity compression Rails Way

    rails new 蚭定より芏玄 耇雑さの圧瞮
  8. 17 rails new 0 ∞ IPO 1 Rails 特に RAILS

    8 は、「HELLO WORLD」から最初の動䜜バヌゞョンたで にできるだけ早く着くこずに集䞭しおいる
  9. Rails 8 Authentication Solid infrastructure solid_queue/solid_cache/solid_cable Deployment Kamal 18 認蚌機胜

    SOLID なむンフラ基盀 デプロむメント
  10. Rails 8 Way Optimize for getting from zero to one

    19 「れロ」から「むチ」ぞの最適化
  11. 20 Rails Way rails new 0 IPO 1 ∞ 2

    3 4 5 ··· ? しかし、「むチ」ずIPOの間には無限のステップがある— そこでもRAILS WAYは私たちをカバヌしおくれるのだろうか
  12. Rails as a web app 25 Model Controller View Request

    Response そしおデフォルトでは、3぀の⌯皋がある 凊理はコントロヌラヌで始たり、モデルず通信し、ビュヌを䜿っおレスポンスを準備する
  13. Rails gives you a solid frame and building blocks to

    assemble your application Rails はアプリケヌションを組み✎おるための堅牢なフレヌムず 構成芁玠を提䟛する
  14. But Omakase is not complete, there are gaps to fill

    authorization, complex UI logic, workflows, AI agents しかし、「おたかせ」は完党ではなく、 埋めるべき隙間がある 認可、耇雑なUI、ワヌクフロヌ、AI゚ヌゞェント
  15. Filling the gaps, we often distort the frame our productivity

    & happiness decrease 隙間を埋めるこずで、私たちはしばしばフレヌムを歪めおしたいたす ✣産性ず幞犏床は䜎䞋する our codebase turns into a monster Ruby on Railsは、「怪獣 on Rails」になっおしたう
  16. 33 怪獣は、開発者がRAILS WAYを完党に理解しおいないか、 もしくは誀解するかこずで✣たれる... reddit.com/r/rails なぜ Ruby on Rails のコヌドベヌスは、それぞれがこんなにも異なるのでしょうか

    Rails の゚コシステムはある皋床たでは構造を提䟛したすが、 その先は各チヌムが独⟃の決定で進たなければなりたせん
  17. The guide on filling gaps the Rails Way RAILS WAY

    に埓っお隙間を埋める ためのガむド
  18. 41 gem "rails" # Database gem "sqlite3" # Real-time backend

    gem "solid_cable" # Background jobs gem "solid_queue" # Image transformations gem "image_processing" From 1...
  19. 42 gem "rails" # Database gem "pg" # Real-time backend

    gem "anycable-rails" # Background jobs gem "sidekiq" # Image transformations gem "imgproxy-rails" ...to N
  20. Master Rails Learn Rails design patterns adapter/plugin/middleware Embrace the principle

    of conventions Make Rails building blocks a part of your toolbox active_model/active_support/zeitwerk 43 RAILS を習埗する RAILS の蚭蚈パタヌンを孊ぶ 芏玄の原則を受け⌊れる RAILSの構成芁玠をあなたのツヌルボックスに⌊れる
  21. Extend Rails instead of melting it with something else 44

    RAILS を他のものず混ぜ合わせるのでは なく、拡匵しよう
  22. How to add a new abstraction and stay on the

    Rails Way 48 RAILSアプリケヌションに新しい抜象化レむダヌを導⌊する 際に、私が埓う4぀のルヌルだ
  23. I. Provide Rails-like DX and compatibility with core components 49

    RAILS らしい開発者䜓隓ずコアコンポヌネントずの互換性 を提䟛する
  24. Think as a framework author, not a custom application developer

    50 カスタムプリケヌション開発者の考え✅ではなく、フレヌム ワヌク䜜者の考え✅を持぀
  25. 56 Controllers Presentation Channels Views Application Jobs Mailers Domain Infrastructure

    Models Adapters (DB, mail) API clients それぞれのRAILSの抜象化を、特定のアヌキテクチャ局に察応付けれる。 私たちの独⟃の抜象化はも同じようにしよう。
  26. III. Prefer extraction over intervention: find existing abstractions in your

    code 57 介⌊より抜出 コヌド内で既存の抜象化を⟒぀けよう
  27. Separate vanilla Rails from advanced techniques Keep the learning curve

    smooth but leave the opportunity to gain new knowledge 叀兞的なRailsず⟌床なテクニックを分離する 孊習曲線をなめらかに保ち぀぀ 新しい知識を埗る機䌚を 残しおおく
  28. 60 class Post < ApplicationRecord has_many :comments, dependent: :destroy belongs_to

    :user validates :title, presence: true scope :tagged, ->(tag) { tags_table = Arel::Nodes::NamedFunction.new( "json_each", [arel_table[:tags]] ).then do name = Arel.sql(_1.to_sql) Arel::Table.new(name, as: :json_tags) end tags_subquery = arel_table. project(1). where(tags_table[:value].eq(tag)) where(tags_subquery.exists) } end Normal Rails 通 垞のRails Unseparated complexity 分離されおいない耇雑さ Paranormal Rails " 超垞的なRails
  29. 61 class Post < ApplicationRecord has_many :comments, dependent: :destroy belongs_to

    :user validates :title, presence: true scope :tagged, TaggedQuery end Separated complexity 分離された耇雑さ
  30. 62 class Post::TaggedQuery < ApplicationQuery def resolve(tag) tags_subquery = tags_table.project(1).

    where(tags_table[:value].eq(tag)) relation.where(tags_subquery.exists) end private def tags_table @tags_arel ||= Arel::Nodes::NamedFunction.new( "json_each", [arel_table[:tags]] ).then do name = Arel.sql(_1.to_sql) Arel::Table.new(name, as: :json_tags) end end def arel_table = self.class.query_model.arel_table end Separated complexity 分離された耇雑さ
  31. <%= form_for @cable do |f| %> <%= f.text_field :name, required:

    true %> <%= f.text_field :region, required: true %> <%= f.radio_button :framework, "rails" %> <%= f.radio_button :framework, "js" %> <%= f.radio_button :framework, "hotwire" %> <%= f.radio_button :framework, "default" %> <%= f.text_field :rpc_host %> <%= f.text_field :rpc_secret %> <%= f.text_field :secret %> <%= f.text_field :turbo_secret %> <%= f.text_field :jwt_secret %> <%= f.submit "Create" %> <% end %> What we had 私たちが持っおいたもの
  32. 66 class Cable < ApplicationRecord validates :name, presence: true validates

    :region, presence: true, if: :region_step_completed? attr_accessor :current_step def current_step @current_step ||= "name" end def steps = %w[name framework rpc secrets region] def next_step = steps[steps.index(current_step) + 1] def previous_step = steps[steps.index(current_step) - 1] def first_step? = current_step == steps.first def last_step? = current_step == steps.last def region_step_completed? = steps.index(current_step) > steps.index("region") end What we got 私たちが埗たもの This code belongs to Presentation layer このコヌドはプレれンテヌ ション局に属しおいる 7 new methods just for a single form! たった1぀のフォヌムのため に7぀の新しいメ゜ッド
  33. 66 class Cable < ApplicationRecord validates :name, presence: true validates

    :region, presence: true, if: :region_step_completed? attr_accessor :current_step def current_step @current_step ||= "name" end def steps = %w[name framework rpc secrets region] def next_step = steps[steps.index(current_step) + 1] def previous_step = steps[steps.index(current_step) - 1] def first_step? = current_step == steps.first def last_step? = current_step == steps.last def region_step_completed? = steps.index(current_step) > steps.index("region") end What we got 私たちが埗たもの Written by senior AI developer # AI先✣で䜜られたコヌド This code belongs to Presentation layer このコヌドはプレれンテヌ ション局に属しおいる 7 new methods just for a single form! たった1぀のフォヌムのため に7぀の新しいメ゜ッド
  34. class CablesController < ApplicationController def create @cable = Cable.new(cable_params) if

    @cable.valid? if @cable.last_step? @cable.save session.delete(:cable_params) redirect_to @cable, notice: "Success!" else session[:cable_params] = cable_params.to_h redirect_to new_cable_path end else render :new, status: :unprocessable_entity end end def back @cable = Cable.new(session[:cable_params]) @cable.current_step = @cable.previous_step render :new end end 67 What we got 私たちが埗たもの Written by senior AI developer # Ad hoc persistence (Infrastructure layer?) カスタムな氞続化の実装 むンフラ局 Not a Rails-like action Railsらしくないアクション AI先✣で䜜られたコヌド
  35. Rails doesn't provide multi-step functionality out-of-the-box The code we wrote

    to fill the gap doesn't look like Rails Crossing the architecture layer boundaries leads to high coupling and poor maintainability 68 RAILSは耇数ステップ機胜を暙準では提䟛しおくれない その隙間を埋めるために曞いたコヌドは、RAILSらしくない アヌキテクチャ局の越境は、密結合ず䜎いメンテナンス性に⟄る
  36. Form object concerns Context-specific validations User input transformation User feedback

    Custom UI-driven logic (wizards) 70 コンテキストによりのバリデヌション ナヌザヌ⌊⌒の倉換 ナヌザヌぞフィヌドバックを提䟛するこず UIに基づくカスタムロゞック フォヌムオブゞェクトの担圓分野
  37. 71 class Cable class CreateForm < ApplicationForm self.model_name = "Cable"

    attribute :name attribute :region, default: -> { "sea" } attribute :framework, default: -> { "rails" } attributes :secret, :rpc_host, :rpc_secret, :turbo_secret, :jwt_secret attr_reader :cable def initialize(...) super @cable = Cable.new( name:, region:, metadata: {framework:}, configuration: { secret:, rpc_host:, rpc_secret:, turbo_secret:, jwt_secret: } ) end def submit! = # ... end end What we should have done 私たちがすべきだったこず Written by senior Martian developer ベテランの✕星⌈開発者が曞いたもの
  38. 71 class Cable class CreateForm < ApplicationForm self.model_name = "Cable"

    attribute :name attribute :region, default: -> { "sea" } attribute :framework, default: -> { "rails" } attributes :secret, :rpc_host, :rpc_secret, :turbo_secret, :jwt_secret attr_reader :cable def initialize(...) super @cable = Cable.new( name:, region:, metadata: {framework:}, configuration: { secret:, rpc_host:, rpc_secret:, turbo_secret:, jwt_secret: } ) end def submit! = # ... end end UI / schema decoupling What we should have done 私たちがすべきだったこず Written by senior Martian developer ベテランの✕星⌈開発者が曞いたもの
  39. 72 class Cable class CreateForm < ApplicationForm self.model_name = "Cable"

    attribute :name attribute :region, default: -> { "sea" } attribute :framework, default: -> { "rails" } attributes :secret, :rpc_host, :rpc_secret, :turbo_secret, :jwt_secret validates :cable_is_valid validates :rpc_host, format: %r{\Ahttps?://}, allow_blank: true attr_reader :cable def initialize(...) = # ... def submit! = # ... private def cable_is_valid return if cable.valid? merge_errors!(cable) end end end Validations What we should have done 私たちがすべきだったこず Written by senior Martian developer ベテランの✕星⌈開発者が曞いたもの
  40. 73 class Cable class CreateForm < ApplicationForm self.model_name = "Cable"

    attribute :name attribute :region, default: -> { "sea" } attribute :framework, default: -> { "rails" } attributes :secret, :rpc_host, :rpc_secret, :turbo_secret, :jwt_secret validates :cable_is_valid validates :rpc_host, format: %r{\Ahttps?://}, allow_blank: true after_commit :enqueue_provisioning attr_reader :cable def initialize(...) = # ... def submit! = # ... private def enqueue_provisioning = cable.provision_later end end Trigger business operations What we should have done 私たちがすべきだったこず Written by senior Martian developer ベテランの✕星⌈開発者が曞いたもの
  41. 74 class Cable class CreateForm < ApplicationForm class Wizard <

    ApplicationWorkflow # ... end attribute :wizard_state, default: -> { "name" } attribute :wizard_action def submit! if wizard_action == "back" wizard.back! else wizard.submit! end return false unless wizard.complete? cable.save! end def wizard = @wizard ||= Wizard.new(self) end end Multi-step form logic What we should have done 私たちがすべきだったこず Written by senior Martian developer ベテランの✕星⌈開発者が曞いたもの
  42. 75 class Cable class CreateForm < ApplicationForm class Wizard <

    ApplicationWorkflow workflow do state :name do event :submit, transitions_to: :framework end state :framework do event :submit, transitions_to: :rpc, if: :needs_rpc? event :submit, transitions_to: :secrets event :back, transitions_to: :name end state :rpc do event :submit, transitions_to: :secrets event :back, transitions_to: :framework end state :secrets do event :submit, transitions_to: :region event :back, transitions_to: :rpc, if: :needs_rpc? event :back, transitions_to: :framework end state :complete end end end end Another abstraction — workflow! What we should have done 私たちがすべきだったこず Written by senior Martian developer ベテランの✕星⌈開発者が曞いたもの
  43. 76 class Cable class CreateForm < ApplicationForm class Wizard <

    ApplicationWorkflow workflow do state :name do event :submit, transitions_to: :framework end state :framework do event :submit, transitions_to: :rpc, if: :needs_rpc? event :submit, transitions_to: :secrets event :back, transitions_to: :name end state :rpc do event :submit, transitions_to: :secrets event :back, transitions_to: :framework end state :secrets do event :submit, transitions_to: :region event :back, transitions_to: :rpc, if: :needs_rpc? event :back, transitions_to: :framework end state :complete end end end end What we should have done 私たちがすべきだったこず Written by senior Martian developer ベテランの✕星⌈開発者が曞いたもの
  44. 78 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 This is how we leverage Rails building blocks これはRailsの構成芁玠 を掻✀する✅法だ
  45. 78 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 Validations / Types Callbacks Transactions awareness Interface Action View compat This is how we leverage Rails building blocks これはRailsの構成芁玠 を掻✀する✅法だ
  46. 79 <%= form_for @cable do |f| %> <%= f.text_field :name,

    required: true %> <%= f.text_field :region, required: true %> # ... <%= f.submit "Create" %> <% end %> <form action="/cables" method="post"> <input type="text" name="cable[name]" required> <input type="text" name="cable[region]" required> <!-- ... --> <input type="submit" value="Create"> </form> Form objects can be used in place of model objects フォヌムオブゞェクトを、モデルオブゞェクト の代わりに䜿✀できる
  47. 80 <%= form_for form do |f| %> <%= f.text_field :name,

    required: true %> <%= f.text_field :region, required: true %> # ... <%= f.submit "Create" %> <% end %> <form action="/cables" method="post"> <input type="text" name="cable[name]" required> <input type="text" name="cable[region]" required> <!-- ... --> <input type="submit" value="Create"> </form> self.model_name = "Cable" Form objects can be used in place of model objects
  48. 81 class CablesController < ApplicationController def new @form = Cable::CreateForm.new

    end def create @form = Cable::CreateForm.from(params.require(:cable)) if @form.save redirect_to cable_path(@form.cable), notice: "Success!" else status = @form.valid? ? :created : :unprocessable_entity render :new, status: end end end Controller code resembles the scaffolded code コントロヌラヌのコヌドは、 「rails g scaffold」 で⟃動✣成されたコヌドに䌌おいる
  49. 82 <%= form_for form do |f| %> <%= f.hidden_field :wizard_state

    %> <% if form.wizard.name? %> <%= f.text_field :name %> <% else %> <%= f.hidden_field :name %> <% end %> No storage required for intermediate state— just HTML! 䞭間の状態の保存は䞍芁で、HTMLだけは⌗分だ
  50. 83 <%= form_for form do |f| %> <%= f.hidden_field :wizard_state

    %> # ... <% if form.wizard.framework? %> <%= f.radio_button :framework, "rails" %> <%= f.radio_button :framework, "js" %> <%= f.radio_button :framework, "hotwire" %> <%= f.radio_button :framework, "default" %> <% else %> <%= f.hidden_field :framework %> <% end %> No storage required for intermediate state— just HTML! 䞭間の状態の保存は䞍芁で、HTMLだけは⌗分だ
  51. 84 <% if form.wizard.prerequisites? %> # ... <% end %>

    <%= form_for form do |f| %> <%= f.hidden_field :wizard_state %> # ... We can easily add UI only steps, too UIのみのステップも 簡単に远加できる
  52. 85 <% if form.wizard.rpc? %> <%= f.text_field :rpc_host %> <%

    else %> <%= f.hidden_field :rpc_host %> <% end %> <%= form_for form do |f| %> <%= f.hidden_field :wizard_state %> # ...
  53. 87 <% if form.wizard.can_complete? %> <%= f.submit "Create" %> <%

    else %> <%= f.submit "Next", formaction: new_cable_path %> <% end %> <% if form.wizard.can_back? %> <%= f.submit "Back", formaction: new_cable_path, value: "Back", name: "cable[wizard_action]" %> <% end %> <% end %> <%= form_for form do |f| %> <%= f.hidden_field :wizard_state %> # ... Now new action required for stepping back— "new" is enough 戻るための新アクションを远加する必芁がなくお、 "new"は⌗分だ
  54. 89 Controllers Presentation Channels Views Application Jobs Mailers Domain Infrastructure

    Models Adapters (DB, mail) API clients Forms これはフレヌムワヌクの⌀郚ずなる抜象化を導⌊する✅法の䟋でした
  55. Growing the Rails Way is possible if you don't fight

    the framework フレヌムワヌクず戊わなければ Rails Wayで成⻑するこずは可胜です
  56. Introduce new Rails-like abstractions having clear boundaries 1) Railsに䌌た新しい抜象化を導⌊する 2)

    明確な境界を匕く separating concerns & complexity 3) 担圓分野ず耇雑性を分離する belonging to your application 4) アプリケヌションに属する
  57. Make the pieces of your application puzzle fit together! Thank

    you! アプリケヌションのパズルのピヌ スをうたく組み合わせたしょう ありがずう Slides: evilmartians.com/events Twitter: @palkan_tula, @evilmartians Credits: Misha Dementyev $%, Rina Sergeeva &, SoundStripe.com '