Slide 1

Slide 1 text

定数参照のトラップと それを回避するために 行う2 つのこと Kaigi on Rails 2023 OGUCHI Haruka @hogucc

Slide 2

Slide 2 text

フィヨルドブートキャンプ卒業生 株式会社エンペイで働いています エンペイでは主に教育業界の集金 業務のキャッシュレス化・DX 化を 実現するenpay というプロダクト を開発しています 今年は弊社もKaigi on Rails のスポ ンサーとして協賛させていただい ております🎉 Haruka Oguchi @hogucc

Slide 3

Slide 3 text

Part 1 定数参照のハマりどころを理解するた めの前提知識の共有 Part 2 ハマりどころをしくみで回避する方法 の模索 トークの概要 Ruby やRails 初心者の方向けのトークです

Slide 4

Slide 4 text

参照したかったのとは 違う定数を参照してしまった! ... ってことありませんか?

Slide 5

Slide 5 text

uninitialized constant エラーが発 生することで気づける 指定した定数が定義 されていなかった そのまま別の値が参照されてし まったらエラーにならないので 気づきづらい 別の定数が参照され てしまった 意図した定数参照になっていない原因

Slide 6

Slide 6 text

実行してみるまで結果が わからないのってスリルありま せんか? 最悪そのまま本番環境へデプロイされて障害が発生...

Slide 7

Slide 7 text

Ruby やRails の 定数探索のしくみ を理解する Ruby やRails のしくみを知る 意図しない定数参照を回避するために 私たちができること 意図した定数を 参照するような しくみを作る 知識のしくみ化

Slide 8

Slide 8 text

Ruby やRails の 定数探索の しくみを理解する

Slide 9

Slide 9 text

Ruby では アルファベットの大文字 から始まる名前 が定数として扱われる Rails のcontroller 名やmodel 名もクラスなので定数 クラス名やモジュール名も定数

Slide 10

Slide 10 text

定数の 読み込み 定数の参照 定数の探索 ここから遡って 説明します 定数が取得されるまでの大まかな流れ

Slide 11

Slide 11 text

定数が参照されているスコープ 外側のスコープ(トップレベルは除く) スーパークラス Ruby の定数探索アルゴリズム トップレベル

Slide 12

Slide 12 text

定数が参照されているスコープ 外側のスコープ(トップレベルは除く) スーパークラス Ruby の定数探索アルゴリズム トップレベル 見つからなかったら エラーを返す

Slide 13

Slide 13 text

MY_CONSTANT = "I’m from the top level scope" module MyModule MY_CONSTANT = "I’m from the my module scope" class ParentClass MY_CONSTANT = "I’m from the super class scope” end class MyClass < ParentClass MY_CONSTANT = "I’m from the current scope” def self.show_constant puts MY_CONSTANT end end end Ruby の定数探索アルゴリズム

Slide 14

Slide 14 text

MY_CONSTANT = "I’m from the top level scope" module MyModule MY_CONSTANT = "I’m from the my module scope" class ParentClass MY_CONSTANT = "I’m from the super class scope” end class MyClass < ParentClass MY_CONSTANT = "I’m from the current scope” def self.show_constant puts MY_CONSTANT end end end Ruby の定数探索アルゴリズム 定数参照している スコープから 探索開始

Slide 15

Slide 15 text

MY_CONSTANT = "I’m from the top level scope" module MyModule MY_CONSTANT = "I’m from the my module scope" class ParentClass MY_CONSTANT = "I’m from the super class scope” end class MyClass < ParentClass MY_CONSTANT = "I’m from the current scope” def self.show_constant puts MY_CONSTANT end end end Ruby の定数探索アルゴリズム 外側のスコープ を探索

Slide 16

Slide 16 text

MY_CONSTANT = "I’m from the top level scope" module MyModule MY_CONSTANT = "I’m from the my module scope" class ParentClass MY_CONSTANT = "I’m from the super class scope” end class MyClass < ParentClass MY_CONSTANT = "I’m from the current scope” def self.show_constant puts MY_CONSTANT end end end Ruby の定数探索アルゴリズム スーパークラス を探索

Slide 17

Slide 17 text

MY_CONSTANT = "I’m from the top level scope" module MyModule MY_CONSTANT = "I’m from the my module scope" class ParentClass MY_CONSTANT = "I’m from the super class scope” end class MyClass < ParentClass MY_CONSTANT = "I’m from the current scope” def self.show_constant puts MY_CONSTANT end end end Ruby の定数探索アルゴリズム トップレベルを探索

Slide 18

Slide 18 text

MY_CONSTANT = "I’m from the top level scope" module MyModule MY_CONSTANT = "I’m from the my module scope" class ParentClass MY_CONSTANT = "I’m from the super class scope” end class MyClass < ParentClass MY_CONSTANT = "I’m from the current scope” def self.show_constant puts MY_CONSTANT end end end Ruby の定数探索アルゴリズム 見つからなかったら エラーを返す

Slide 19

Slide 19 text

定数の 読み込み 定数の参照 定数の探索 次はここ 定数の値が参照されるまでのざっくりとした 流れ

Slide 20

Slide 20 text

Ruby の定数参照 のパターン :: をつけずに参照する 定数の間に:: をつけて参照 する 先頭に :: をつけてトップレ ベルから参照する 例)Zoo 例)Zoo::Animals 例)::Zoo 1 2 3

Slide 21

Slide 21 text

MY_CONSTANT = "Top Level Constant" module MyModule MY_CONSTANT = "Current Level Constant" puts ::MY_CONSTANT end 先頭に:: をつけてトップレベルから探索する “Top Level Constant” が 出力される

Slide 22

Slide 22 text

定数の 読み込み 定数の参照 定数の探索 次はここ 定数の値が参照されるまでのざっくりとした 流れ

Slide 23

Slide 23 text

require やload は不要 同一ファイル内の定 数を読み込む require やload が必要 別ファイルの定数を 読み込む 定数の読み込み autoload で定数が初めて参照された ときに定数が定義されたファイルを 読み込むことができる

Slide 24

Slide 24 text

require "application_controller" require "user" class UsersController < ApplicationController def index @users = User.all end end require は 必要ない! Rails のautoload

Slide 25

Slide 25 text

app 配下に 後から追加した カスタムディレクトリ も対象 デフォルトではapp 配下のassets, javascript, views を除くすべてのサブディレク トリが対象 Q. なぜrequire がいらないのか A. Rails のconfig.autoload_paths に設定され たパスは自動読み込みの対象になるため

Slide 26

Slide 26 text

autoload のメリット 1 2 3 開発環境ではアプリを再起動しなくて もコードの変更がリアルタイムに反映 される ファイル名とクラス名の不整合があれ ばロード時に検出される 本番環境ではアプリケーション起動時にすべてのコードを読み込 む。都度ロードを挟まないことでパフォーマンスを最適化

Slide 27

Slide 27 text

autoload のメリット 1 2 3 開発環境ではアプリを再起動しなくて もコードの変更がリアルタイムに反映 される ファイル名とクラス名の不整合があれ ばロード時に検出される 本番環境ではアプリケーション起動時にすべてのコードを読み込 む。都度ロードを挟まないことでパフォーマンスを最適化 Zeitwerk という コードローダーが 定数のロードを 担っている

Slide 28

Slide 28 text

定数参照の ハマりどころを しくみで回避

Slide 29

Slide 29 text

# app/controllers/user/notifications_controller.rb class User::NotificationsController < ApplicationController def index roles = User::ROLES end end # app/models/user.rb class User < ApplicationRecord ROLES = ["member", "guest"] end 定数参照の失敗例① model のUser::ROLES を 参照したかったのに controller のUser が参照され エラーになってしまった... ~階層化していたcontroller の名前空間とmodel のクラス名が被ってしまったケース~

Slide 30

Slide 30 text

# app/controllers/user/notifications_controller.rb class User::NotificationsController < ApplicationController def index roles = User::ROLES end end # app/models/user.rb class User < ApplicationRecord ROLES = ["member", "guest"] end 定数参照の失敗例① ~階層化していたcontroller の名前空間とmodel のクラス名が被ってしまったケース~ ::User::ROLES のように トップレベルから 参照すればOK

Slide 31

Slide 31 text

module Wrapper module Aws class S3 def self.upload_file s3 = Aws::S3::Client.new(region: ‘xxxx’) end end end end 定数参照の失敗例② 〜名前空間がネストしたモジュールやクラス〜 unitialized constant Wrapper::Aws::S3::Client というエラーが発生🥺 Aws::S3::Client はgem に定義されたクラス

Slide 32

Slide 32 text

module Wrapper module Aws class S3 def self.upload_file s3 = Aws::S3::Client.new(region: ‘xxxx’) end end end end 定数参照の失敗例② 〜名前空間がネストしたモジュールやクラス〜 Wrapper::Aws::S3 と みなされて、Client という 定数は存在しない、と エラーになっていた💦 Wrapper まで辿って Aws::S3 を発見!

Slide 33

Slide 33 text

マージされた 内容をテスト テスト環境 ユーザーが 使う環境 本番環境 Pull Request を マージ 開発環境 開発→テスト→エラー発見→開発→またテスト... エラー発生😢 修正 OK! エラーになった箇所はこの環境から参照される

Slide 34

Slide 34 text

マージされた 内容をテスト テスト環境 ユーザーが 使う環境 本番環境 Pull Request を マージ 開発環境 開発→テスト→エラー発見→開発→またテスト... エラー発生😢 修正 OK! エラーになった箇所はこの環境から参照される 開発環境の時点で気づきたかった... ! さっき見たこの知識があれば気づけた! ... かも?

Slide 35

Slide 35 text

class S3 def self.upload_file s3 = Aws::S3::Client.new(region: ‘xxxx’) end end 絶対に気づける... ? もともとはS3 という クラスで、後から Wrapper::Aws::S3 という 名前空間が切られた としたら気づける... ? 複数箇所の定数を 一度に直した後、 別の定数が参照 されていることに 気づける...? 自分さえ間違えなければ それでいい... ?

Slide 36

Slide 36 text

gem を作りました

Slide 37

Slide 37 text

いま参照している定数が どの定数としてRuby に 認識されているかを 知りたい gem で実現したいこと 定数の候補を出力してほしい 全ての定数の先頭に :: をつけることを強制しても解決はできそうで すが、やりたいことに対してオーバーキルしている感があり、 やめました 意図せず 別の定数参照 しちゃってた 問題を解決 別の名前空間 の定数も検索

Slide 38

Slide 38 text

いま参照している定数が どの定数としてRuby に 認識されているかを 知りたい 定数の候補を出力してほしい ConstantVision.search("HOGE", "Fuga::Piyo") # => origin: xxx, candidates: ["yyy", "zzz"] constant_vision というgem を作りました https://github.com/hogucc/constant_vision

Slide 39

Slide 39 text

No content

Slide 40

Slide 40 text

No content

Slide 41

Slide 41 text

①知識をつける ②しくみで解決する トラップを回避するために行う2 つのこと 技術で問題解決するのは 楽しい! 同じ問題を抱えた他の 誰かも幸せになれるかも

Slide 42

Slide 42 text

Thank you!