Write Barrier Unprotected Objects とは

業務で調べる機会があったのでブログにもまとめる。

世代別 GC とインクリメンタル GC

Ruby 処理系には世代別 GC やインクリメンタル GC が採用されている。これらアルゴリズムを採用するには Write Barrier と呼ばれる仕組みが必要であるが Ruby には実装されていない。そこで Ruby 向けのアルゴリズムを使った RGenGC と RincGC が実装されている*1*2

Write Barrier とは

世代別 GC には、メジャー GC とマイナー GC と呼ばれる、オブジェクトのライフタイム別 GC がある。メジャーは古いオブジェクトに対し、マイナーは新しいオブジェクトに対して GC を行う。古いか新しいかは GC を生き残った回数で計られる。

マイナー GC は新しいオブジェクトのみを GC 対象とするために困った問題が起こる。古いオブジェクトから新しいオブジェクトに参照があったときそれを検知できないのだ。なぜならマイナー GC は新しいオブジェクトをルートにし、そこから辿れるオブジェクトの参照しかチェックしていないから。もしマイナー GC で古いオブジェクトからの参照もチェックしていたら世代別 GC の意味がなくなってしまう。そして、問題というのは、古いオブジェクトから参照されている新しいオブジェクト、つまりまだ使いたいオブジェクトまで誤ってスイープされてしまう。

この問題を回避するために世代別 GC には Write Barrier という仕組みがセットで必要になる。Write Barrier は古いオブジェクトから新しいオブジェクトへの参照が追加されたとき、古いオブジェクトを「リメンバーセット」に登録する。リメンバーセットに登録されたオブジェクトは次のマイナー GC でマークするときの起点に含まれる*3。これにより参照されている新しいオブジェクトの誤ったスイープを回避できる。めでたし。

要約すると、Write Barrier は参照されている新しいオブジェクトを間違えてスイープしないための仕組みといえる。

Write Barrier Unprotected Objects とは

Write Barrier によって保護されていないオブジェクトのこと。

冒頭書いたように、Ruby には Write Barrier が実装されていないので、初期は全てが Write Barrier Unprotected Objects となる。笹田氏らは、Ruby でよく使われるオブジェクト (String, Array, Hash...) に集中的に Write Barrier を実装する戦略を採った。あまり使われないオブジェクトへの実装は後回しにすることで世代別 GC の旨味を早いうちに享受しつつ、順次改善できるという目論見らしい。

では、Write Barrier Unprotected Objects はマイナー GC で誤って Sweep されてしまうではないか、となるがそんなことはない。その辺りは*4に詳しく記されている。Unprotected Object は旧世代扱いにしないことで常にマイナー GC のマーク対象として辿れるようにしているらしい。

直近のリリース Ruby 3.3 でも Time クラスに Write Barrier が導入されたことが記されている*5。ちょっとずつ進んでいるらしい。

世代別GCで必要となるライトバリア(WB)を、複数のクラスに導入しました。WB がない場合は、時間がかかるけどちゃんとうごく、というアルゴリズムで、頑張って WB 入れれば速くなるぞ、みんなで移行しようね、という方針だったんですが、その移行処置を進めたということですね。Timeはよく使うので対応したかったんですが、よくわからなくて放置していたので対応されてよかったです。

ちょっとした実験

❯ ruby --version
ruby 3.1.2p20 (2022-04-12 revision 4491bb740a) [x86_64-darwin21]

Ruby 3.1 時点では Time クラスは Write Barrier 未導入のクラスである。String クラスは Write Barrier が導入済みである。両クラスのオブジェクトをたくさん作ってみて Write Barrier Unprotected Object の数がどう変化するか見てみた。


irb(main):001> GC.stat
=>
{:count=>24,
 :time=>33,
 :heap_allocated_pages=>150,
 :heap_sorted_length=>150,
 :heap_allocatable_pages=>0,
 :heap_available_slots=>61291,
 :heap_live_slots=>59407,
 :heap_free_slots=>1884,
 :heap_final_slots=>0,
 :heap_marked_slots=>45843,
 :heap_eden_pages=>150,
 :heap_tomb_pages=>0,
 :total_allocated_pages=>150,
 :total_freed_pages=>0,
 :total_allocated_objects=>234419,
 :total_freed_objects=>175012,
 :malloc_increase_bytes=>22992,
 :malloc_increase_bytes_limit=>16777216,
 :minor_gc_count=>20,
 :major_gc_count=>4,
 :compact_count=>0,
 :read_barrier_faults=>0,
 :total_moved_objects=>0,
 :remembered_wb_unprotected_objects=>282,
 :remembered_wb_unprotected_objects_limit=>462,
 :old_objects=>45438,
 :old_objects_limit=>72896,
 :oldmalloc_increase_bytes=>1509072,
 :oldmalloc_increase_bytes_limit=>16777216}
irb(main):002> ary = Array.new
=> []
irb(main):003> 100.times { |n| ary[n] = String.new }
=> 100
irb(main):004> GC.stat
=>
{:count=>40,
 :time=>56,
 :heap_allocated_pages=>150,
 :heap_sorted_length=>150,
 :heap_allocatable_pages=>0,
 :heap_available_slots=>61291,
 :heap_live_slots=>59651,
 :heap_free_slots=>1640,
 :heap_final_slots=>0,
 :heap_marked_slots=>47291,
 :heap_eden_pages=>150,
 :heap_tomb_pages=>0,
 :total_allocated_pages=>150,
 :total_freed_pages=>0,
 :total_allocated_objects=>462873,
 :total_freed_objects=>403222,
 :malloc_increase_bytes=>8928,
 :malloc_increase_bytes_limit=>16777216,
 :minor_gc_count=>36,
 :major_gc_count=>4,
 :compact_count=>0,
 :read_barrier_faults=>0,
 :total_moved_objects=>0,
 :remembered_wb_unprotected_objects=>285,
 :remembered_wb_unprotected_objects_limit=>462,
 :old_objects=>46874,
 :old_objects_limit=>72896,
 :oldmalloc_increase_bytes=>1994128,
 :oldmalloc_increase_bytes_limit=>16777216}
irb(main):005> 100.times { |n| ary[100+n] = Time.new }
=> 100
irb(main):006> GC.stat
=>
{:count=>43,
 :time=>62,
 :heap_allocated_pages=>150,
 :heap_sorted_length=>150,
 :heap_allocatable_pages=>0,
 :heap_available_slots=>61291,
 :heap_live_slots=>59802,
 :heap_free_slots=>1489,
 :heap_final_slots=>0,
 :heap_marked_slots=>47588,
 :heap_eden_pages=>150,
 :heap_tomb_pages=>0,
 :total_allocated_pages=>150,
 :total_freed_pages=>0,
 :total_allocated_objects=>515147,
 :total_freed_objects=>455345,
 :malloc_increase_bytes=>30416,
 :malloc_increase_bytes_limit=>16777216,
 :minor_gc_count=>39,
 :major_gc_count=>4,
 :compact_count=>0,
 :read_barrier_faults=>0,
 :total_moved_objects=>0,
 :remembered_wb_unprotected_objects=>385,
 :remembered_wb_unprotected_objects_limit=>462,
 :old_objects=>47087,
 :old_objects_limit=>72896,
 :oldmalloc_increase_bytes=>1966240,
 :oldmalloc_increase_bytes_limit=>16777216}

remembered_wb_unprotected_objects を見ると現在の Unprotected Object の数が分かる。結果は、String クラスのオブジェクト生成では Unprotected Object は増えなかったが、Time クラスの時は増えた。

初期状態: 282
String 生成後: 285
Time 生成後: 385

Write Barrier 実装 PR

軽く PR を見てみたところ直近のもので EnumeratorTracePoint に Write Barrier が実装されていた。

github.com

github.com

<参加レポ>アーキテクチャを突き詰める Online Conference

Findy 主催のカンファレンスに参加したので感想をまとめる。

モチベーションとしては、関わっているプロダクトが持続的に成長していくためにどういうアーキテクチャがいいのか考えることが多く、そのヒントになるアイデアが得られたら嬉しいなという感じだった。結果としては、とても学びが多く参加できてよかったです。

findy.connpass.com

大きな泥団子に立ち向かうためのソフトウェア設計 本格入門

とても分かりやすくて話がスッと入ってきた。関心の分離を中心テーマとして、それを実現するアーキテクチャや設計技法について紹介していた。ポートアンドアダプター*1というアーキテクチャパターンを例にとり「計算」と「外部」を分離することの重要性と効果を説いていた。ちなみに発表中に「計算」と呼んでいたのは業務ロジックと同義と捉えたけどあってるかな?

「コードの雑音を気にしない開発チームに明るい未来はない」という強めの言葉が印象的だった。本当にそうだと思う。

speakerdeck.com

月間17億レコードを処理する動態管理システムのアーキテクチャ

5秒に1回、何千台(何万台?)ものトラックが位置情報を送ってくる動態管理システムのアーキテクチャ紹介。Kinesis Data Streams で位置情報をストリーミングして Lambda に受け渡す構成を取っている。トラックのエンジンが切れて位置情報の供給が止まったときの補正やスループット改善のための並列化、インデックス追加といった工夫を知れて面白かった。

speakerdeck.com

ビジネスの構造をアーキテクチャに落とし込みソフトウェアに可変性を注入する〜モノタロウ基幹システム刷新の実践例

モノリスな基幹システムのドメインモデリングを進め、分割されたドメインに従ってアプリケーションの実行環境を分離していくという話。モノタロウ規模のアプリケーションでもモノリスを分離していけるというのは勇気をもらえる人が多いのではないだろうか。ドメインモデリングにおいて大切なことは Biz と Dev が一緒になって議論を重ねていくことなんだと学んだ。イベントストーミング、よさそう。

speakerdeck.com

見えないものに着目すると上手くいく、モデリングの勘所

モデリングがうまくいかない原因として「物理的な存在に囚われているから」と強調していた。商品テーブル、ユーザテーブルといった肥大化しやすいモデルがどうして生まれるのか?それは物理的な存在をそのままモデリングしているから。うーん、たしかに。目的ベースで抽象化することで「目に見えない概念」を認識し、適切な粒度(目的に合致した単位)でモデリングすることが重要だと学んだ。つまりは、関心の分離。

speakerdeck.com

ユーザーフレンドリーな取引明細のアーキテクチャVISAカードの複雑性に向き合う実践例〜

クレジットカードに特化した決済システムの枠組みの中で、プリペイドカードでいかにユーザフレンドリな体験を作るかという発表。オーソリやクリアリングといった決済システムの裏側を知れただけでも面白かったが、トリッキーな決済システムの仕様を上手くマスクし、お金のトレーサビリティを下げない工夫に感心した。(お金のトレーサビリティってワードいいな)

speakerdeck.com

その「プロトタイプ」は何のためか

プロトタイピングとは何か、その正体は何であり、なぜ難しいのかを一緒に考えていく内容だった。不確実な世界の中で、仮説を検証し、実証されたら本番投入していく。と流れを書くだけなら単純だが、実際にやってみると目的を見失いやすかったり、いつの間にかプロトタイプが本番稼働するみたいなことが起こるらしい。事業的な仮説精度が低い状態ではプロトタイプを捨てやすい状態に保つことも重要なのだと学んだ。

TODO: スライドが公開されたら貼る

一言

Room A と B を選ばないといけないシステム辛い...どっちも聞きたい...

Ruby コード実行過程を雰囲気で理解する

調べようと思った背景

RubyKaigi2024 に際して現地でより多くの収穫を得るために、言語処理 (CRuby) の前提知識を整理しておきたい。まずは Ruby コードがどのような流れを経て実行されるのか確認する。

参考資料:Rubyのしくみ -Ruby Under a Microscope-

概観

下記資料にて綺麗にまとまっていたので引用する。上段の「CRuby 実行環境」の流れを追っていく。

コード実行の流れ

  1. Ruby コード
  2. 字句解析 (生成物: トークン列)
  3. 構文解析 (生成物: AST ノード)
  4. コンパイル (生成物: バイトコード)
  5. YARV 命令実行

字句解析

Ruby コードが実行されて最初に行われるのが字句解析。ただの文字列のかたまりであるコードに意味を与える。字句解析ではトークン列と呼ばれる「理解可能な単語の列」へと文字列を変換する。

構文解析

次に構文解析トークン列から AST ノードを生成する。このステップでは、パーサコードがトークン列を解析し、Ruby が理解できる文やフレーズにグルーピングする。

コンパイル

構文解析で生成された AST ノードをバイトコードに変換する。バイトコードとは、YARV が解釈できる低級な命令列のこと。

YARV 命令実行

コンパイラが生成したバイトコードYARV によって実行される。

サンプルコードで見てみる

字句解析、構文解析コンパイルまでの流れを実際のコードで見てみる。

字句解析

Ripper.lexRuby コードをトークン列に分割し、そのリストを出力する。

require 'ripper'
require 'pp'
code = <<STR
10.times do |n|
  puts n
end
STR
puts code
pp Ripper.lex(code)
❯ ruby lex.rb                
10.times do |n|
  puts n
end
[[[1, 0], :on_int, "10", END],
 [[1, 2], :on_period, ".", DOT],
 [[1, 3], :on_ident, "times", ARG],
 [[1, 8], :on_sp, " ", ARG],
 [[1, 9], :on_kw, "do", BEG],
 [[1, 11], :on_sp, " ", BEG],
 [[1, 12], :on_op, "|", BEG|LABEL],
 [[1, 13], :on_ident, "n", ARG],
 [[1, 14], :on_op, "|", BEG|LABEL],
 [[1, 15], :on_ignored_nl, "\n", BEG|LABEL],
 [[2, 0], :on_sp, "  ", BEG|LABEL],
 [[2, 2], :on_ident, "puts", CMDARG],
 [[2, 6], :on_sp, " ", CMDARG],
 [[2, 7], :on_ident, "n", END|LABEL],
 [[2, 8], :on_nl, "\n", BEG],
 [[3, 0], :on_kw, "end", END],
 [[3, 3], :on_nl, "\n", BEG]]

10 . times のように文字列を分解し、数値を示す on_int やメソッドや変数名を示す on_ident などの種別を付与していることが確認できる。[n, m]トークンが現れるコード位置を示している。

まとめ: 字句解析は、文字列を意味のある単位に分解し、それぞれに種別をラベリングしている。

構文解析

Ripper は字句解析だけでなく構文解析の結果も出力できる。Ripper.sexpRuby コードを S 式のツリーにして出力する。S 式は木構造データ形式のことのようだ。余談だが、Ruby もアイデアを継承している Lisp では S 式をソースコードの表現としても使うらしい。

require 'ripper'
require 'pp'
code = <<STR
10.times do |n|
  puts n
end
STR
puts code
pp Ripper.sexp(code)
❯ ruby sexp.rb
10.times do |n|
  puts n
end
[:program,
 [[:method_add_block,
   [:call, [:@int, "10", [1, 0]], [:@period, ".", [1, 2]], [:@ident, "times", [1, 3]]],
   [:do_block, [:block_var, [:params, [[:@ident, "n", [1, 13]]], nil, nil, nil, nil, nil, nil], false], [:bodystmt, [[:command, [:@ident, "puts", [2, 2]], [:args_add_block, [[:var_ref, [:@ident, "n", [2, 7]]]], false]]], nil, nil, nil]]]]]

全て見ると大変なので一部を見てみる。

[[:command, [:@ident, "puts", [2, 2]], [:args_add_block, [[:var_ref, [:@ident, "n", [2, 7]]]], false]]]

[:@ident, "puts", [2, 2]] のような配列が構文木の1つのノードを示している。args_add_blockputs の引数とブロックを受け付けるためのノードである。さらに変数 n を参照するためのノード var_ref をネストしている。

まとめ: 構文解析では、与えられたトークン列から構文木を生成しており、構文木は意味のあるまとまり(ノード)を作ってネストして連結することで表現される。

コンパイル

RubyVM::InstructionSequence#disasmRuby コードを YARV 命令列に変換した結果を見れる。

code = <<STR
10.times do |n|
  puts n
end
STR
puts RubyVM::InstructionSequence.compile(code).disasm
❯ ruby disasm.rb
== disasm: #<ISeq:<compiled>@<compiled>:1 (1,0)-(3,3)>
0000 putobject                              10                        (   1)[Li]
0002 send                                   <calldata!mid:times, argc:0>, block in <compiled>
0005 leave

== disasm: #<ISeq:block in <compiled>@<compiled>:1 (1,9)-(3,3)>
local table (size: 1, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
[ 1] n@0<Arg>
0000 putself                                                          (   2)[LiBc]
0001 getlocal_WC_0                          n@0
0003 opt_send_without_block                 <calldata!mid:puts, argc:1, FCALL|ARGS_SIMPLE>
0005 leave                                                            (   3)[Br]

上部がトップレベルで呼び出す 10.times の命令列、下部がブロックで渡される puts n の命令列になっている。異なるスコープは別々の命令列として実行されるらしい。どちらの命令列でもメッセージを送るオブジェクト(レシーバ)が初めに設定されていることが分かる。そして send で オブジェクトに対してメッセージを送っている様子が見て取れる。最後の leave は return 文を示している。

まとめ: コンパイルでは、YARV 命令列が生成される。レシーバの設定やメソッド呼び出しが命令として順番に並べられる。

一言

サンプルコードで処理過程を追うことで雰囲気を理解できた。Ruby 完全理解した。

2024年1~3月に読んだ本

オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方

大変参考になったので記事にした。

sasamuku.hatenablog.com

メタプログラミングRuby 第2版

大変参考になったので記事にした。

sasamuku.hatenablog.com

ChatGPT vs. 未来のない仕事をする人たち

タイトルに釣られて買ってみた。ChatGPT に心はあるのか、というテーマが面白かった。ChatGPT は9歳児相当の心を持っていると評価されている。機械なのだからそれは本当の心ではないと反論できそうだが、人間の心も外から観測不能ブラックボックス)なので、それらしく振る舞っていれば心があると評価できるのもなるほどと思った。

Rubyのしくみ -Ruby Under a Microscope-

メタプログラミングRuby 第2版 を踏まえて読むと、概念が構造として形になる感覚が得られてとてもよかった。例えば、オブジェクトにメソッドはなく、インスタンス変数とクラスへの参照があるだけだ、と説明されて終わるより、構造体(クラスポインタやインスタンス変数の配列)を図として捉える方が理解しやすい。Ruby を通じてコンピュータサイエンスの基本的な概念(ハッシュテーブル、クロージャなど)に触れられるのもよかった。

Web API The Good Parts

厳格な REST から一定の距離を置いたスタンスで記述されているのがよかった。例えば、検索のエンドポイントに search という単語を URI に含めてよいかは、REST 的には NG だが本書ではアリとしている。厳密な REST に従うことが必ずしも正しいとは限らない。エンドポイントの設計、HTTP の仕様、セキュリティなど API 設計に必要なことがある程度網羅されていた。

RSpec 向けの tree コマンドを作った

RSpec の追記やレビューをしていると全体構造を把握するのが辛いときがある。context が根深くなっていたり、単純に行数が長くなっていたり。(そんなテストを書いちゃいかんというのもある)

せっかくなので Gem の勉強も兼ねて CLI ツールを作ってみた。

github.com

こんな感じで使うことができる。

$ rspec_tree all /path/to/your_spec.rb
desc: Sample
desc: First describe
├─────ctx: First context
├───────it: should do something
├───────ctx: First nested context
├─────────it: should do something
├───────it_behaves_like: shared example
desc: Second describe
├─────ctx: Second context
├───────it: should do something else

ちなみに RubyMine だとエディタ上で綺麗に表示する機能があるらしい。

VSCode には OUTLINE という類似機能があるが満足する表示内容ではなかった。

実装

実装は spec ファイルを文字列として受け取り eval を実行している。モンキーパッチも当てまくっておりなかなかにひどい内容になっている。とりあえずは目的に叶うので時間があるときに見直していきたい。

有識者の方から parser gem を使って構文木を作成してみたらどうかとアドバイスをいただいた。コードを実行する必要はなく、テスト構造をツリー形式で出力できればよいだけなのでこちらも試してみたい。

メタプログラミング Ruby を読んだ

感想

書くとき、読むとき、レビューするときに、そのコードの解像度が広がるというか、奥に広がる世界にまで意識が届くようになった。読んでいても知識を押し付けられる感覚がないので、楽しみながら Ruby の奥深さを学ぶことができる。

Ⅰ部

1章 頭文字 M

メタプログラミングとは、コードを記述するコードを記述することである。

C++ のようなコンパイル型の言語では、コンパイルすると変数やメソッドはその実体を失う。コンパイル後にインスタンスメソッドのことをクラスに質問できない。Ruby のようなインタプリタ型の言語では、あらゆる言語要素 (変数、メソッド、クラス等) が実行時にも存在している。irb のような対話的にコードを打ち込めるシェルを使っていると、この辺りは実感としてはある。

Active Record は Movie#titleMovie#director= といったメソッドをこっそり定義している。

class Movie < ActiveRecord::Base
end

Active Record は目に触れる機会の多いメタプログラミングなのかもしれない。attr_reader とかも暗黙的にメソッドを定義しているので身近なメタプログラミングと言えるのだろうか。

2章 月曜日: オブジェクトモデル

オープンクラス

いつでも既存のクラスを再オープンして、その場で修正できる。この技法をオープンクラスと呼ぶ。

オブジェクトの中身

インスタンス変数

class MyClass
  def my_method
    @v = 1
  end
end

obj = MyClass.new # この時点では @v は存在しない
obj.my_method # はじめて @v が存在できる

メソッド

オブジェクトにはメソッドはなく、インスタンス変数とクラスへの参照があるだけだ。

インスタンスそのものにメソッドが定義されているわけではない。ただ、「クラス MyClass が my_method を持つ」というのも誤解がある。この明確な呼び分けとして「インスタンスメソッド」と「クラスメソッド」がある。MyClass#my_methodMyClass.my_method のように表記する。

クラスの真相

Ruby のオブジェクトモデルを学ぶときに最も重要なのは「クラスはオブジェクト」ということだろう。

クラスはオブジェクトであり、クラスにもクラスがある。クラスのクラスは Class である。

String.class
#=> Class

Class クラスのインスタンスにはメソッドがある。

Class.instance_methods(false)
#=> [:allocate, :superclass, :subclasses, :new]

new なんかは分かりやすい。

Array クラスは Object クラスを継承している。つまり「配列はオブジェクトである」と言うことができる。

Array.superclass
#=> Object

Ruby ではあらゆるクラスのスーパークラスは Object になる。Object クラスは to_s のようなあらゆるオブジェクトで便利に使えるメソッドを持っている。

モジュール

Class のスーパークラスは Module だ。

Class.superclass
#=> Module

これはちょっと意外だった。Class はインスタンスを生成して使う、モジュールはインスタンスを生成せずに include して使う、のように使われ方の違いが明確であり、Class は Module であると言われると今一つしっくりこない。

クラスはオブジェクトの生成 new や継承元クラスを確認する superclass などの4つのインスタンスメソッドを追加した「モジュール」だ。[筆者要約]

定数

大文字で始まる参照は、クラス名もモジュール名も含めて、すべて定数だ。

定数と変数の違いはなにか。重要な違いは「スコープ」にある。定数のスコープは独自ルールに基づいている。

モジュールおよびクラスがディレクトリで、定数がファイルだ。

module M
  class C
    X = 'constant'
  end

  C::X
end

M::C::X

Rake の例

module Rake
  class Task
  ...
end

Task のような汎用的な名前の定数が衝突しないよう Rake というモジュールでまとめる。このようなモジュールを「ネームスペース」と呼ぶ。Task の完全な名前は Rake::Task となる。

いろいろまとめると

3章 火曜日: メソッド

  • 静的言語
    • 静的型チェックを持つ
      • すべてのメソッド呼び出しに対して、合致するメソッドをオブジェクトが持っているかどうかをコンパイラがチェックする
      • => コードを実行する前に、コンパイラがミスを指摘してくれる
  • 動的言語
    • 型チェックを持たない
      • メソッドの呼び出しをチェックするようなコンパイラは存在しない
      • => オブジェクトにメソッドが実装されていなくても実行可能

動的メソッド

メソッドを呼び出すというのは、オブジェクトにメッセージを送っていることなんだ。

Object#send をイメージすると分かりやすい。send を使うとメソッド名にシンボルが使える。コード実行時に動的に呼び出すメソッドを決定できる。これを動的ディスパッチと呼ぶ。

define_method を使えば、実行時にメソッド名を決定できる。これを動的メソッドと呼ぶ。

class MyClass
  define_method :my_method do |my_arg|
    my_arg * 3
  end
end

obj = MyClass.new
obj.my_method(2) # => 6

ゴーストメソッド

オブジェクトにメソッドが見つからなければ、元のオブジェクトの method_missing を呼び出す。BasicObject の private インスタンスメソッドにそれはある。

method_missing をオーバーライドすると不明なメッセージを途中でキャッチして振る舞いを変えることができる。

class Lawyer
  def method_missing(method, *args)
    puts "You called: #{method}(#{args.join(', ')})"
    puts "(You also passed it a block)" if block_given?
  end
end

bob = Lawyer.new
bob.talk_simple('a', 'b') do
  # block
end
# =>
You called: talk_simple(a, b)
(You also passed it a block)

この特性をうまく活用して存在しないメソッド呼び出しに「あたかもそのメソッドがあるように」見せる手法をゴーストメソッドと呼ぶ。

動的メソッド vs ゴーストメソッド

可能であれば動的メソッドを使い、仕方なければゴーストメソッドを使う

ゴーストメソッドにはバグが生まれやすい。

4章 水曜日: ブロック

ブロックがスコープを制御するのに強力なツールだってことは、まだ知らないんじゃないかな?スコープというのは、変数やメソッドがどのコード行まで見えるかというものだ。

ブロックの基本

ブロックを定義できるのはメソッドを呼び出すときだけ。メソッドに渡されたブロックは yield を使ってコールバックされる。

def a_method(a, b)
  a + yield(a, b)
end

a_method(1, 2) {|x, y| (x + y) * 3} # => 10

例外が発生しても実行しなければいけない処理をシンプルに書くこともできる。

module Kernel
  def with(resource)
    begin
      yield
    ensure
      resource.dispose
    end
  end
end

# 呼び出し側
r = Resource.new
with(r) do
  # 何かしらの処理
end

ブロックはクロージャ

束縛

ブロックは「コード自体」と「束縛の集まり」の2つから構成される。ローカル変数、インスタンス変数、self といったものが束縛される。ブロックを定義した時点でそこにある束縛を取得し、メソッドに束縛ごと一緒に渡す。

def my_method
  x = "Goodbye"
  yield("cruel")
end

x = "Hello"
my_method {|y| "#{x}, #{y} world" } # => "Hello, cruel world"

x は「ブロックを定義したとき」に束縛される。ブロックからメソッドのローカル変数である x は見えない。

スコープ

local_variables を使ってローカル変数を確認することで、スコープの遷移を追跡できる。このコードでは、「トップレベルのスコープ」「MyClass のスコープ」「my_method のスコープ」の3つを往来している。あるスコープから他のスコープのローカル変数は見えない。

v1 = 1
class MyClass
  v2 = 2
  local_variables # => [:v2]
  def my_method
    v3 = 3
    local_variables
  end
  local_variables # => [:v2]
end
obj = MyClass.new
obj.my_method # => [:v3]
puts local_variables # => [:v1, :obj]

スコープゲート

スコープが変化する場所は3つある。これらはスコープゲート (スコープの出入り口) として振る舞う。

  • クラス定義
  • モジュール定義
  • メソッド

フラットスコープ

ローカル変数はスコープゲートを超えられない。

my_var = "Hello, World!"

class MyClass
  # my_var をここに表示したい

  def my_method
    # my_var をここに表示したい
  end
end

Class.newdefine_method を使えばスコープをフラット化できる。この技法をフラットスコープと呼ぶ。

my_var = "Hello, World!"

MyClass = Class.new do
  puts my_var

  define_method :my_method do
    puts my_var
  end
end

puts MyClass.new.my_method

余談だけど JavaScriptRuby のようにブロックを使わなくても関数自体がクロージャとして働いている。

JavaScript の関数はクロージャとなるためです。クロージャは関数とその関数が作られた環境という 2 つのものの組み合わせです。 クロージャ - JavaScript | MDN

instance_eval

instance_eval はレシーバを self にしてから評価される。スコープは移らないのでローカル変数にもアクセスできる。

class MyClass
  def initialize
    @v = 1
  end
end

v = 2

obj = MyClass.new
obj.instance_eval do
  puts self # => #<MyClass:0x00000001007353b0>
  puts @v # => 1
  @v = v
  puts @v # => 2
end

呼び出し可能オブジェクト

コードを塊として保管しておき、あとから呼び出す方式には以下がある。

  • ブロック
  • Proc
  • lambda
  • メソッド

ブロックはこれまで触れた通り、他の3つを確認していく。

Proc

Proc はブロックをオブジェクトにしたもの。

z = 3
inc = Proc.new { |x| x + z }
puts inc.call(2)  # => 5

lambda

Proc オブジェクトを生成する別の方法。

dec = ->(x) { x - 1 }
puts dec.class # => Proc
puts dec.call(2) # => 1

Proc と lambda は「return の挙動」と「引数チェックの有無」に違いがある。

メソッド

メソッドも Method オブジェクトとして取り出し可能。

class MyClass
  def initialize(value)
    @x = value
  end

  def my_method
    @x
  end
end

obj = MyClass.new(1)
m = obj.method :my_method
puts m.class # => Method
puts m.call # => 1

ブロックや Proc が定義されたスコープで評価されるのに対し、メソッドはオブジェクトに束縛され、オブジェクトのスコープで評価される。

5章 木曜日: クラス定義

Ruby のクラス定義は実際に「コードを実行」している。

クラス定義

カレントクラス

クラス定義の中では、そのクラス自身がカレントオブジェクト self になる。それと同様に「カレントクラス」という概念も持っている。クラス内でメソッドを定義すると、それはカレントクラスのインスタンスメソッドとなる。

  • def で定義される全てのメソッドは、カレントクラスのインスタンスメソッドとなる
  • クラス定義の中では、「カレントオブジェクト self = カレントクラス」となる
  • クラスへの参照があれば class_eval でクラスをフラットスコープでオープンできる

クラスインスタンス変数

クラスは Class クラスのインスタンスであり、インスタンス変数を持つことができる。全てのインスタンス変数はカレントオブジェクト self に属している。クラスも例外ではない。

class MyClass
  @my_var = 1
  def self.read; puts @my_var; end
  def write; @my_var = 2; end
  def read; puts @my_var; end
end

obj = MyClass.new
obj.read # => nil
obj.write
obj.read # => 2
MyClass.read # => 1

このようにクラスに属するインスタンス変数を「クラスインスタンス変数」と呼ぶ。

クラス変数

ちなみに @@ プレフィックスをつけた「クラス変数」もある。クラスインスタンス変数とは異なり、サブクラスやインスタンスメソッドからもアクセスできる。さらに、クラス階層間で共有される特性がある。

class MyClass
  @@v = 1

  def self.read
    puts @@v
  end
end

MyClass.read # => 1

class SubClass < MyClass
  @@v = 2
end

MyClass.read # => 2
SubClass.read # => 2

特異メソッド

特定のオブエジェクトに追加したメソッドを「特異メソッド」と呼ぶ。特異メソッドは、オブジェクトのクラスに影響を与えない。つまりそのオブジェクトにのみ追加される。

str = 'hogehoge'

def str.title?
  self.upcase == self
end

puts str.title? # => false

クラスメソッドはクラスの特異メソッド

特異メソッドの構文は常にこうなる。

def object.method
  # メソッドの中身
end

クラスメソッドもこの構文に漏れない。つまりクラスメソッドはクラスの特異メソッドである。Class クラスのオブジェクトにメソッドを追加している、と言い換えても同じ。

class MyClass; end

def MyClass.read
  puts @v
end

def MyClass.write
  @v = 1
end

MyClass.write
MyClass.read

#### クラスマクロ クラス定義の中で便利に使えるクラスメソッドを「クラスマクロ」と呼ぶ。attr_* 族のようにクラス定義の中でキーワードのように便利に使えるものを指す。

特異クラス

特異メソッドはどこに定義されているのだろうか。オブジェクトはクラスへの参照を持つだけであり、インスタンスメソッドはクラスに定義されているはずだ。

def MyClass; end

obj = MyClass.new
def obj.my_method; end

obj.my_method # MyClass に my_method はない

この答えが「特異クラス」である。

class << an_object という特別な構文を使うことで、特異クラスのスコープに連れて行ってくれる。

obj = Object.new
puts obj.class # => Object

singleton_class = class << obj
  self
end

puts singleton_class.class # => Class

このような手続きを踏まなくても singleton_class メソッドを使うことで簡単に特異クラスを参照できる。

puts obj.singleton_class # => #<Class:#<Object:0x000000010097d5c8>>

特異クラスの特徴

  • Object#singleton_class や class << を使わないと見れない
  • 特異クラスはインスタンスを1つしか持てない
  • 継承ができない
  • 特異クラスはオブジェクトの特異メソッドの住処
  • 特異クラスは継承チェーンの一番下に置かれる

メソッド探索

class C
  def a_method
    'C#a_method()'
  end
end

class D < C; end

obj = D.new
obj.a_method # => "C#a_method()"

このコードのオブジェクトモデルの世界を表すと下図になる。

特異クラスをオープンしてインスタンスメソッドを追加する。

class << obj
  def a_method
    'D#a_method()'
  end
end

obj.a_method # => "D#a_method()"
obj.singleton_class # => #<Class:#<D:0x000000010092cad8>>
obj.singleton_class.class # => Class
obj.singleton_class.superclass # => D

これをオブジェクトモデル図に反映すると下図になる。

特異クラスとクラスメソッド

特異クラスとは特定のオブジェクトに追加されたメソッドが置かれる場所だった。クラスメソッドも同様に Class クラスのオブジェクトに特別に追加されたメソッド、つまり特異メソッドである。

クラスメソッドを上記のコードに追加してみる。

class C
  class << self
    def a_class_method
      'C.a_class_method'
    end
  end
end

C.a_class_method # => "C.a_class_method"

特異クラスとそのスーパークラスを訪ねてみる。

C.superclass # => Object
D.superclass # => C
C.superclass.superclass # => BasicObject
C.superclass.superclass.singleton_class # => #<Class:BasicObject>
C.singleton_class # => #<Class:C>
D.singleton_class # => #<Class:D>
D.singleton_class.superclass # => #<Class:C>
C.singleton_class.superclass # => #<Class:Object>

オブジェクトモデル図にまとめるとこのようになる。

特異クラスのスーパークラスが、スーパークラスの特異クラスになっている。どうしてこんな複雑なことをするのか。それはこう配置することでサブクラスからもクラスメソッドを呼び出せるようになるからだ。

D.a_class_method # => C.a_class_method

説明を付け加えると、クラス D がクラスメソッド a_class_method を実行するとき、それは D の特異クラス #D のインスタンスメソッドである。インスタンスメソッドは継承チェーンを上に登っていく。#D にないのであれば、次に見に行くのは...。

クラス拡張

クラスメソッドをモジュールでインクルードできるか。

module MyModule
  def self.my_method; puts 'hello'; end
end

class MyClass
  include MyModule
end

MyClass.my_method # => NoMethodError

なぜエラーになるかというと、クラスがモジュールをインクルードして得られるのはインスタンスメソッドだからだ。クラスメソッドを得るには、「特異クラスのインスタンスメソッド」にしなければならない。

module MyModule
  def my_method; puts 'hello'; end
end

class MyClass
  class << self
    include MyModule
  end
end

MyClass.my_method # => hello

my_method は MyClass の特異クラスのインスタンスメソッドである。つまり、my_method は MyClass のクラスメソッドになった。この技法を「クラス拡張」と呼ぶ。

わざわざ特異クラスをオープンしなくても、Object#extend を使えばよい。これはレシーバの特異クラスにモジュールをインクルードするためのショートカットである。

module MyModule
  def my_method; puts 'hello'; end
end

class MyClass
  extend MyModule
end

MyClass.my_method # => hello

6章 金曜日: コードを記述するコード

Kernel#eval

コードを文字列として実行して、その結果を返す。

arr = [10, 20]
element = 30
eval('arr << element') # => [10, 20, 30]

Binding オブジェクト

スコープをオブジェクトにして返す。Binding でスコープを取得すれば、そのスコープを持ち回ることができる。eval と組み合わせて後からそのスコープでコードを実行できる。

class MyClass
  def my_method
    @v = 1
    binding
  end
end

b = MyClass.new.my_method

eval '@v', b # => 1

irb は標準入力やファイルをパースして、各行を eval に渡している。Binding を使って異なるコンテキストでも実行できるようになっている。

# workspace.rb
eval(statements, @binding, file, line)

eval vs. block

Kernel#eval と class_eval や instance_eval は、コードを文字列で実行するか、ブロックとして実行するかの違いしかない、というのは誤りである。instance_eval もコードを文字列で評価できる。

ではどちらを使うべきなのか。基本的にはコード文字列を避けるべきである。

コード文字列を避けるべき理由

  • シンタックスハイライトや自動補完が効かない
  • コードインジェクションの標的になる

フックメソッド

クラスが継承されたときや新しいメソッドを獲得したとき、このようなイベントが起きたときに実行されるメソッドを「フックメソッド」と呼ぶ。イベントに「フックをかける」ことからこのように呼ばれる。

Class#inherited はクラスが継承されたときに Ruby が自動的に呼び出してくれる。デフォルトでは何もしないので、オーバーライドして使う。

class String
  def self.inherited(subclass)
    puts "#{self} was inherited by #{subclass}"
  end
end

class MyString < String; end

# Output:
# String was inherited by MyString

クラスのライフサイクルにプラグインする Class#inherited などと同様に、モジュールのライフサイクルにプラグインするものもある。

module M1
  def self.included(othermod)
    puts "M1 was included into #{othermod}"
  end
end

module M2
  def self.prepended(othermod)
    puts "M2 was prepended to #{othermod}"
  end
end

class C
  include M1
  prepend M2
end

# Output:
# M1 was included into C
# M2 was prepended to C

フックメソッドを活用した最終的なサンプルコードはこのようになる。

module CheckedAttributes
  def self.included(base)
    base.extend(ClassMethods)
  end

  module ClassMethods
    def attr_checked(attribute, &validation)
      define_method "#{attribute}=" do |value|
        raise 'Invalid attribute' unless validation.call(value)
        instance_variable_set('@#{attribute}', value)
      end

      define_method attribute do
      instance_variable_get("@#{attribute}")
      end
    end
  end
end

class Person
  include CheckedAttributes
  
  attr_checked :age do |v|
    v >= 18
  end
end

Ⅱ部

9章 Active Record の設計

オートローディング

require 'active_record' したときに読み込まれるファイル。

github.com

Active Record は Active Model と Active Support の2つのライブラリに大きく依存している。Active Support::Autoload モジュールを extend して autoload をクラスマクロとして使用する。これはモジュールを初めて呼び出したときに自動的にソースコードを require するというもの。これにより active_record を require するだけで配下の様々なモジュールを利用できる。

ActiveRecord::Base

ActiveRecord::Base にロジックはなくモジュールを include あるいは extend するだけ。オートローディングの仕組みによって require してからモジュールを include する必要がない。

github.com

ActiveRecord::Validations

ActiveRecord::Base クラスは ActiveRecord::Validations モジュールを include している。valid? メソッドはここで定義されている。

github.com

10章 Active Support の Concern モジュール

ActiveSupport::Concern モジュールがあることで、クラスが include するモジュールにいちいちフックメソッドを定義しなくてよくなる。

Concern 以前の Rails

ActiveRecord::BaseValidations を include すると以下のことが起きる。

module ActiveRecord
  module Validations
    def self.included(base)
      base.extend ClassMethods
    end

    module ClassMethods
      def validates_length_of(*args)
        # ...
      end
    end

    def valid?
      # ...
    end
  end
end

このコードの課題は、モジュールに重複したフックメソッドが定義されること。

class Base
  include Validations
  extend Validations::ClassMethods
  # ...
end

このように書けば同じ目的を達成できる。extend の1行は追加されるが問題ないと思うかもしれない。これにはもっと深刻な問題が隠されている。それは、モジュールを入れ子で include したときに起きる。

module SecondLevelModule
  def self.included(base)
    base.extend ClassMethods
  end

  module ClassMethods
    def second_level_class_method
      "Second level class method"
    end
  end

  def second_level_instance_method
    "Second level instance method"
  end
end

module FirstLevelModule
  def self.included(base)
    base.extend ClassMethods
  end

  module ClassMethods
    def first_level_class_method
      "First level class method"
    end
  end

  def first_level_instance_method
    "First level instance method"
  end
end

class BaseClass
  include FirstLevelModule
end

BaseClass.new.first_level_instance_method # => "First level instance method"
BaseClass.new.second_level_instance_method # => "Second level instance method"

BaseClass.first_level_class_method # => "First level class method"
BaseClass.second_level_class_method # => NoMethodError

second_level_class_methodBaseClass のクラスメソッドではなく、FirstLevelModule のクラスメソッドとなる。

ActiveSupport::Concern

クラスメソッドを定義するためにフックメソッドを定義しなくてもよくなる。

require 'active_support'

module MyConcern
  extend ActiveSupport::Concern

  def an_instance_method; "an instance method"; end

  module ClassMethods
    def a_class_method; "a class method"; end
  end
end

class BaseClass
  include MyConcern
end

BaseClass.new.an_instance_method # => "an instance method"
BaseClass.a_class_method # => "a class method"

ActiveModel::Validations

validateActiveRecord::Base クラスのクラスメソッド (クラスマクロ) として利用される。ソースコードより ActiveSupport::Concern を extend し ClassMethods モジュールに validate メソッドが定義されていることが分かる。

github.com

オブジェクト指向設計実践ガイドを読んだ

オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 | Sandi Metz, 髙山 泰基 |本 | 通販 | Amazon

重要だと思った箇所をメモに残した。 八、九章で力尽きてしまったのでまた気が向いたら読む。

第一章: オブジェクト指向設計

設計とは

設計とは、同一の製品をつくる組み立てラインではなく、アトリエなのです。

原則

  • SOLID 原則
    • 単一責任 (Single Responsibility)
    • オープン・クローズド (Open-Closed)
    • リスコフの置換 (Liskov Substitution)
    • インターフェース分離 (Interface Segregattion)
    • 依存性逆転 (Dependency Inversion)
  • DRY (Do not repeat yourself)
  • LoD (Low of Demeter)

手続き型言語との対比

手続き型言語は、データと振る舞いは完全に別物になっているのが特徴。Ruby はデータと振る舞いを1つのオブジェクトにまとめる。Ruby は文字列もオブジェクトであり、言語構文に組み込まれているわけではない。Ruby ではプログラミング言語に期待されるデータ型の全てに対し、前もってクラスが用意されている。

まとめ

オブジェクト指向の目的は「変更を容易にすること」にある。よい設計は、理論を実践に変換する能力にかかっている。

第二章: 単一責任のクラスを設計する

凝集度

クラス内の全てがそのクラスの中心的な目的に関連していれば、「凝集度が高い」と言える。

DRY

DRY はただ同じ記述を繰り返すな、と言っているわけではない。単一責任のクラスを実現すればどのような振る舞いもただ1箇所にのみ存在するようになる。原則を満たした結果として表出するのが「DRY という状態」であり、DRY にすること自体が目的ではない。

変更を歓迎するコード

  • インスタンス変数の隠蔽(カプセル化
    • 「クラス自身からインスタンス変数を隠蔽する」のはインスタンス変数の直接参照にリスクがあるから
    • 直接参照は変更に弱いためラッパーメソッドで抽象化し、必要な知識を最小限に留めるが目的だと解釈
  • カプセル化とは
  • 本書内における「メッセージを送る」とは
  • データ構造の隠蔽
    • 複雑な構造への直接参照は混乱を招く
    • Ruby の Struct は配列などのデータ構造に関する知識(とりわけどのインデックスにどのデータがあるなど)を剥がすのに便利
    • Struct とは
      • 新しいクラスを作るほどではないが、いくつかの属性を1つに束ねておくのに便利

第三章: 依存関係を管理する

依存関係とは

オブジェクトとオブジェクトの間に生まれる関係の1つ。A が B のことをどれだけ知っているか、それを知っていることは適切なことなのか、を考える必要がある。A を変更するとき、B も変更しなければいけないなら、B は A に依存していると言える。

疎結合なコードを書く

  • 依存オブジェクトの注入
    • オブジェクトのクラスではなく「送ろうとしているメッセージ」こそが重要!!
    • この視点の逆転こそがオブジェクト指向の真髄
    • これがダックタイピングに通ずる道になる
    • 特定の振る舞いを持つ任意のオブジェクトであれば誰とでも共同作業できる

依存方向の選択

自身より変更されないものに依存しなさい

第四章: 柔軟なインターフェースをつくる

インターフェースとは

クラス内にあるメソッドのこと。パブリックインターフェースは外部から呼ばれることを想定して公開しているメソッドを指す。

他の意味のインターフェースとして、要求されるメソッドを実装するクラスはどんなクラスであれその「インターフェース」のように振る舞うというものがある。これは「型」としてのインターフェースであり、ダックタイピングを扱う上で重要な概念になる。

インターフェースの定義

レストランとお客さんの例が分かりやすい。厨房では多くのことが行われるが、お客さんに公開されるのはメニューだけ。どの料理を頼むかだけを指定すればよく、それが中華鍋で作られるのか、レンジで作るのかは知る必要がない。お客さんが「料理の仕方」を知ってしまうとき、料理方法が変わったらお客さんにも訂正しないといけなくなる。

パブリックインターフェース・プライベートインターフェース

パブリック | プライベート

  • クラスの
    • 主要な責任を明らかにする | 実装の詳細に関わる
  • 外部から実行され
    • る | ない
  • 変更され
    • にくい | やすい
  • 依存するのは
    • 安全 | 危険
  • テストで
    • 文書化される | されないことが多い

コンテキストを最小限にする

パブリックインターフェースを構築するときは、そのパブリックインターフェースが他者に要求する「コンテキストが最小限」になることを目指す。

第五章: ダックタイピングでコストを削減する

ダックタイピングとは

インターフェースで定義したメソッドを持つオブジェクトはその型として扱う(それが本当は何であれ)。重要なのは「何であるか」ではなく「何をするか」なのだ。これが冒頭にもあったオブジェクトの「クラスではなくメッセージが重要だ」という主張につながる。

隠れたダックを認識するために

以下が出てきたらダックタイピングを導入する余地があると考える

  • クラスで分岐する case 文
  • kind_of?
  • respond_to?

具象的なコードの危険性

具象的なコードは理解するのは簡単だが、拡張するにはコストを伴う。いつだって抽象は分かりにくいが、その拡張性は大きな力になる。

ダックを信頼する

オブジェクトを信頼する。信頼に足るオブジェクトを設計するのが設計者の仕事。

第六章: 継承によって振る舞いを獲得する

継承とは

本質的には「メッセージの自動委譲」の仕組みと言える。オブジェクトが理解できなかったメッセージの転送経路を定義するもの。メッセージの自動委譲によるコード共有方法には「モジュール」もある。

スーパークラスの作り方

継承のルール

  • オブジェクトが「一般 - 特殊の関係」になっている
  • 正しいコーディングテクニックを使っている

正しいコーディングテクニックとは

スーパークラスとサブクラスを疎結合にする

「フックメッセージ」を作る。フックメッセージは、サブクラスがぞれに合致するメソッドを実装することで、情報を提供できるようにするための専用のメソッド。具体的にはスーパークラスでメッセージ送信と実装の両方を行い、サブクラスで実装をオーバーライドする。サブクラスはスーパークラスについて知るべきことを少なくできる。サブクラスは実装したメソッドが何らかのオブジェクトによって、何らかのタイミングで呼び出されると想定するだけでよい。サブクラスに必要なのはテンプレートメソッドを実装するだけ(申請フォームに必要事項を記入するかのように)。

第七章: モジュールでロールの振る舞いを共有する

モジュールとは

Ruby において、ある振る舞いをオブジェクト (クラスやクラスのインスタンス) に混ぜ入れる方法のこと。メソッドの集合に名前をつけてグルーピングできる。ダックタイピングがメソッドのシグネチャを共有するのみだったのに対し、一箇所に定義された特定の振る舞い (多くの場合は複数のメソッドから成る) をオブジェクト間で共有することができる。

クラスとモジュール

「である (is-a)」「のように振る舞う (behaves-like-a)」の違い。クラスは揺るぎないが、モジュールは役職(ロール)のように取り外し可能。

extend

include がクラスにメソッド探索の経路を追加する (つまり応答できるメッセージが増える) のに対し、extend は何をしてくれるのか。extend はモジュールの振る舞いをオブジェクトに直接追加する。クラスをモジュールで extend すると「そのクラス自体に」クラスメソッドとして追加される。クラスのインスタンスを extend すると「そのインスタンス自体に」インスタンスメソッドとして追加される。これはクラスも単なるオブジェクトに過ぎないことを表す。

リスコフの置換原則 (LSP)

SOLID 原則の「L」。スーパークラスが使えるところではサブクラスが使えるという原則。派生型は上位型と常に置換可能であるということ。

第八章: コンポジションでオブジェクトを組み合わせる

後で書く。

第九章: 費用対効果の高いテストを設計する

後で書く。