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

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

Vladimir Dementyev
October 25, 2024
130

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

Vladimir Dementyev

October 25, 2024
Tweet

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 Way Optimize for getting from zero to one

    19 「ゼロ」から「イチ」への最適化
  10. 20 Rails Way rails new 0 IPO 1 ∞ 2

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

    Response そしてデフォルトでは、3つの⼯程がある: 処理はコントローラーで始まり、モデルと通信し、ビューを使ってレスポンスを準備する
  12. Rails gives you a solid frame and building blocks to

    assemble your application Rails はアプリケーションを組み⽴てるための堅牢なフレームと 構成要素を提供する
  13. But Omakase is not complete, there are gaps to fill

    authorization, complex UI logic, workflows, AI agents しかし、「おまかせ」は完全ではなく、 埋めるべき隙間がある: 認可、複雑なUI、ワークフロー、AIエージェント
  14. Filling the gaps, we often distort the frame our productivity

    & happiness decrease 隙間を埋めることで、私たちはしばしばフレームを歪めてしまいます ⽣産性と幸福度は低下する our codebase turns into a monster Ruby on Railsは、「怪獣 on Rails」になってしまう
  15. The guide on filling gaps the Rails Way RAILS WAY

    に従って隙間を埋める ためのガイド
  16. 41 gem "rails" # Database gem "sqlite3" # Real-time backend

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

    gem "anycable-rails" # Background jobs gem "sidekiq" # Image transformations gem "imgproxy-rails" ...to N
  18. 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の構成要素をあなたのツールボックスに⼊れる
  19. Extend Rails instead of melting it with something else 44

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

    Rails Way 48 RAILSアプリケーションに新しい抽象化レイヤーを導⼊する 際に、私が従う4つのルールだ
  21. I. Provide Rails-like DX and compatibility with core components 49

    RAILS らしい開発者体験とコアコンポーネントとの互換性 を提供する
  22. Think as a framework author, not a custom application developer

    50 カスタムプリケーション開発者の考え⽅ではなく、フレーム ワーク作者の考え⽅を持つ
  23. 56 Controllers Presentation Channels Views Application Jobs Mailers Domain Infrastructure

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

    code 57 介⼊より抽出: コード内で既存の抽象化を⾒つけよう
  25. Separate vanilla Rails from advanced techniques Keep the learning curve

    smooth but leave the opportunity to gain new knowledge 古典的なRailsと⾼度なテクニックを分離する 学習曲線をなめらかに保ちつつ 新しい知識を得る機会を 残しておく
  26. 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
  27. 61 class Post < ApplicationRecord has_many :comments, dependent: :destroy belongs_to

    :user validates :title, presence: true scope :tagged, TaggedQuery end Separated complexity 分離された複雑さ
  28. 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 分離された複雑さ
  29. <%= 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 私たちが持っていたもの
  30. 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つの新しいメソッド!
  31. 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つの新しいメソッド!
  32. 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先⽣で作られたコード
  33. 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らしくない アーキテクチャ層の越境は、密結合と低いメンテナンス性に⾄る
  34. Form object concerns Context-specific validations User input transformation User feedback

    Custom UI-driven logic (wizards) 70 コンテキストによりのバリデーション ユーザー⼊⼒の変換 ユーザーへフィードバックを提供すること UIに基づくカスタムロジック フォームオブジェクトの担当分野
  35. 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 ベテランの⽕星⼈開発者が書いたもの
  36. 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 ベテランの⽕星⼈開発者が書いたもの
  37. 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 ベテランの⽕星⼈開発者が書いたもの
  38. 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 ベテランの⽕星⼈開発者が書いたもの
  39. 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 ベテランの⽕星⼈開発者が書いたもの
  40. 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 ベテランの⽕星⼈開発者が書いたもの
  41. 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 ベテランの⽕星⼈開発者が書いたもの
  42. 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の構成要素 を活⽤する⽅法だ
  43. 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の構成要素 を活⽤する⽅法だ
  44. 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 フォームオブジェクトを、モデルオブジェクト の代わりに使⽤できる
  45. 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
  46. 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」 で⾃動⽣成されたコードに似ている
  47. 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だけは⼗分だ
  48. 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だけは⼗分だ
  49. 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のみのステップも 簡単に追加できる
  50. 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 %> # ...
  51. 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"は⼗分だ
  52. 89 Controllers Presentation Channels Views Application Jobs Mailers Domain Infrastructure

    Models Adapters (DB, mail) API clients Forms これはフレームワークの⼀部となる抽象化を導⼊する⽅法の例でした
  53. Growing the Rails Way is possible if you don't fight

    the framework フレームワークと戦わなければ Rails Wayで成⻑することは可能です
  54. Introduce new Rails-like abstractions having clear boundaries 1) Railsに似た新しい抽象化を導⼊する 2)

    明確な境界を引く separating concerns & complexity 3) 担当分野と複雑性を分離する belonging to your application 4) アプリケーションに属する
  55. 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 '