『正しいものを正しくつくる』を読んだ

会社の人がおすすめしていた正しいものを正しくつくるを読みました。その読書感想文です。

本書のざっくりとした主張としては、仮説検証を繰り返すことによって「正しいもの」つまりユーザーにとって価値のあるプロダクトを探索し、それを「正しく」つまり不確実性をコントロールしやすいようにアジャイルに作りましょうということ。特に後者のアジャイルな開発についてはフレームワークとしてスクラムが厚く紹介されています。また、帯に「あるいはアジャイルのその先について」とあるように、素朴にアジャイル開発を適用した際の困難とその対応策が議論されています。本書の貢献は特にこの「アジャイルのその先」についての議論でしょう。その中で、自分の体験とも絡む特に印象に残った議論を少し掘り下げます。

問題の出発点は、スクラムにおけるプロダクトオーナーに機能が集中しすぎており、そこに期待を押し込めて分断してしまってはチームのパフォーマンスが頭打ちになるというところです。その状況を乗り越えるためには、開発チームがプロダクトオーナーの世界に「越境」していく必要がある、そして越境のカオスをコントロールするためにプロダクトに関する共通理解を育てましょう、共通理解を作るための手段として仮説検証を繰り返すことが有効だろう、というように話が進みます。このストーリーにはかなり同意します。自分の日々の暮らしの中で、うまくいっていないこと、うまくいったことが綺麗に言語化されて、良い本を読んだなという感触があります。

まずPOと開発チームの分断には心当たりがあります。各々がまずはスクラムにおけるロールを理解し、それを実践しようとするフェーズにあるとき、この状態に陥りやすいのではないかと感じました。誰が何をするべきかを指差し確認して理解しようとする過程で、過剰なセクショナリズムが生まれがちなのだろうと思います。

この問題意識に対する「越境」という方針に大変強く同意します。この本で僕が一番好きな部分です。短いエンジニア経験の中でうまくいったなと思うプロジェクトでは、確かにこの「越境」がありました。POならこう考えるだろうなというように、POの考え方を自然にトレースするようになり、プロダクトのコアに関わる判断や提案をスムーズに行うことができたと感じる期間があります。まさに共通理解によって自己組織化された状態です。スクラムを次のレベルに進めたいと思った時、「越境」という標語はかなり使えそうだなと思います。

最後の共通理解を育てることについては様々な方法があると感じました。そもそも何が課題かわからない、どう解決したら良いかわからない、つまり「正しいもの」の部分が未達の状態で共通理解を育てるには、確かに本書で述べられている仮説検証のプロセスを共に行うのが有力な手段だと思います。一方で、「正しいもの」がある程度固まっている場合は、別なコミュニケーションがありえるでしょう。自分が体験したのは、POがプロダクトの設計理念やユーザー行動を饒舌に話してくれるというケースです。デイリースクラムなどで仕様の質問や確認をすると、必ず「なぜ」を一緒に話してくれました。「ここのユーザーさんは〇〇な傾向が強いから是非こうしたい」「サービスとして〇〇が大事だからここは絶対外したくない」と言った具合です。こういう問答を数ヶ月繰り返すと、少しずつPOの考え方が頭に染み込んできます。そうすると、POが普段言っていることと違う仕様を発見して改善提案をしたり、どうしてもスコープを減らす必要がある時に筋よく落とすタスクを提案したり、といった高度な判断ができるようになっていきました。本書で述べられている「越境」はもっと積極的なPOの世界への関与を述べていそうなので、自分の体験は少し弱いかもしれませんが、段階的な越境の手段としては有用ではないかと思います。

また、本書では「目的」つまり「なぜそれをやるのか」の部分をとても大事にしており、その点も最近の自分の考えと一致しています。自己組織化の鍵は「良い目的が良いアクションを引き出す」だと個人的に考えており、本書でもそれに沿った主張がいくつか見られました。共通理解に基づく越境は最たるものです。プロダクトの目的を深く理解することによって、POの世界へ越境するアクションが自然と起こる、という体験をしていきたいし作っていきたいですね。本書で厚く議論されている仮説検証のプロセスと、それをつくる側と連携させる部分については、まだ自分ごとに落とし込めていないので、そこをもっと掘り下げて越境のための道具を増やしていけたら良いなと思っています。

GraphQL API を悪意あるクエリから守る手法

実サービスで GraphQL API をインターネットに公開する際は、悪意あるクエリに対する防衛が欠かせません。この記事における「悪意あるクエリ」とはサービスに意図的に負荷をかけるクエリのことです。GraphQL では 、木構造再帰的な構造を利用して、一回のクエリで容易に数百万・数千万件のデータを取得することができます。そのようなクエリを実行してしまうと、アプリケーションサーバーや、その後ろにいる別のサービスに甚大な負荷がかかります。これは攻撃者からしてみれば恰好の的で、なんらか対策を講じる必要があります。

幸いこうした問題はよく知られており、クエリを静的に解析するライブラリがいくつか存在します。しかし、そうしたライブラリをどう使うかといったことはあまり議論されておらず、効果的な対策を行うのは依然として難しい状況だと感じます。この記事では、典型的な負荷の高いクエリとその具体的な対策を紹介し、悪意あるクエリから GraphQL API を守る知見を展開したいと思います。

サービスに意図的に負荷をかけるクエリ

ここでは典型的な負荷の高いクエリを3種類紹介します。簡単のためデータは RDB から取得するということにしましょう(現実には別の API など様々なデータソースが想定されます)。

データを大量に取得するクエリ

GraphQL では、以下のようなクエリで簡単に大量のデータを取得することができます。firstSQL でいう limit で、取得する件数を指定する引数です。もしデータが十分に存在していたら、このクエリで 100 countries × 100 cities x 1000 streets = 1000万ノード 取得することができます。

{
  countries(first: 100) {
    name
    cities(first: 100) {
      name
      streets(first: 1000) {
        name
      }
    }
  }
}

重い計算をさせるクエリ

以下のクエリの superComplexStatistics は非常に重い統計値を計算するフィールドです。仮に1回の計算に1秒かかるとしましょう。すると、このクエリでは city を 10,000 件取得するので、superComplexStatistics を 10,000 回計算することになります。これだけで 10,000 秒(約3時間)かかります。

{
  countries(first: 100) {
    cities(first: 100) {
      superComplexStatistics
    }
  }
}

タイムアウトを設定することで多少は問題を回避できますが、タイムアウトに達するまで CPU を使い続けるようなクエリを何度も実行するのは避けたいものです。

大量にDBアクセスを発生させるクエリ

GraphQL ではデータ同士に関連がある場合は実体を結びつけるのが良い、つまり RDB 上で city が country の id なりを持っていなるなら、country の実体を引けるようにする方が良いとされています。この方針に従ってスキーマを設計すると、country --> cities[0] --> country --> cities[0] ... といったように無限に循環する構造を作ることができます。これを利用すると以下のようなクエリを書くことができます。

{
  countries(first: 1) {
    cities(first: 1) {
      country {
        cities(first: 1) {
        ...(これがあと996段続く)
        }
      }
    }
  }
}

基本的に GraphQL サーバーは、あるフィールドを resolve して、そのあと子フィールドを resolve するというように動きます。つまり素朴に実装するとネスト1段ごとに1回 DB アクセスが発生することになります。この例だと、データ量こそ大したことはないですが、ネストが 1000 段あるので DB アクセスが 1000 回発生することになります。アプリケーションレイヤーでのキャッシュなどを工夫すれば問題を回避できる場合もありそうですが、一般の場合に備えるのは難しいでしょう。

悪意あるクエリへの対策

GraphQL API がインターネットに公開されている限り、上述のようなクエリによって GraphQL サーバーや DB が負荷にさらされるリスクは常に存在します。こうしたクエリに対する基本的な対応方針は、クエリを実行前に解析し負荷の問題がない場合に制限して実行するというものです。この記事では「ホワイトリストによる制限」と「complexity による制限」の2つの対策を紹介します。

ホワイトリストによる制限

これは、GraphQL API が受け付けるクエリ一覧をホワイトリストとして持っておいて、クライアントから送られてきたクエリがそれに一致する場合のみ実行するやり方です。この手法のメリットとしては、導入が簡単であることが挙げられます。クエリが文字列として一致するかを検査するだけでよいため非常に簡単です。また、導入が簡単である割に非常に高い効果が期待できます。実際にクライアントで使うクエリのみを実行するため攻撃の余地がありません。一方で、デメリットもいくつかあります。ひとつは、ホワイトリストのメンテナンスコストです。クライアント側を更新するたびにホワイトリストを更新する必要があり、なんらか仕組みが必要になります。もうひとつのデメリットは、そもそも適用できる場面が限られることです。まず、公開 API ではユーザーが自由にクエリを書ける必要があるためホワイトリストは使えません。また、クライアントが複数あったり、クライアントの開発が別チームで行われていたりすると、ホワイトリストの運用は格段に難しくなります。次に紹介する complexity による制限のように、サーバーサイドで完結するほうが簡単である場合もあるでしょう。

complexity による制限

これは、クエリの負荷に比例して大きくなる数値(=complexity)を計算し、その数値が閾値に収まっている場合のみ実行するやり方です。任意のクエリに対して適用できるので、ホワイトリストによる制限が使いづらい場合は、まず complexity による制限を検討することになるでしょう。以下では典型的な負荷について complexity の例を紹介します。

まずはデータ量に関する complexity です。最も素朴なのはノード数を見積もることです。例えば以下のクエリではノード数は 50 countries + 50 × 100 cities = 5050 nodes となります。この値が一定以下の場合のみ実行することにすれば、異常なデータ量を処理してしまうことはなくなります。

{
  countries(first: 50) {
    name
    cities(first: 100) {
      name
    }
  }
}

ノード数を計算する場合、リスト(Relay の言葉では Connection)を取得するフィールドで、first のような件数が分かる引数が必須になっていることが重要です。こうした引数なしに100件固定で取得するフィールドなどが存在すると、そこを起点に大量にデータを取得できてしまう可能性があります。後述する cost ディレクティブなどを使えば対応は可能ですが、当然、クエリ解析の複雑さは増します。GitHub GraphQL API v4 では、まさにこうしたノード数による制限を行っており、かなり妥当な方針であると考えて良さそうです。

次は計算量に関する complexity です。典型的なやり方は、スキーマ側に directive を使って計算コストの情報を付加するというものです。例えば、以下のようにして計算量の多いフィールドに directive をつけることが考えられます。この場合は superComplexStatistics にコスト10を与えています。

type City {
  name
  superComplexStatistics @cost(n: 10)
}

GraphQL サーバーはこの情報をもとに complexity を計算します。例えば以下のようなクエリの計算量は (100 × 100 cities) × 10 cost = 100,000 と計算できます。

{
  countries(first: 100) {
    cities(first: 100) {
      superComplexStatistics
    }
  }
}

このディレクティブを使った制限は用途が広く、こうした計算量だけでなく、DBアクセス回数や、first などの引数を取らない全件取得フィールドの負荷を表現するのにも使えます。

それ以外の complexity として、クエリの文字列長やネストの深さがよく話題に上りますが、一般には負荷に比例しない complexity で、あまり筋の良い方針ではないと考えます。自分のスキーマを眺めてみて、文字列長や深さによって悪意あるクエリを制限できているかをよく検討する必要があるでしょう。スキーマによってはこうした簡単な指標で十分な場合もあり、選択肢としてないわけではありません。

実装と課題

complexity による制限を行うための実装はいくつか存在します。例えば JavaScriptgraphql-cost-analysis や Go の gqlgen が備える complexity limit があります。ライブラリが存在しない言語でも、クエリ解析のみをマイクロサービスとして切り出して、サポートの厚い言語で実装するといったことが考えられます。

しかし、そうしたライブラリのサポートがあってもなお、complexity による制限には多くの課題があります。ひとつは、complexity 設計のベストプラクティスが十分に蓄積されていないことです。本記事ではノード数・計算量といった典型的な負荷にフォーカスして対策を紹介しましたが、そもそもこのように掘り下げて議論している資料からして多くありません。自分で一から悪意あるクエリの対策を検討してみると、十中八九、無数の細かいデザインチョイスに苦しむことになるでしょう。また、本記事ではノード数・計算量・DBアクセス回数といった複数の complexity を紹介しましたが、それらを併用する風潮はあまりなく、complexity がひとつの場合を厚くサポートする実装が多いのが現状です。そうなると、必然的にひとつの complexity の中に複数の尺度を押し込むことになり、コストや閾値の設定が格段に難しくなります。DBアクセス1回はノード数いくつ分にあたるでしょうか。ある重い計算はDBアクセス何回分でしょうか。それらが複合したときの適切な閾値はいくつでしょうか。今のところ、こうした値を実測値などに基づいて無理やりこじつけていく必要があります。graphql-cost-analysis は複数の complexity による制限が可能な設計にはなっており、より厚いサポートが期待されます。

まとめ

この記事では、攻撃として利用できる典型的な負荷の高い GraphQL クエリを挙げ、そうしたクエリを判別するための静的解析の手法を2つ紹介しました。ひとつはホワイトリストによる制限です。単にクエリ文字列の一致を見るだけで良く、効果も高いですが、使える場面が限られます。もうひとつの手法は complexity による制限です。任意のクエリに適用可能ですが、ベストプラクティスが確立されていない面があり、効果的な対策を行う難易度は高いです。今後のライブラリの発展が期待されます。

最後に補足として、本記事で紹介したような対策は必ずしも完璧に行う必要はありません。そこまで高い安全性が必要でなければ、特に危険なノード数に限って complexity による制限を行うといったことも考えられます。そもそも対策を全く行わず、悪意あるクエリを送ってくるクライアントを ban するだけで事足りるかもしれません。最も重要なのは、リスクと対策の全体像を把握した上で要件に合わせた手法を採用することです。

エンジニア8人チームで"効果的に"タスクをアサインするために検討した8つの軸

最近、締め切りのある大きめなプロジェクトでWebアプリケーションエンジニア兼プロジェクトマネージャーとして仕事をしました。一年目なので当然プロジェクト管理の経験はなく、本を読んで知識を得たり、チームメンバーに助けられたりと、だいぶ手探りでの挑戦となりました。その中でもっとも難しかった仕事の一つとして、タスクの効果的なアサイがあります。

エンジニアは最大8人おり、その技術力、ドメイン知識、勤務地などは多岐に渡ります。ウォーターフォール的な開発だったため、タスクは事前に洗い出されており、タスク管理ツール上に無数に登録されていました。適当に人とタスクを辞書順でソートしてアサインするだけなら簡単ですが、現実はそうもいきません。締め切りは厳しく、チームの生産性を少しでも高く維持しなければいけません。メンバーのモチベーションが下がったり、依存の多いタスクが遅れたりといったことはなんとしても避けたいものです。この難しい課題を乗り越えるために、私はタスクと人の両面をよく理解することから始め、その知識に基づいて、パズルを解くようにしてアサインを決めました。本記事では、その際に検討した8つの軸をまとめます。

8つの軸は「タスク優先度」と「人」の2側面に大別されます。まずは「タスク優先度」から見ていきましょう。

タスク優先度に関する2つの軸

タスクを消化する順番は非常に重要です。これを間違えると、作業が止まってしまったり、あとから複雑な要件が明らかになったり、プロジェクトの安定した進行を脅かします。そうした問題を回避するために、今回のケースでは大きく分けて「依存関係」「不確実性」の2つを検討しました。これ以外にもプロダクトオーナーの要請などで優先度が変化することはありますが、支配的な要因ではありませんでした。

依存関係

Webアプリケーションの機能には、当然ながら依存関係があります。Aという機能を作ってからではないとBという機能を作りづらい、といった事情が無数にあります。これによって、先に着手しておかないと困るタスク群というのが現れます。これを、担当者がなるべく依存関係を気にしないで済むように‥‥最低でも依存関係によって作業が止まることのないようにできれば、チームの生産性は上がるはずです。

「精査して気をつける」以上の取り組みとしては以下のようなものがありました。

  • もしどうしても依存のあるタスクを並行して進める必要がある時は、両方の担当者に対して情報を共有し、お互い連携しつつ進めてもらうように依頼する
  • チームメンバーの「依存を解決できる最小のタスクを作って進めると良いのでは」という提案に基づいてタスクの分割の仕方を変えた

また、今回のプロジェクトでは、機能ができないとデザイナーが稼働しづらいという依存関係もあり、デザイナーの稼働率を上げるために画面に関連するタスクを優先的に消化する必要もありました。

不確実性

要件や実装方法が明らかではないタスクは、実装にかかる期間を見積もりづらくリスクが高い状態で、早めに着手するのが望ましいとされています。私が今回携わったプロジェクトでは、既存システムをそのまま利用できない「新機能」の枠がこれにあたりました。幸い、チームのベテランエンジニアが、割と早い段階で「新機能は危ない」と声を上げてくれており、事前にタスクにタグ付けをしていたため、容易に一覧して検討材料とすることができました。不確実性の高いタスクの注意点として、期間だけではなく、難易度も不確実であるということがあります。調査をしてみたら異常に難しい、よくある解法を知らないと難しいといった可能性があり、誰をアサインするかは注意深く検討しました。

人に関する6つの軸

個人の特性に合わせてタスクをアサインすることは非常に重要だと感じます。全員に興味と能力にマッチしたタスクをアサインできれば、チームの生産性は間違いなく向上するでしょう。モチベーションの向上に伴ってチームの雰囲気も上向くでしょう。全員のタスク完了ペースも揃いやすいので、ベロシティを安定して計測できるといったことも考えられます。こうしたメリットを狙って、人に関する軸として「技術力」「ドメイン知識」「興味・得意分野」「成長機会」「コミュニケーション」「自分」の6つの軸を検討しました。

技術力

これは言うまでもなく重要な軸です。チームメンバーは全員優秀で、誰にどのタスクをお願いしたとしても、どうにかして完遂してくるだろうという信頼はありました。しかし、締め切りに追われる状況下では「安定して」「筋よく」各タスクが完了するのが望ましいです。止むを得ない側面もあり、技術的に難しいタスクは、経験のある技術力の高いメンバーにアサインするというのを基本方針としました。しかし、これには若手の成長機会が失われるといったデメリットもあり、後述する他の軸も検討してバランスを取るように心がけました。

ドメイン知識

これは「技術力」と同じ、エンジニアのシンプルなパワーに関する軸で、やはり重要です。今回のプロジェクトでは、既存システムの改修やつなぎ込みが必要な部分があり、その既存システムを理解していないと難しいタスクがいくつかありました。そういったタスクを社歴の浅い人にアサインするのは、スピード重視の状況下ではあまり筋が良いとは言えません(一般にはドメイン知識を展開するメリットを考慮するべきでしょう)。過去のプロジェクトの話を少し聞いてみたりして、よりドメイン知識がマッチする人にタスクをアサインできないか検討しました。

興味・得意分野

締め切りに追われる日々だとしても、毎日の仕事が楽しいに越したことはありません。一人一人がやりがいを感じて働けることは、他の人にも良い影響を与えるでしょう。そうした観点から、興味や得意分野を把握できている人には、なるべくそれに近いタスクをアサインするように心がけました。とはいえ、急ごしらえの大きなチームでメンバーの嗜好を把握しきれなかったり、他の軸を優先せざるを得ない場面も多かったりと、なかなか上手くハマる場面は少なかったという印象です。次に大きなプロジェクトをやるときに、重点的に考慮したい軸の一つです。

成長機会

この軸については、id:t_wada さんの こちら の発表が大変印象に残っています。

では、品質と速度についてのトレードオフが意識されるとき、実際には何と何が秤にかけられているのか。

(中略)プロダクトの品質を支えるために必要なメンバーの成長とその成長のために必要なフィードバックや学習の時間が秤にかけられているのではないかと思う。

タスクのアサインを検討する中で、チームの生産性を向上させるための最も素朴な最適化は、難しいタスクをできる人にアサインすることでした。しかし、それを続けていては知識は展開されませんし、時間があればアサインできていたはずの若手の成長機会が失われます。今回のプロジェクトでは、興味・得意分野が上手くハマる場合に少し挑戦的にアサインをしてみたり、一時的にベテランと若手の2人をアサインしてペアプロをお願いするなどして、ささやかな抵抗を試みました。これはまさに「抵抗」という言葉がぴったりで、締め切りによって失われたエンジニアとしての楽しみを、少しでもチームに取り戻すという気持ちで検討しました。

コミュニケーション

誰をアサインするかによって、タスクを進める上でのコミュニケーションの難易度は変化すると考えます。例えば、社歴の浅いメンバーにとって、チーム外調整が必要なタスクは相対的に難易度が上がります。上で述べたペアでのアサインでも、勤務地が違う2人をアサインするより、同じ勤務地でペアプロをしやすい2人をアサインした方が簡単になります。調整が可能な場合は、こうした細かい最適化も検討しました。

自分

これは、いわゆるプレイングマネージャーとして取り組んでいる場合に特有の検討事項でした。自分は、いちエンジニアとして作戦を考えて、時には他のエンジニアに厄介な仕事をお願いする立場です。しかも、やろうと思えば自分に一番面白いタスクをアサインすることすらできてしまいます。そういう立場で、いかにチームメンバーからフォロワーシップを引き出せるかが重要だと感じていました。そのために、自分がラクをしないことを心がけました。厄介な新機能や、チーム外との調整が必要になるタスク、クリティカルパスに関わる割り込みを積極的に取って、1人のプレイヤーとして、締め切りに対して誠実に行動していることを示せるように心がけました。幸いチームメンバーは優しい人ばかりだったので、実際のところフォロワーシップの心配はありませんでした。しかし、そこにあぐらをかくわけにはいきません。これはプライドの問題です。

まとめ

以上、効果的なアサインを考えるために検討した8つの軸を紹介しました。どの軸を重視するかといった問題も非常に難しく、プロジェクトの状況に応じて柔軟に検討するようにしました。プロジェクト自体はなんとか成功しましたが、アサインミスで特定の人の負荷が上がってしまったり、そのしわ寄せによってチームの他のセクションが不安定になってしまったりと、色々と反省ポイントはありました。また、今回の議論は、場当たり的に必要なものを挙げていった結果生まれたもので、先人の議論を十分には参考にできていません。また、全ての軸を効率よく検討する手法も確立できていません。より再利用性の高いフレームワークとなるように、今後も改善を続けていきたいと思います。

コラム: タスクをアサインすることの是非

一般的なアジャイル開発においては、スプリントプランニングで人にタスクをアサイしないのが良いとされています。タスクの属人性を排除することで、チームとして仕事を進められるようになり、結果として助け合いなどが起こり安定した生産性を発揮できるようになるという考え方です。しかし、今回はプロジェクトがあまりにも難しく複雑でした。アサインをすべて決めることで、属人性すら最適化の道具として利用し、チームの生産性をシビアに議論する必要がありました。それがチームにとって最良の状態ではないという意識は常にあり、プロジェクトの不確実性が減少するにつれて、少しずつチームメンバーにタスクを選んでもらう形式も取り入れました。プロジェクトの性質やチームの状況に合わせて、スプリントプランニングの方法を検討できると良いのでは、というのが所感です。

見積もりの基礎知識と「ストーリーポイント vs 理想日」の考察

昨年の10月頃から締め切りのある大きなプロジェクトに参加し、部分的にではありますがプロジェクトマネジメントを担当しました。この記事では、その業務を通して得た見積もりに関する知見をまとめます。教科書的な知識は「アジャイルな見積もりと計画づくり」に依りますので、詳しくはそちらをご参照ください。

見積もりの基礎知識

見積もりは、提供したい機能をどれくらいの期間で実装できるかを予想し、計画について議論をするために行います。見積もりを行うことによって、全くアタリがつかない状態から、「プロジェクトのフェーズがこの辺りなので、平均で N 週間、最悪で 2N 週間くらいかかる可能性があります」などと答えられるようになります。さらに、プロジェクトのフェーズが進んで不確実性が減少すれば「平均で M 週間、最悪で M+1 週間くらいかかります」と答えを正確にできるかもしれません。まさにこれが「アジャイルな計画づくり」です。

見積もりのベストプラクティスは「期間ではなく規模を見積もること」であると言われています。ある機能があったときに、直接「2週間くらいかかります」と答えるかわりに、機能の大きさを表す数字(ポイント)を答えるのです(例えば「5くらいです」と)。この数字は相対的な値です。どういうことか理解するために、試しに4つほどタスクを見積もってみましょう。世界で最初の機能A は、比較対象がないので、ひとまず「2」という適当な値を与えておきます。次の機能B は、Aと似ていますが、Aに比べると倍ぐらいの画面が必要そうです。よって倍の「4」を与えます。次の機能Cは、Aよりは大変そうですが、Bよりは簡単そうです。間の数字である「3」を与えましょう。最後の機能Dは、Aと同じような機能です。Aと同じ「2」を与えましょう。‥‥このくらいやれば、お分かりいただけたのではないでしょうか。ある機能の見積もりは、過去に見積もった機能と比較した相対的な大きさによって決めるのです。

このように規模の見積もりを行った状態で1ヶ月ほど開発を進めれば、チームが単位期間(例えば1週間)あたりに完了できるポイント数を予想できます。これをベロシティと呼びます。あとは簡単で、提供したい機能の総ポイント数をベロシティで割ると、プロジェクトの完了にどのくらいの期間が必要か予想することができます。規模の見積もりを期間の見積もりに変換することができました。

なぜこんな回りくどいことをするのでしょうか。いくつか理由はあります。ひとつは、期間の見積もりをベロシティという実績値から導出することで、見積もりのミスが自動的に補正されるメリットがあります。実際にかかる期間がわからなくても、機能の相対的な規模さえ正しく認識できていれば、自動的に信頼できる期間の見積もりを得られるのが優れたポイントです。もうひとつ理由をあげるなら「期間の見積もりは人によって違う」からです。「〇〇さんがやるとしたら1週間ですね」「□□さんがやるなら2週間ですね」といった意見が乱立してしまっては、話をまとめることができません。仮にある人にアサインする想定で期間の見積もりを与えたとして、実際に着手するときにその人の手が空いているとも限りません。その度に見積もりが更新され、全体のスケジュールも目まぐるしく前後するようでは、経営陣や顧客からの信頼も損なうでしょう。規模の見積もりは、そうした人による差を吸収し、機能の相対的な大きさとチームとしての生産力にフォーカスした議論を可能にします。

ストーリーポイント vs 理想日

規模の見積もりに使う代表的な単位として、ストーリーポイントと理想日というものがあります。ストーリーポイントは、まさに前の節で「2」とか「4」とか言っていた、機能の規模を表す相対的な値のことです。「ストーリー」は「ユーザーストーリー」から来ています。一方、理想日は「その機能の開発だけに取り組んだとしてかかる日数」のことです。純粋な規模の見積もりではありませんが、説明・導入のしやすさから使われることがあります。

アジャイルな見積もりと計画」8章によれば、理想日には次のようなデメリットがあるとされています。

  1. 議論が職能別の詳細に入り込んでしまいチームとしての働き方を阻害する
  2. メンバーの能力が向上するにつれて見積もりが変化し、ベロシティが信頼できなくなる
  3. 理想日を現実時間と比べてしまって規模の見積もりができない
  4. 具体的な作業による見積もりになり、見積もり自体に時間がかかる傾向がある
  5. 人によって理想日は違う

逆にいえば、ストーリーポイントは以上の問題を解決します。理論の上ではストーリーポイントのほうが望ましいのは明らかでしょう。そのようにする具体的なメリットがあります。

しかし、現実問題として、チームメンバー全員にストーリーポイントという概念を導入するにはかなりの慣れやリーダーシップが必要とされるように感じます。単に説明するだけなら理想日のほうが格段に簡単です。そこで私が提起したいのは、仮に理想日のデメリットを低コストでコントロールできたとしたら、そちらのほうが良い場合もあるのではないか、ということです。実際、私が参加したプロジェクトでは見積もりの単位として理想日を用いましたが、上で述べたようなデメリットを感じることはなく、スケジュールを議論する材料としてプロジェクトの終盤まで活躍してくれました。以下では、理想日が機能した事例を掘り下げることで、見積もりと計画に関する考察を深めようと思います。

理想日が機能した事例

私がプロジェクトマネジメントを担当したのは、とあるプロジェクトのWebアプリケーション開発セクションです。プロジェクトのために一気に人が集められ、エンジニアはそのセクションだけで最大8人稼働、しかも社内で採用事例の少ないモダンな技術スタックで開発を進める、というかなり挑戦的なセッティングでした。プロジェクトの序盤に、全タスクの洗い出しと見積もりを行うことになり、その際、見積もりの値として理想日を用いることにしました。ストーリーポイントを使うかどうかを議論しましたが、チームへの導入のしやすさから、理想日を導入することになりました。すぐにエンジニアを集めて「このタスクだけをするとしたら何日かかるか、フィボナッチ数列で出してください」というふうに見積もり会を行いました。この会はスムーズに進行し、結局プロジェクト終盤まで同じ見積もりの仕組みが機能しました。

ここからは理想日見積もりのデメリットをひとつずつ見ていき、いかにしてそれをかいくぐったかを紹介します。

  1. 議論が職能別の詳細に入り込んでしまいチームとしての働き方を阻害する
    • これはそもそも、プログラマ、DBアーキテクト、テスター、デバッガーなどといったようにロールが細分化されていることを前提とした議論で、今回のチームにはあてはまりませんでした。全員がDBからフロントエンドまで一気通貫で作業する前提で議論をしたので、問題になりませんでした
  2. メンバーの能力が向上するにつれて見積もりが変化し、ベロシティが信頼できなくなる
    • これを感じる場面は、わずかですがありました。しかし、以下の2点によって最後まで安定したベロシティを得ることができたと感じます
      • 大部分の見積もりはプロジェクト序盤の見積もり会で一気に終わらせた
      • その後の追加タスクについては、理想日が相対ポイントであることを知っているメンバーが中心になって見積もりを行っていた。実際「このタスクは前に見積もった〇〇とほとんど同じなので2理想日は少ないと思います」といったように軌道修正をする場面があった
  3. 理想日を現実時間と比べてしまって規模の見積もりができない
    • これは上で述べたとおり、理想日が相対ポイントであることを知っているメンバーが軌道修正することで、かなり終盤まで秩序を保つことができました
  4. 具体的な作業による見積もりになり時間がかかる傾向がある
    • これについては、実際に具体的な作業に関する議論になることがあり、時間がかかったのは事実だと思います。しかし、プロジェクトのフェーズ的に、全員が具体的な作業をイメージできるくらいタスクを掘り下げて知識を展開することにもメリットはあり、始めからある程度は具体的な議論をするつもりでした。理想日だから時間がかかった、そしてそれが悪かった、という印象は全くありませんでした
  5. 人によって理想日は違う
    • これは、以下のような要因によって問題にならなかったと感じています。
      • 最初の見積もり会にエンジニアが10人近く参加しており平均化が容易であった
      • 社内で例の少ない技術スタックを採用したこと、また新規参加のエンジニアが多かったことで、スタート時点の能力のバラツキが抑えられていた

以上をまとめると、理想日を相対ポイントとして運用する明確な力が働いていたこと、期間に対する認識を平均化しやすい状態で大部分の見積もりを行ったこと、この2点が、理想日を機能させた大きな要因であったと思います。特に前者については、見積もりのベストプラクティスを理解していたからこそできたことで、仮にストーリーポイントを採用しないにしても、知識として知っておく価値のある概念だと感じます。

どちらを採用するべきか

今回の事例から考えると、最初から全ての要件が明らかになっているウォータフォール的な開発や、チームメンバーの能力が平均化される大規模な開発では、理想日のデメリットが少なく、ストーリーポイントを頑張って導入するよりも理想日を運用でコントロールする方が簡単というケースがあるかもしれません。一方で、技術力に差がある少人数チームや、アジャイルな開発での理想日見積もりは難しそうです。3人で見積もりをしたとして、あるタスクについて「1日」「3日」「5日」というようにバラけ続けたり、時期によって感覚が変わっていくのをまとめるのは大変そうです。ストーリーポイントのような、より相対感のある指標を使うことによって、議論を「見積もり済みのタスクと比べてどうか」という方向に導けるとしたら、そちらを導入するメリットはあるように感じます。どちらにせよ、最も重要なのは「期間ではなく規模を見積もる」ことで、それを簡単に実現できる仕組みを、チームの状況や自分のスキルにあわせて設計していくことが良い計画を作る近道なのではないかというのが、この半年の経験に基づく自分なりの結論です。

【React】カスタムフックでstatefulなコンポーネントを整理する

こんにちは。最近は仕事で React を書き始めました。数年前に React を触った時は class component をせっせと書いた覚えがあるのですが、最近は functional component と react hooks を組み合わせて書くこともできるようです。react hooks の概要は公式ドキュメント等を読んでもらうことにして、本記事では、自作のカスタムフックで stateful なコンポーネントを整理する一例を紹介し、それによって享受できるメリットを少し深掘りしたいと思います。

整理する前のコード

まずは僕が最初にお試しで書いた TypeScript のコードをお見せします。インターネットに転がっている例をつぎはぎして、見よう見まねで書いた投稿フォームです。コード中の useQuery は、apollo client のフックで、与えられた GraphQL クエリを POST して結果を取得してくれるものです。より現実的な課題を共有するために追加しています。

const MyForm: React.FC = () => {
  // ログイン中の自分のアカウントを取得する
  const { data, loading, error }: GetMeQueryResult = useQuery(getMeQuery);
  const [body, setBody] = React.useState('');
  const [message, setMessage] = React.useState<string | undefined>(undefined);

  const handleChange = 
    React.useCallback<React.ChangeEventHandler<HTMLInputElement>>(
      e => {
        if (e.target.name === 'body') setBody(e.target.value);
      },
      [setBody]
    );

  const handleSubmit = React.useCallback<React.FormEventHandler<HTMLFormElement>>(
    async e => {
      e.preventDefault();
      if (window.confirm('本当に投稿しますか?')) {
        setMessage('投稿中です。');
        // body を REST API 経由で投稿する
        await post(body);
        setMessage('投稿が完了しました。');
      }
    },
    [body, setMessage]
  );

  if (loading) return <p>ロード中…</p>;
  if (error) return <p>投稿できません。</p>;

  return (
    <form onSubmit={handleSubmit}>
      {message === undefined ? <div>{message}</div> : null}
      <span>{`ユーザー名「${data!.me.name}」として投稿します。`}</span>
      <input name={'body'} value={body} onChange={handleChange} />
      <button>投稿</button>
    </form>
  );
};

動かすので精一杯といった様子ですが、初めてにしては頑張った方ではないでしょうか。早速、このコードの何が問題かを考えましょう。ひとことで言えば「ロジックとビューが分離していない」というバッドプラクティスのお手本を踏んでいるわけですが、そこをもう少し丁寧に言葉にします。

ロジックの再利用性が低い

handleSubmit の内部を見てください。post の前後で setMessage をして、ユーザーが処理の様子を把握できるようにしています。例えば、この投稿機能を別の場所でも使いたい、しかも、ボタンで投稿するのではなく Enter を押して投稿するインラインのフォームにしたい、と言われたらどうでしょうか。今のままでは handleSubmit をコピペしてお茶を濁すしかないように見えます。状態に対する操作がコンポーネントと密結合しているため、コードの再利用性が下がっています。

テストがしづらい

このコンポーネントをテストすることを考えてみましょう。プロダクトの品質を高めるべく意気揚々と取りかかりますが、次々と面倒ごとが襲ってきます。

まず、MyFormレンダリングしないことにはテストのしようがないので、useQuery のモックは必須でしょう。フロントエンドのテストで実際の GraphQL API へ依存はしたくありません。幸い apollo client は useQuery 等を mock するための仕組みを用意してくれているのですが、こいつが意外と面倒です。テストのたびに react context に入っている client をモックしたり、リクエストとレスポンスを表すデータをせっせと組み上げたり、テストしたい事柄とは関係ない準備がたくさん必要になり、テストのコストが増加します。これと同じ理由で、スナップショットテストや storybook といったレンダリングの結果だけが必要なツールも利用しづらくなっています。

また、handleChangehandleSubmit の動作を確認するためには、実際に DOM を操作してイベントを発火させる必要があります。幸い React コンポーネントのテストでよく使われる enzyme では simulate などのメソッドを使って実現可能ですが、DOM の構造に依存している以上、デザイナーが構造を変更するとテストが壊れるといった問題がたびたび発生します。テストが壊れづらい CSS セレクタの書き方に頭を悩ませるのは、どうにも本質的ではない感じがしてきます。

カスタムフックを使ったリファクタリング

では、上で書いたコードをカスタムフックで整理していきます。

まずは「ロジックの再利用性が低い」という問題を抱えていた handleSubmit の内部を整理してみましょう。以下の usePost は「post 前後で適切に message を設定する」というロジックを抜き出したフックです。

const usePost = () => {
  const [message, setMessage] = React.useState<string | undefined>(undefined);
  return {
    message,
    wrappedPost: React.useCallback(
      async (body: string) => {
        setMessage('投稿中です。');
        // body を REST API 経由で投稿する
        await post(body);
        setMessage('投稿が完了しました。');
      },
      [setMessage, post]
    ),
  };
};

message を使いたいコンポーネントでは post ではなく wrappedPost を使うことになります。このようにすることで、post にまつわる状態操作のロジックを再利用することができます。また、React Hooks Testing Library 等を使うことにより、実際のフォームや他のロジックと独立にテストをすることができます。

次に、テストがしづらかった handleChangehandleSubmit を抜き出してフックにします。以下の useMyFormHook が対応するフックになります。

type MyFormHook = {
  body: string;
  message: string | undefined;
  handleChange: React.ChangeEventHandler<HTMLInputElement>;
  handleSubmit: React.FormEventHandler<HTMLFormElement>;
}

const useMyFormHook = (): MyFormHook => {
  const [body, setBody] = React.useState('');
  const { message, wrappedPost } = usePost();
  return {
    body,
    message,
    handleChange: React.useCallback<React.ChangeEventHandler<HTMLInputElement>>(
      e => {
        if (e.target.name === 'body') setBody(e.target.value);
      },
      [setBody]
    ),
    handleSubmit: React.useCallback<React.FormEventHandler<HTMLFormElement>>(
      async e => {
        e.preventDefault();
        if (window.confirm('本当に投稿しますか?')) {
          await wrappedPost(body);
        }
      },
      [body, wrappedPost]
    ),
  };
};

この整理の仕方は、MVPパターンにおけるP(Presenter)に相当すると考えることができそうです。このように整理することで、usePost と同じように実際のフォームと独立にテストをすることができます。特に面白いのは、こうした境界で handleChange 等を分割することで、ユーザーのアクションを単なる JS のオブジェクトで表現してテストできるようになっていることです。例えば、フォームの body が変化したイベントは { target: { name: 'body', value: 'new body' } }handleChange の引数として与えることで表現できます(このあたりは、実際のイベントを発火させるべき、もっとリッチなイベント生成ライブラリを使うべきといった話題もあるようで、さらに議論を深めることはできそうです)。

最後にフォームのコンポーネントを実装します。

const MyFormView: React.FC<GetMeQueryResult & MyFormHook> = ({
  data,
  loading,
  error,
  body,
  message,
  handleChange,
  handleSubmit,
}) => {
  if (loading) return <p>ロード中…</p>;
  if (error) return <p>エラーです。</p>;
  return (
    <form onSubmit={handleSubmit}>
      {message === undefined ? <div>{message}</div> : null}
      <span>{`ユーザー名「${data!.me.name}」として投稿します。`}</span>
      <input name={'body'} value={body} onChange={handleChange} />
      <button>投稿</button>
    </form>
  );
}

const MyForm: React.FC = () => {
  // ログイン中の自分のアカウントを取得する
  const queryResult: GetMeQueryResult = useQuery(getMeQuery);
  const myFormHook = useMyFormHook();
  return <MyFormView {...queryResult} {...myFormHook} />
};

フックから得られた値を使って ReactElement を構築する部分を、さらに MyFormView として分離しているのがポイントです。このようにすることでビューが pure な関数になるので、スナップショットテストや storybook といったツールでの取り回しが非常に良くなります。フックの返り値の型を mixin するような感覚でビューの props の型を定義すれば良いので、ビューコンポーネントを分割するコストはほとんど感じません。

結局何が良くなったか

ロジック、つまり「イベントを受けて状態を変化させる処理」をカスタムフックとして切り出し、さらに ReactElement を構築する処理を別コンポーネントに分割することで、ロジックとビューを疎結合にしたのが本質的にやったことです。一般的な設計論に従えば、カスタムフックで Presenter を実装して、MVP パターンでコンポーネントを整理したということもできそうです。このようにすることで、再利用性が高くテストしやすいコードになりました。また、MVP パターンに当てはめて考えられるようになったことで、カスタムフックの肥大化が設計の黄色信号である、ということも容易に理解できるようになりました。カスタムフックはあくまで「イベントを受けて状態を変化させる処理」に集中し、それ以外のドメインロジックを Model に蓄積していくと良いコードが書けそうです。

Apollo ClientのInMemoryCacheとMutationに関する調査・考察

本記事は はてなエンジニアAdvent Calendar 2019 3日目のエントリーです。

はじめに

Apollo Client (執筆時 v2.6.4)は、Apollo プラットフォームが提供する JavaScript で実装された GraphQL クライアントです。Apollo Client には InMemoryCache (執筆時 v1.6.3)という強力なキャッシュ機構が用意されており、クエリの結果をキャッシュすることができます。このキャッシュ機構は、多くの場面で API サーバーとの通信回数を削減しパフォーマンスの向上に貢献します。しかし、Mutation によるオブジェクトの作成・更新・削除を行う際には、Apollo Client を使う側がその変更をキャッシュに反映するよう意識する必要があります。例えば、オブジェクトAを含むリストがキャッシュされていたとして、オブジェクトAを Mutation で削除した場合、キャッシュに含まれるオブジェクトAも無効化されるのが望ましいでしょう。そうした操作を効果的に行うには、InMemoryCache の動作を理解することが不可欠です。本記事では、まず Apollo Client の InMemoryCache の動作を具体例を用いて解説します。そのあとで、Mutation を行う際のキャッシュの取り扱いについて調査・考察した内容をまとめます。

InMemoryCache の動作

InMemoryCache はクエリの結果のオブジェクトをキャッシュします。クエリの結果はツリー状のデータ構造ですが、キャッシュするときはデータを正規化して保存します。正規化の方法については こちら で詳しく述べられています。大雑把な動作としては、ツリーをたどって見つかった各オブジェクトについて、以下のようにキーを決定して、単純なキーバリューの組に分解します。

  • id (ないし_id)フィールドと __typename フィールドが存在するオブジェクトは "(__typename):(id)" というキーで保存する
    • (補足): Apollo Client はデフォルトで __typename フィールドを補完してクエリを発行するため、プログラマ__typename フィールドを明示する必要はない
  • 上の条件を満たさなければ、ツリーにおけるそのオブジェクトまでの経路をキーとして保存する

GitHub が提供する GraphQL API を使って例を見てみます。以下のようなクエリを Apollo Client で実行します。

query  {
  viewer {
    id
    name
    repositories(first: 10) {
      edges {
        node {
          id
          name
        }
      }
    }
  }
}

結果は以下のような JSON になります。

{
  "viewer": {
    "id": "yigarashi",
    "name": "Yuu Igarashi",
    "repositories": {
      "edges": [
        { "node": { 
            "id": "repo0", 
            "name": "repository_0" 
          } 
        }, 
        { "node": { 
            "id": "repo1", 
            "name": "repository_1" 
          } 
        }, 
        ... 
      ]
    }
  }
}

キャッシュされているデータは client.store.cache.data で参照できます。以下のようなキーが並んでいます。

  • ROOT_QUERY
  • User:yigarashi
  • $User:yigarashi.repositories({"first":10})
  • $User:yigarashi.repositories({"first":10}).edges.0
  • ...
  • $User:yigarashi.repositories({"first":10}).edges.9
  • Repository:repo0
  • ...
  • Repository:repo9

ROOT_QUERY はトップレベルのデータを表す特殊なキーです。先頭が $ で始まっているキーはオブジェクトまでのパスから生成されたものです。それ以外は (__typename):(id) の形式で生成されたものです。確かに、RepositoryConnectionRepositoryEdgeid を引いておらず、それ以外のオブジェクトでは id を引いており、上で説明した正規化戦略と結果が一致していることがわかります。

各キーに対応する値は基本的にクエリの結果で返されたデータですが、参照先がキャッシュ内に存在する場合はそのキーだけを持つようになっています。例えば $User:yigarashi.repositories({"first":10}).edges.0 というキーの値は { node: { id: " Repository:repo0", ... } } というようになっており、キャッシュ内にある Repository オブジェクトを指すようになっています。

次回のクエリ実行では、デフォルトではまずキャッシュのデータからクエリの結果を構築しようとし、データが足りればそのまま返して、足りなければサーバーに問い合わせることになります。

InMemoryCache と Mutation

書き込み操作を行うときにマスターデータとキャッシュの一貫性を気にする必要があります。例えば、キーが Repository:repo0 のキャッシュが存在する状態で、repo0 の名前を変更する Mutation を発行したら、キャッシュされているリポジトリのデータも更新しなければいけません。さもなくば、キャッシュが維持されている間は画面にリポジトリ名の変更が反映されません。私が知る限り、キャッシュの恩恵を受けながらキャッシュを更新する方法として、4つが Apollo Client のドキュメントにまとめられています。以下では、それらを簡単に紹介するとともに、メリット・デメリットについてまとめます。また、最後にそれらを組み合わせた実践的な使い方を考察します。

1. Mutation の結果としてオブジェクトを取得する

こちら で述べられているように、Mutation の結果としてオブジェクトを取得し、それによってキャッシュを更新します。例えば GitHubリポジトリ名の更新は、次のような Mutation を発行するだけでオブジェクトが再取得され、キャッシュの Repository:repo0 というキーに対応するオブジェクトが更新されます。

mutation {
  updateRepository(input: { repositoryId: "repo0", name: "NewRepoName0" }) {
    id
    name
  }
}
  • メリット
    • Mutation で必要なフィールドを含むように気をつけるだけでよく簡単
  • デメリット
    • 更新操作でしか機能しない(要素の作成や削除は自動的に反映されない)

2. 変更したオブジェクトを含みうるクエリを明示的に発行する

こちら の前半で述べられているように、mutation 関数のオプショナル引数である refetchQueries を用いて、Mutation による変更が反映されるべきクエリを明示的に引き直してキャッシュを更新します。GitHub API の例に沿うなら、createRepository mutation を行なった後に viewer.repositories を引き直すといった場面が考えられるでしょう。キャッシュと Mutation について考える時、「作成・削除操作とそのオブジェクトを含むリスト」という場面が代表的な悩みのタネになりますが、この方法はその解決策の1つとなります。

  • メリット
    • オブジェクトの作成や削除に伴うキャッシュの更新に対応できる
  • デメリット
    • 大規模アプリケーションで引き直すべきクエリが増えたとき管理が難しい
    • 実際にサーバーにリクエストを送信する(軽微なデメリット)

3. キャッシュを直接書き換える

こちら の後半で述べられているように、mutation 関数のオプショナル引数である upadte を用いて、キャッシュを直接操作して更新を反映します。readQuery でキャッシュからデータを読み出し、それを書き換えて、writeQuery で再びキャッシュに書き込むのが基本の形になります。オブジェクトの作成や削除に対応できるという点で2番目の方法と類似しています。

  • メリット
    • オブジェクトの作成や削除に伴うキャッシュの更新に対応できる
    • キャッシュの操作のみで完結するので通信のオーバーヘッドがない
  • デメリット
    • 明示的なキャッシュ操作が必要で実装コストが高い
    • 大規模アプリケーションで操作すべきキャッシュを網羅するのが難しい

4. キャッシュポリシーを調整する

こちら で述べられているように、query 関数の fetchPolicy をキャッシュしないような設定にすることで、パフォーマンスや通信コストと引き換えに煩雑なキャッシュ更新操作を避けることができます。fetchPolicyここ にまとめられており、クエリごとに用途にあったポリシーを選択することができます。このメリット・デメリットは、キャッシュポリシーや扱うデータの特性に大きく依存するため、ここで一概にまとめるのは難しいでしょう。

以上の手法をどのように組み合わせるか

実際のアプリケーションでは、データの特性に応じて異なる戦略を取るのが効果的であると考えます。例えば以下のようなことが考えられます。

  • トップページの「新着コンテンツ」のような箇所では、ユーザーのコンテンツ操作に応じて頻繁にデータが更新される必要はないことが多いです。そのような場合はキャッシュをしても問題はなく、Mutation 時の明示的な更新も考える必要がないかもしれません。
  • ログインユーザーに見せる「自分のコンテンツ一覧」のような箇所では、ユーザーのコンテンツ作成・削除が直ちに反映されるのが望ましいでしょう。そのような箇所に限って 、2. や 3. の手法をとったり、キャッシュポリシーを必ずサーバーに問い合わせるものに設定するのは効果的です。
  • 作成・削除のUIと一覧表示が同じ画面内に存在するときは、特に 3. が有効です。ページ遷移がない分、通信のオーバーヘッドが目立ちやすい箇所なので、ブラウザ内の計算のみで完結することでユーザー体験を向上させることができるでしょう。

まとめ

本記事では、まず Apollo Client の InMemoryCache の動作についてまとめました。InMemoryCache はクエリの結果を正規化して保存しており、平らなキーバリュー型のデータ構造になっていることを説明しました。次に、そうした知識を踏まえて、Mutation のあとにどのようにキャッシュを更新したら良いかについてまとめました。キャッシュの更新は大きく分けて4つの方法があり、データの特性に合わせて戦略を変えたり、複数の方法を組み合わせるのが良いでしょう。

Python処理系入門 〜1 + 1 で学ぶ処理系解読の基礎〜

この記事は CAMPHOR- Advent Calendar 2018 4日目の記事です.

1. はじめに

プログラミング言語 Python は汎用の動的型付き言語で,機械学習や Web 開発を中心に幅広く使われています.特にここ数年の Python 人気は凄まじいものがあり,某大学生協の本屋では,プログラミング系の平積みコーナーが一面Python 関連書籍で埋め尽くされています.所属しているコミュニティの関係でプログラミング初心者の学生にもよく会うのですが,第一言語Python という方が非常に多く,まさに猫も杓子も Python といった状況です.

そんなわけで,人々がこぞって Python でプログラムを書いているわけですが,「Python 自体がプログラムである」という事実に目を向けたことのある人は非常に少ないと思います.みなさんが Python のプログラムを書いて実行する時,実際には,処理系(インタプリタ)と呼ばれるプログラムが起動し,みなさんが書いたプログラムを読み込んで実行しています.みなさんが日々入力している python というコマンドもまた,処理系というプログラムなのです.

最も主要な Python の処理系(つまり Pythonソースコードを読み込んで実行するプログラム)はC言語で書かれた CPython で,「Pythonをインストールする」と言った時は,大抵この CPython をコンパイルしてできた実行ファイルをインストールしています.CPython はオープンソースソフトウェアで,誰でもソースコードを閲覧したり開発に参加することができます.CPython のソースコードPython の振る舞いをより深く知るのにはうってつけの資料で,CPython の開発に携わっていなくとも,サービスのボトルネック発見といった課題のためにC言語で書かれたソースコードを読む破目になる人もいることでしょう.

しかしこの CPython,ソースコードが膨大で,最初は読むべき場所を探し当てるのが非常に難しいです.CPython のデータ構造や実行の全体像といった内容については有志の解説記事がそれなりに存在しますが,コードの読み方にまで踏み込んだ情報はなかなか存在せず,とっかかりを見つけるのがとても大変です.

この記事は,そうした厳しい状況を少しでも改善することを目的とした,CPython 入門者のためのハンドブックです.「CPython を改造して 1 + 1 = 3 にする」というお題を通して,CPython の基礎知識から,CPython のコードの読み方まで解説します.次の2章では,CPython に関する最低限(本記事を理解するのに必要なだけ)の知識を説明します.3章では,「CPython を改造して 1 + 1 = 3にする」というお題を通して,実際のソースコードを追いかけてみます.ここでは,ただ変更箇所を示すのではなく,どのように変更箇所を探し当てるのかという部分に焦点を当てて解説します.4章では,3章のコードリーディングからエッセンスを抽出し,CPython のソースコードを自力で読んで行くための一般的な知見を説明します.5章でさらに掘り下げていくための資料をいくつか紹介し,6章でまとめとします.CPython の構造体や実行方法についてぼんやり知っているという人は,2章を飛ばして3章から読んでも良いでしょう.

続きを読む