RET

Reflection English Technology

「英語環境で開発生産性をあげるために苦労したこと・やったこと」感想、学び

英語環境で開発生産性をあげるために苦労したこと・やったこと (2023/02/08 12:00〜)

こちらのセミナーに参加させていただき、とても勉強になったので感想、学びをアウトプットしたい。

開発メンバーの英語への抵抗感のなさ大事

delyさんでは英語を使用していくという方針はトップダウン的に組織の上の人間が決めて、それを社員全体が実行していったらしい。

この進め方で上手くいったのは社員の方々が英語を使うのに抵抗が少なかったからだとお話しされていた。「英語なんて使いたくない」と思う人が結構出てきてもおかしくないが、メンバーの抵抗感が少ないというのは結構ラッキーだなと思った。昨今グローバル化の流れを感じて、英語ができる必要があるということを多くのエンジニア自身が認識しているのでラッキーというよりむしろ必然の結果だったのかもしれない。

いつまでたっても「ここは日本なんで、英語なんて使いたくないです」みたいなモチベーションでいると、社内全体のグローバル化の流れを阻害しうる。今の環境が英語を使っていなくても、今後英語環境への転向を目指すことになるかもしれない。そんな時にも柔軟に対応できるようにエンジニア一人一人が開発環境のグローバル化を自分事として捉えていかないといけないのかもなあと思った。

開発組織の立ち上げ段階でグローバル人材を確保してくのが大事

delyの大竹さんのお話によると、グローバル採用によって多少なりとも開発効率が下がったそうなのだが、一方LINE Fukuokaの新田さんはグローバル採用をしても開発効率は下がらなかったとお話しされていた。

なぜかというと、開発組織の立ち上げフェーズですでにグローバル人材の確保をしていたので、グローバル人材を採用することによる大きな変化がなかったらしい。

組織立ち上げ段階からグローバルな開発組織を作る前提でいると、後々のギャップ、変更点が少ないのだと学んだ。今後、組織作りに関わってくることがあればとても参考になるお話しなだと思った。

物事を順序立てて説明する能力が大事

英語によるコミュニケーションはテキストベースか、それとも会話をしないと成立しないかといったトピックが話題に上がった。

結論としては、自分が書いたダイアグラムであったりを説明しないといけないシーンがあるのでどうしても会話は必要になるが、会話といっても何かしら順序立てた説明ができればOKらしい。

豊かな表現や、ユーモアのある会話ができるような英語を使える必要はないみたいなので、求められる英語力のハードルが高いわけではない。が、やはり言語共通の「説明力」がとても大事。英語を話していなくても常日頃から説明力に関しては意識する必要があると再認識させられた。

ローコンテクスト文化が大事

お恥ずかしながら「ローコンテクスト」というワードを聞いたのが今回で初めてだった。

ローコンテクストと相対する言葉としてハイコンテクストというものがある。

ざっくり調べたところ、ハイコンテクストは曖昧で間接的な説明や言葉遣いをして、その不明瞭さを共通認識やカルチャーで穴埋めしてくようなコミュニケーションスタイルのようだ。日本は特にハイコンテクスト文化が強いらしい。「行けたら行くわ」がいい例だ。直接的に「行かない」とは言わないけど、共通認識として「行かない」と言っているのと同じような意味になる。

対してローコンテキストは、明確で直接的な説明をして事前の文化や知識の共有が不要なコミュニケーションだ。話し相手は会話の行間を読む必要がないので、言われたことをそのまま受け入れればいい。

ハイコンテクスト文化が根付いていると、不十分なコミュニケーションによって何かしら大切なプロセスが抜け落ちてしまったりすることがあるらしい。

日本人はハイコンテクストを好むらしいが、確かに私自身もハイコンテクストな会話が多いかもしれない。多かもしれないとは思うのだが、いざ自分が使っているハイコンテクストな会話の一例を考えてみると意外と全く思いつかなかった。そういった曖昧な言葉遣いは無意識的に使っているので記憶に残らないのだろう。話し相手がどうすれば理解しやすいかを考えて、意識的に明確な言葉遣いを選ぼうと思った。

年末年始からこつこつGoいじってみた

Goの概要

Rubyと比べたGoの特徴

Ruby

Go

  • コンパイラ言語
  • 静的型付け言語
  • クラスや継承の概念がないがオブジェクト指向プログラミング(っぽいこと)が可能
    • 構造体やインターフェースをうまいこと使う
    • 正直理解がいまいちなので、ここら辺は別記事でいつか詳しくまとめたい
  • マイクロサービスアーキテクチャの中で使われる
  • 書いていて冗長でコードが長くなるという人もいるらしい
    • 「シンプルで要素が少なく、読みやすく、学びやすいことの裏返し」 by 実用Go言語

※1 クラウドネイティブ

クラウドネイティブは、クラウドコンピューティング環境で最新のアプリケーションを構築、デプロイ、および管理するソフトウェアアプローチです。

https://aws.amazon.com/jp/what-is/cloud-native/

クラウドを活用したアプリケーション開発のアプローチ。このアプローチで開発されたアプリをクラウドネイティブアプリケーションという。

クラウドネイティブアプリケーションは、マイクロサービスと呼ばれる相互に依存する複数の小規模なサービスで構成されるソフトウェアプログラムです。(引用元は上と同様)

この引用を見ると、クラウドネイティブアプリケーションはコンテナを使ったマイクロサービスアーキテクチャを採用するものであると考えられるが、文脈によって意味が変わってくるかも知れない。

クラウドネイティブ化(マイクロサービス)の利点としては

  • サービスごとの負荷分散が可能
  • ある一つのサービスに障害が起きても、影響を局所化できる

などがあるらしい。

(この辺りの話が色々書いてありそうな本があるので、後々読みたい)

www.amazon.co.jp

www.amazon.co.jp

Goの長所

Goの短所

マイクロサービスとGo

  • httpサーバー機能がデフォルトで実装されている
  • goroutine という軽量スレッドのおかげで並行処理が最適化される

これらの特徴に加えて文法もシンプルなので他の言語と比べて簡単にマイクロサービス実装可能

https://jp.quora.com/Goがマイクロサービスと相性が良いと言われているの

その他参考

Rubyとの違いを意識しながらTour of Goやってみた

細かい内容には触れず、「へぇ〜Goではそうやって書くんだ〜」と思ったところを軽く書いてみる。

戻り値を複数持てる

func swap(x, y string) (string, string) {
    return y, x
}

戻り値に名前をつけることができる(named return value)

func add(x, y int) (sum int) {
  sum = x + y
  return
}

GoのNamed return valueについてメリデメを考える

変数に初期値を与えずに宣言すると、勝手に初期値が入る(zero values)

var b boolfalse

定数は型の宣言が不要(型がない)

暗黙的な型変換を許さないことによる不便さを解消したい。

var num1 int = 3
var num2 float32 = 1.1

func main() {
    fmt.Println(num1 + num2)
}
 => invalid operation: a + b (mismatched types int and float32)

普通に3 + 1.1したいだけなのに、型が違うだけで拒否られる

const num1 = 3
const num2 = 1.1

func main() {
    fmt.Println(num1 + num2)
}
=> 4.1

このように定数の型を定義しなくてもいいことによって、柔軟さが生まれる

Go の定数の話 - Qiita

簡易文付きif

以下のvalueのようにif文の中でしかない変数を定義できる

func three(num int) string {
  if value := num % 3; value == 0 {
    return "3の倍数"
  } else {
    return "3の倍数ではない"
  }
}

func main() {
    fmt.Println(three(3))
}

例が雑だが以下のようにvalueを参照できない

func three(num int) string {
  if value := num % 3; value == 0 {
    "3の倍数"
  } else {
    "3の倍数ではない"
  }
  
  return value;
}

switch文でbreakがいらない

上から下にcaseが評価され、条件が一致したらそこで終了

func fruit(f string) string {
  var value string
  switch f {
    case "apple":
      value = "りんご"
    default:
      value = "果物を入力せよ"
  }
  
  return value
}

func main() {
  fmt.Printf(fruit("apple"))
}

ポインタ

package main

import "fmt"

func main() {
    i := 30
  fmt.Println(i);
    // ==> 30
    fmt.Println(&i)
    // ==> 0xc00001a090

    p := &i
    fmt.Println(*p)
    // ==> 30
    *p = 100
    fmt.Println(i)
}

slices

配列は固定長なのに対して、スライスは可変調。配列よりも一般的に使われているらしい。

package main

import "fmt"

type Dog struct {
    Name string
    Age int
}

func main() {
    var slice = []int{1, 2, 3, 4 ,5}
    fmt.Println(slice[1])

    var array = [5]int{1, 2, 3, 4, 5 }
    var s = array[1:4]
    fmt.Println(s)
}

goの連想配列(ハッシュ)はmap

var m = map[string]int{"age": 28, "height": 175}

組み込み関数のmakeを使っても連想配列を作れる

var l = make(map[string]int)
l["hoge"] = 3

methods

型に対してメソッドを定義できる。

package main

import "fmt"

type Dog struct {
    Name string
    Age int
}

func (a Dog) hello(){
    fmt.Printf("My name is %s\n", a.Name)
}

func main() {
  d := Dog{Name: "dog", Age: 13}
    d.hello()
}

逆引きGolang (マップ)

感想

Rubyだとpメソッドだけで済むのをimport "fmt”; fmt.Printf()とか書くのめんどくさいなあというのが正直な第一印象。 でも触るにつれどんどん「確かにシンプルなのかもなあ」と思ったり。まあチューリアルこなしただけだとまだまだ全然よくわかってないので、とりあえず今後Goで何かしらAPI作ってみるところまでやってみたい。

コードに残す「コメント」について

コードを読みやすくしたり、コードからは読み取れない新しい情報をコメントとして残す。

コメントが経年劣化して、誤った情報になってしまったり、無駄なコメントが多すぎて逆に可読性が落ちてしまうという状況は避けたい。

チェックポイント

  • 変数名、関数名をそのまま文章化したようなコメントになっていないか?
  • 変数や関数の命名のわかりにくさをコメントで補おうとしていないか?
  • ロジックの内部構造をなぞったようなコメントになっていないか?

コメントすべきこと、コメントする際のポイント

  • 「定数がなぜその値になったのか」といった実装の背景をコメントで残す
  • 他のエンジニアが見て疑問に思いそうなイレギュラーなポイントがあれば、コメントで説明
  • 「それ」といった何を指しているのか誤解を招きうる代名詞のない簡潔で正確なコメントにする
  • コードの意図は(実装の詳細から離れた)高レベルな説明にする
  • 仕様変更の時に注意する必要のあるポイントにはコメントしておく

コメント不要論に関して

https://qiita.com/hiro-hori/items/1cf28a6ac75db077c8f3

こちらの記事のコメント欄がすごい白熱していて参考になる。

見た感じの結論だと「コメントは一つたりとも書くな」と言うわけではなくて、良いコメントは積極的に書いていこうということになると思う。やはりロジックの詳細をなぞっただけのコメントやコードを見ただけでわかるようなことが書いてあるコメントは不要であるが、このコードを書いた意図(特にwhy not)を残しておくのは大事そう。

まとめ

コメントとして残しておくこと

  • 「なぜそうしたか」「なぜそうしなかった」という意思決定や背景
  • 他のエンジニアが読んだら疑問に思うと想定されるポイント
  • 仕様変更時の注意点

(今まであまりコメントを書いてこなかったので改めて勉強してみた、、、)

<SOLID原則>単一責任の原則について

単一責任原則とは

オブジェクト指向のSOLID原則のうちの一つ。色々な説明のされ方がある

  • モジュールを変更する理由はたった一つだけであるべきある。
  • モジュールはたったひとつのアクターに対して責務を負うべきである
  • 1つのモジュール(クラス)は1つだけの責任を持たなければならない

単一責任はクラスの責務の話として言及されることが多いが、メソッドレベルでも単一責任を意識する必要がある。

単一責任と凝集度、結合度

単一責任を実現するクラスは、責務に沿ったデータとロジックをまとめた凝集性の高いものである必要がある。

逆にいうと、密結合低凝集なクラスを作ると単一責任を阻害することになる。

https://blog.cleancoder.com/uncle-bob/2014/05/08/SingleReponsibilityPrinciple.html

Robert C. Martinは上の記事で、以下のように単一責任に関して言及している。

Another wording for the Single Responsibility Principle is:

Gather together the things that change for the same reasons. Separate those things that change for different reasons.

If you think about this you’ll realize that this is just another way to define cohesion and coupling.

つまりは単一責任原則を実現することは、高凝集低結合を実現することと同義であるといえる。

単一責任にする重要性

複数の責任を持つモジュールは再利用しにくい。

このようなモジュールを再利用しようとすると、一つの修正によって予期せぬところでバグが発生する可能性を作り出してしまう。

単一責任を阻害する「共通化

単一責任原則の説明の中でよく一緒に語られるのが、過度な共通化だ。

過度な共通化によって、一つの変更が予期せぬバグを生みうる状況が作られてしまう。

例えばリマインドメールを送るクラスと、メルマガを送るクラスがあるとする。

その2つのクラス内で、現在の日付がメール配信日かどうかを判定するバリデーションロジックが存在していて、内容が全く同じである。

class RemindMail
  def send_mail
    send_remind_mail if delivery_date?
  end

  private

  def delivery_date?
    今日が配信日かどうか調べて、真偽値を返す
  end
end

class MailMagazine
  def send_mail
    send_mail_magazine if delivery_date?
  end

  private

  def delivery_date?
    RemindMail内のdelivery_dateと全く同じ内容
  end
end

これがDRY原則に反すると考え、バリデーションロジックを別のクラスに移動させた。

class DateValidator
  def initialize(date)
    @date = date
  end

  def perform
    今日が配信日かどうか調べて、真偽値を返す
  end
end

そして以下のように2つのクラスでDateValidatorを呼び出す

class RemindMail
  def send_mail
    send_remind_mail if DateValidator.new(Date.today).perform
  end
end

class MailMagazine
  def send_mail
    send_mail_magazine if DateValidator.new(Date.today).perform
  end
end

さて、このような状況の時に、リマインドメール側で配信日かどうかを調べるロジックを変更する必要が出てきた。そこでDateValidatorのperformメソッドをいじくることにした

class DateValidator
  def initialize(date)
    @date = date
  end

  def perform
    何かしらの変更を施す
  end
end

こうなったときに、変更の必要性がないメールマガジンの配信日チェックロジックにも影響が出てしまう。最悪の場合、気付かれることなくバグが放置され業務に大きな損害をもたらす。

このようにMailMagazineクラスはMailMagazineクラス自身の変更の必要性だけでなく、RemindMailクラスの変更の必要性にも影響を受けた。

つまり、「あるモジュール(クラス)が変更される理由は必ず一つでなければならない」とする単一責任原則に反していると言える。

DRYとは何か

同じコードを重複して書いてはいけないとする原則がDRY原則である。

しかし、DRYが「ソースコードのコピー&ペーストをしてはいけない」とする原則であると単純解釈するのは危険だ。

厳密にはDRYというのは「知識」や「意図」の二重化についての原則なのである。

コードが全く同じでも、そのコードが表現している知識や意図が異なっているのであれば、それらを共通化するべきではない。

「知識」とは何か

ここで言う「知識」の中の一つが「ビジネス知識」であるとミノ駆動本には書いてある。ビジネス知識はビジネスのなかで使われる概念で、先程のコードの例だと「リマインドメール」「メールマガジン」がビジネス概念と言える。

リマインドメールとメールマガジンという異なった知識に関するコードを共通化するべきではなかったのだ。

通化するタイミング(設計を決定するタイミング)

早い段階で設計を決定する必要はない。

  • 今設計を決定するメリット
  • 将来の変更によってかかってくるコスト

この2つをうまく比較衡量する。

ある機能を共通化するという設計判断に関して考えてみよう。もし、今共通化しなくても将来の変更による負担があまり大きくないのであれば、共通化の決定を遅らせてもいい。

Sustainable Web Development with Ruby on Rails」では同じ記述が3回出てきたら重複したコードを共通化すると書かれており、このルールを「rule of three」と呼んでいる。

同じコードが2箇所現れても共通化する判断を遅らせているいい例なのではないだろうか。

あるクラスやメソッドが単一責任かどうか確認するには?

  • 一文でクラスやメソッドについて説明してみる
    • and的なワードが入っていればそれは単一責任ではない
  • クラス内のパブリックメソッドが、クラスの責務を果たすためのものになっているか確認する
    • パブリックメソッドがそのクラスの責務を表現する

責任が単一かどうかの判断は難しい、、

どうしても単一責任かどうか迷うようであれば、先述の通り設計の判断を遅らせるのもあり。

ここで「アジャイルソフトウェア開発の奥義」からの抜粋を載せたい。

ここには必然的な法則がある。「変更の理由が変更の理由たるのは、実際に変更の理由が生じた場合だけである」という法則だ。変更の兆候もないのに単一責任の原則(SPR)を適用するのは賢明ではない。他の原則についても同様だ

これはとても重要な法則であるように思える。「単一責任にしないといけない」という原則に固執して、無理して単一責任を適用した結果、コードに無駄な複雑になるのであれば意味がない。

「最初の弾丸は甘んじて食らう」という方針のもと、最初は変更が起きないことを前提としてコードを書き、もしも変更が起きてしまったらその時に責務分割に着手するというのでもいいのかもしれない。

責任を考える上で役立ちそうな視点「アクター」

コードに対する変更を引き起こしているのは何なのだろうか?

それは「人」であるとRobert C. Martinと断言する。変更を要求する人がいるから、コードを書き換えないといけない。

However, as you think about this principle, remember that the reasons for change are people. It is people who request changes.

そして単一責任の原則は「人」に関する原則であると言う。

And this gets to the crux of the Single Responsibility Principle. This principle is about people.

https://blog.cleancoder.com/uncle-bob/2014/05/08/SingleReponsibilityPrinciple.html

この「人」を彼の著書Clean Architectureでは「アクター」と呼んでいる。

ここでは、変更を望む人たちをひとまとめにしたグループとして扱いたい。このグループのことをアクターと呼ぶことにしよう

(一人のユーザーやステークホルダにとどまらず、)特定の意図や変更要求を持ったユーザーやステークホルダの集合体がアクターと言える。

なおステークホルダとは「人」だけでなく、

システムが呼び出す外部Webシステム 機器組み込み系のプログラムにおける各種センサー バッチシステムにおけるスケジューラ

なども考えられるらしい。

https://www.ogis-ri.co.jp/otc/hiroba/others/OOcolumn/single-responsibility-principle.html

彼は単一責任の原則を「モジュールを変更する理由はたった一つだけであるべき」という内容だとしつつ、さらにその「変更される理由」は何かについて踏み込んだ。

モジュールはたったひとつのアクターに対して責務を負うべきである

つまりは、あるモジュール(クラス)に対して変更要求してくるアクターはたったひとつだけであるべきであるということである。

When you write a software module, you want to make sure that when changes are requested, those changes can only originate from a single person, or rather, a single tightly coupled group of people representing a single narrowly defined business function.

責務を見出すために、モジュールの機能を利用するアクターが誰(何)なのかを考えるというのは視野を広げるという意味で結構有益なのではないかと思う。

先程のリマインドメールと自動メルマガの例で再び考えてみると

  • リマインドメールを利用するのは、特定のイベントに参加申し込みをしていたユーザー
  • 自動メルマガを利用するのは、自動メルマガを受け取る設定をしているユーザー

といった感じになる。

リマインドメールと自動メルマガでアクターが異なるので、無理な共通化はやめておくべきと判断できそうだ。

このクラスに対応するアクターは何か?という問いは責務を考える上でいつか役立つかもしれない。

その他参考資料

オンラインイベント「顧客提供価値を高めるための技術的負債への向き合い方」感想・学び

先日こちらのオンラインイベントに参加させていただきました。色々なことを学ばせていただき、とても良いイベントでした。印象に残ったお話や感想を軽くまとめていこうと思います。

顧客提供価値を高めるための技術的負債への向き合い方 (2022/11/10 12:00〜)

技術的負債の解消に積極的な人が少ないので、メンタル的にハード

以前、参加させていただいた「良いコード/悪いコードで学ぶ設計入門」の著者トークで著者の仙塲さんも似た体験をされたとお話しされていました。やはり、レガシー改善に対して消極的な人は一定数いるんだなと再認識しました。

技術的負債の解消をスタートさせても、「それいいね!どんどんやっていこう!」て感じで仲間が増えていくところまでいくのが大変だそうです。

技術的負債の解消に積極的でない人たちと敵対関係を作らないような配慮も重要です。

技術選定の観点

  • 今いるメンバーの技術スタック的にマイグレーションしやすいか
  • 採用で魅力的に映るか

等の視点で新しい技術を選定していったというお話もありました。

技術選定はシンプルに技術の特性を理解しているだけでなくて、メンバーのことやビジネスのことも考慮する必要があるんですね。

ビジネスの成長とレガシー改善の足並みを揃えるのが難しい

ビジネス側の人たちにとっては事業の成長が最優先事項であり、技術的負債の解消にあまり関心がないケースがあるみたいです。そのため「技術的負債を解消させてください」とビジネスサイドを説得するのが難しく、「生産性を上げるために〇〇したい」みたいな宣言をしてしまうと、どのくらい改善したかという数字を出す必要が出てきてきついというお話がありました。こういった事情があるため、技術的負債の解消はビジネス成長のための機能追加と同時にしれっと実行しているそうです。

「許可を得ないとやれないというマインドでやっているといつまでもやれない」というのはとてもリアルな経験談でためになりました。

メンバーや経営陣に対する働きかけ

  • 経営者に対して、リプレイスへの投資の意義や費用対効果に関する説明責任がある
  • 開発メンバーがどんどん挑戦していけるような文化を醸成する

技術的負債の解消に着手する前段階の環境作りって予想以上に根気が必要そうです。

また、リーダーシップと同じくらいフォロワーシップも大事というお話があり、その通りだなあと思いました。

挑戦を後押しするような雰囲気を作るには、リーダーだけが頑張ればいいのではなく、その他の開発メンバー一人一人の協力がとても大事であるということです。たとえリーダー的ポジションでなくても、開発メンバー個々人が「チームの変革者」として当事者意識を持つことがチーム全体を良くしていくのかもしれません。

感想

技術的負債の解消って、いわゆるレガシーコードをどのように改善していくかみたいなところがメインだと思っていました。しかし、今回お話を聞いて、コードをどういじるかよりも、コードの外にある色々な要因の調整がとても重要であることを学べました。私自身もフォロワーシップ、リーダーシップを発揮して技術的負債を解消していけるようなチーム作りに貢献したいと思います。

FactoryBotで、特定の属性の値が異なる大量のテストデータを作りたい時のメモ

FactoryBotで大量のテストデータを作りたい時にはcreate_listメソッドが使える

create_list(:food, 100)

これでFoodのレコードが100件保存される。

では、消費期限が異なるFoodのレコードを100件作りたい場合はどうするか。

以下のようにcreate_listメソッドにはブロックが渡せる。

create_list(:food, 100) do |food, i|
  food.expiration_date = Date.today + i
end

これで、expiration_dateの値が異なるFoodのレコードを100件作れる。と思いきや、、、

expect(Food.all.pluck(:expiration_date)).to eq []

=>
expected: []
     got: [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil、、、(省略)

レコードは作られるものの、expiration_dateが全てnilになってしまう。

create_listにブロックを渡したときの挙動

下の記事の表現を引用させていただく。

https://dev.to/hernamvel/the-optimal-way-to-create-a-set-of-records-with-factorybot-createlist-factorybot-buildlist-1j64

create_listメソッドは以下のような挙動になる

def create_list
  object = build the object
  object.save!
  yield(object) if block_given?
end

つまり、foodをsaveした後に、そのsaveしたfoodをブロックに渡すという挙動になる。

そのため、すでにsaveされたfoodに対してexpiration_dateをセットしているものの、再度saveされていない(expiration_dateを更新していない)ためexpiration_dateはnilのままになる

解決策

以下のようにブロックの中でsave!をする。

create_list(:food, 100) do |food, i|
  food.expiration_date = Date.today + i
  food.save!
end

しかし、上のやり方だと、ひとつのオブジェクト(food)に対して2回saveが走る。

以下のようにbuild_listを使うと1オブジェクト1saveでテストデータを作成できる。

build_list(:food, 100) do |food, i|
  food.expiration_date = Date.today + i
  food.save!
end

ただ、このやり方で100件もsaveするとSQLの発行回数も100件になってしまい、テストが非常に重くなる。

ということで以下のようにバルクインサートする。

foods = build_list(:food, 100) do |food, i|
  food.expiration_date = Date.today + i
end

Food.insert_all foods.map(&:attributes)

参考文献

<Rails>sessionメソッドの実装について軽くコードリーディングしてみた

はじめに

session "メソッド" って言うけど、使われ方が全然メソッドっぽくない。

sessionメソッドは以下のようにして使われる。

session[:user_name] = user.name

まるでハッシュのようだ。

しかも

session.class
=> ActionDispatch::Request::Session

というふうにあたかもsessionというオブジェクトに対してclassメソッドが使えているように見える。

とはいえ、RailsチュートリアルRailsガイドにもsession (インスタンス)メソッドと書いてあるので当然ながらメソッドである。

コントローラー内でbyebug↓

(byebug) defined? session
#=> "method"

続けてsessionとだけ打ってみる。

(byebug) session
#<ActionDispatch::Request::Session:0x00007ff84f3fc9c0 @by=#<ActionDispatch::Session::CookieStore:0x00007ff853201cc0 (長いので省略)>

sessionメソッドでActionDispatch::Request::Sessionのインスタンスが返る。

つまり、session[:user_name] というのはsessionメソッドの戻り値であるActionDispatch::Request::Sessionのインスタンスに対して[]= メソッドが呼び出せているということになる。

sessionにclassメソッドが使えているのは、sessionメソッドの戻り値であるActionDispatch::Request::Sessionのインスタンスに対してclassメソッドが呼び出せているという仕組み。

ということは、[]= メソッドがActionDispatch::Request::Session内に定義されているはずなのだが、ActionDispatch::Request::Sessionに行き着くまでにコードがどのような流れで処理されているのかをコードリーディングしたい。

コードリーディングして実装を調べてみる

trace_locationを使用してsession[:current_user_id] = 7というコードがどのような流れで処理されていくのかを調べてみる。

https://github.com/yhirano55/trace_location

TraceLocation.trace do
   session[:current_user_id] = 7
end

そうするとログが得られるので、内容を以下で要約。

処理の順番
1. ActionController::Metal#session2. Rack::Request::Helpers#session3. Rack::Request::Env#fetch_header4. ActionDispatch::Request::Session#[]=**

順番に見ていく。

ActionController::Metal#session

sessionメソッドが使われると、まずここに処理が飛ぶ。

delegate :session, to: "@_request"

https://github.com/rails/rails/blob/main/actionpack/lib/action_controller/metal.rb#L157

delegateメソッド

delegateは以下のメソッドを自クラスに定義しているのと同じ。

def session
  @_request.session
end

つまりActionController::Metal内でsessionインスタンスメソッドが定義されてる。

ApplicationControllerがこのActionController::Metalを継承しているため、UsersController等の自作のコントローラー内でもsessionインスタンスメソッドが使用できるというわけだ。

@_requestは何者か?

@_requestはおそらく、ActionDispatch::Requestのインスタンスである。

なぜなら、sessionメソッドの実体は以下で見ていくRack::Request::Helpersに定義されているのだが、このモジュールをincludeしているのがActionDispatch::Requestしかないからだ。

https://github.com/rails/rails/blob/6291a9f6d36d77b32df7d07a8c7c56103a555d39/actionpack/lib/action_dispatch/http/request.rb#L19

Rack::Request::Helpers#session

def session
  fetch_header(RACK_SESSION) do |k|
    set_header RACK_SESSION, default_session
  end
end

@_request.sessionを実行するにあたり、次はsessionメソッドの実体を探しに行く。

(trace_locationを知るまでこのコードに辿り着けなくて苦労した、、)

fetch_headerってなんやんということで次。

Rack::Request::Env#fetch_header

def fetch_header(name, &block)
  @env.fetch(name, &block)
end

@envとは何かがわからなかったので以下を参考にする。

https://qiita.com/coe401_/items/ad7dc2f3e319c5beaf40#%E3%83%AC%E3%82%B7%E3%83%BC%E3%83%90env%E3%81%A8%E3%81%AF

@envはHTTPヘッダを表すハッシュ

nameRACK_SESSION

&blockset_header RACK_SESSION, default_session (厳密に言うとdo ~ endのかたまり)

fetchメソッド

Hash#[]と同じく、指定したkeyのvalueを取得する。

Hash#[]と違いkeyが無い場合nilではなく例外が発生する。

第二引数にデフォルト値を指定することが可能。key が存在しない場合はこのデフォルト値が返る。

book = { name: 'hoge', price: 1000 }
book.fetch(:name, "fooo")
=> 'hoge'

book.fetch(:author, "foobar")
=> 'foobar'

つまり@envというハッシュからname(つまりRACK_SESSION)というkeyを探し、存在しなければ&blockが返る。

trace_locationのログにset_headerに関するものがないため、(僕の環境で)sessionメソッドが走った時はRACK_SESSIONというkeyが存在していたということなのだろう。

ActionDispatch::Request::Session#[]=

def []=(key, value)
  load_for_write!
  @delegate[key.to_s] = value
end

ここで[]=メソッドの定義に処理がとぶ。

つまりはここまででsessionメソッドの処理が終わったということである。 sessionメソッドの戻り値はActionDispatch::Request::Sessionのインスタンスなので、RACK_SESSIONというkeyのvalueがActionDispatch::Request::Sessionのインスタンスであると推測できる。

@delegate

@delegateとはなんだろうか?処理を止めて調べてみる。

(byebug) @delegate
{"session_id"=>"f15808a61b80a6b1f8143482897524a2", "_csrf_token"=>"tqNAbzcy135NjTirnmZbtIjHcS9mAXQMWhku37T9iuk=", "current_user_id"=>7}

https://github.com/rails/rails/blob/7c1165c8c8752a84a9d02ef06fccbf113b8ff6b0/actionpack/lib/action_dispatch/request/session.rb#L69

@delegateにはsessionの中身が入っていた。

つまりは

session[:key] = value

でsessionにデータを追加したり更新できたりするということになる。

load_for_write!に関するログもあるが、お腹いっぱいのためここまで。