YAPC::Kyoto 2023 で ORM について喋ってきた

資料は こちら です。

背景

アーキテクチャ的に何かを足したいとき、我々はチーム開発を行っているのだから、チームの共通認識を変えるということになる。認知負荷が高い場合は提案を拒否されてしまうので、認知負荷をできる限り小さくして導入したい。つまり差分の最小化です。*1

現在のコードベースと、入れたいアーキテクチャを対比させつつ、こう導入するのがベストと見切るところが今回のトークの面白ポイントです。

PoEAA のデータソースのアーキテクチャに関するパターン

PoEAA は 20 年前の本なので、当時の開発風景を想像できる人と会話しながら読むと良いです。エリックエヴァンスの DDD 本も似た時期ですね。2002 年は Java 1.4 がリリースされた頃。デザインパターンUMLXML が流行っていた。ライブラリのパッケージマネージャやセントラルリポジトリがまだ無い。*2

再利用性があり生産性が高いらしい「オブジェクト指向プログラミング」に全ての夢を託していたあの時代。また、もちろん Rails も無いので migration (アプリケーションエンジニアが DB を気軽に変更し続ける) という文化もまだ発見されていない。

PoEAA では、アプリケーションはドメインを表現するようにしたい。データベースは容易には変更できないので、ドメインと結合させない、DataMapper パターンに自然となっていくだろう、という語り口で書かれている。その後、現実は密結合で加速する方が認知負荷が低くて扱いやすい、DB のリファクタリングをどんどんやる、そもそもドメインとデータソースを分離するのは割と無理筋、という方向になったっぽいと思う(観測範囲)。

データソースのアーキテクチャに関するパターン 4 つを構造で分類して説明するというアイディアは PHP/アクティブレコードってなに? から借りた。*3

greppability

Sample::Repository::User みたいに、各テーブルに対応するクラスを作ることの是非かぁ。そもそも世の中を席巻している ActiveRecord パターンはテーブルに対応するクラスを作るので、そこで違和感を持つことは無かったなぁ。

ただテーブル名を文字列から定数にするだけではなく、ちょっと複雑なクエリ置き場を作る。クラスにしたことで、こういうものはココに書くと決められる、という意味を付けられたんじゃないか。まさに TableDataGateway として整理した。

greppability に関しては、現実世界のオペレーションとして「どこからこのテーブルが触られているのか」というのは時々眺める必要がある。謎データが入った原因を探したり、急に垂直分散する必要が出たり。*4 少しでも日々の作業が楽になるように多少の工夫はしたい。

SQL が分からなくならない?

Perl の DB 周りのライブラリには基本的にどこで発行した SQL なのかをコメントで残す機能がある *5 ので、スロークエリの検知や集計さえできていれば発生した問題には対応できる。 また、集計や検知するためのサービスが現代では整っていて、自前で pt-query-digest 等で集計しなくても Performance Insight が教えてくれるので、頑張らなくても見つけられるんじゃないかと思っています。

今は移行直後なのでみんな SQL への脳内マッピングができる状態だけど、「ORM しか触ったことのない人」が増えた世界だと「発行される SQL を見てくれ!!」と言いたくなりそうですね。例えば Devel::KYTProf を入れて流れる SQL を眺めながら開発するよう働きかけるとか、それをやってもログだと眺める人が少ないのでブラウザ上にどうにかして表示するとかが必要になるかもしれません。

そもそも問題が発生しないようにするのは難しいよねー……。工夫はして防ぐけど、たまにやらかしが出うる、勝ち目の少ない戦いっぽいと思う。

ドメイン

質疑もあった。(まとめてくれている id:stefafafan は本当に助かる)

低レベルな API としての SQL、もうちょっとだけ高レベルな (CRUD だけを司る) TableDataGateway、もうちょっと高レベルな (ある程度ドメインロジックも持つ) ActiveRecord、更に高レベルな (テーブルと疎結合になってドメインそのものを表す) DataMapper パターンとあって、DDD のドメインは DataMapper パターンのドメインが一番近いんですよね。

たぶん質問は「Repository」という言葉に引っ張られたのだと思う。DDD のリポジトリ (ドメインとデータストアとのマッピングを行う、まさに DataMapper のこと) ではなく、TableDataGateway(特定のテーブルに対する CRUD を集めたもの)に Sample::Repository::User という名前を付けました。高レベルなものは低レベルのものを組み合わせて実現するので、やればできますが、僕らのアーキテクチャだと既にもう一個上に置いてある Service 層をドメインロジック置き場としているので、新たなドメインモデルを作る理由が無かった、が回答になりました。

これは ドメインモデル貧血症 なのではないかという指摘もありそうですね。その通りと思います。困るようになったら改善するかもしれません。が、僕は 95% のものはテーブルと紐付いたドメインモデルで十分と思っているので、必要なところでだけどうにかする、をやりそう。

話せなかった内容

使用メモリが倍増したのでリリースして慌てて取り下げたとか、JSON を BLOB カラムに入れている場合があって encode_utf8 で一度ハマったとか、トランザクション境界が合ってなくて導入しづらいものがあったとか、入れる上でのトラブルが一通りあるんだけど、あのペースで喋っても時間いっぱいだったので入れられなかった。

Repository や Row を扱いやすくするための工夫や、ライブラリに送った PR についても話したかったけどこれも時間の関係で見送り。具体的には Row をトップレベルの名前空間に置いたのは、特にやって気持ちが楽になった工夫です。タイプ数が多いと普段使いしづらい。ドメインオブジェクトであり、普段から触るものだと認識しやすくするためには、浅いところに置くのが望ましいと思っています。

まとめ

  • 対象領域を整理しよう
    • 今回は ORM を PoEAA の語彙で 4 パターンに、さらに 2 軸で整理した
  • 自分のコードベースを見極めよう
    • どのパターンに近いのか、例外的に扱っているところはあるか等
    • どうあるべきかを考えることになるので、自然とアーキテクチャと仲良くなる
    • アーキテクチャが綺麗だと例外が少ない
  • ギャップが少なくなるようにして導入に持ち込もう
    • 感覚じゃなく、整理して、だからこれが最適解だ、を突きつける
    • 今までとのジャンプアップの大きさは注意しよう
      • 大きすぎると認知負荷が高すぎて導入できない
  • いちど整理すると面白副作用が出てくるのでオススメ
    • 混沌だと手が出せなかったが、整理済みなら改善できる
      • 認知負荷を下げるとハードルが下がるらしい

参考資料とか

*1:なんでも差分最小が良いわけではなく、理想と現実のギャップを捉えた上で、落としどころを探すことになる

*2:Maven も無く Ant で入れていたと思う

*3:のだけれど、今見ると 403 だなー? :thinking_face:

*4:ISUCON じゃない現実でもそういうことはある

*5:Teng の場合は https://github.com/nekokak/p5-Teng/blob/0.33/lib/Teng.pm#L272-L287

デュアルトラックアジャイルとの向き合い方。あるいはエンジニアとビジネスの距離感

昨日(もう日付余裕で回ってるので一昨日だな)Findy さん主催のイベントで話してきた。

speakerdeck.com

背景

近年「エンジニアは事業貢献してこそ」「エンジニアもユーザファーストでビジネス貢献」といった言説がIT界隈で増えて来ている感じがしている。

……とたまたま昨日関連してるようなしてないような話をしている エンジニアとビジネスの距離感の難しさ|ばんくし|note という記事があったので書き出しを真似してみたんだけど。

昨今、ビルドトラップに陥るな、アウトプットじゃなくアウトカムに着目しろ、って言われることが増えてますよね。でも僕は逆張りして、アウトプットにまず着目しろという声を上げておきたいのです。

開発生産性(いろいろある*1)の話をするときに、ディスカバリーとデリバリーの 2 軸で考えるのはコモディティ化してきたと思う。でも、それによって、デリバリーの重要性が薄くなっているとも思っている。両輪と口では言いつつ、意識は最近見つけたディスカバリー側に寄っちゃってるんじゃないか。

ディスカバリーもデリバリーもまだまだ改善の余地があるときに、開発チームはどちらを優先して伸ばすべきか。これは僕は基本的にデリバリーだと思っています。開発チームはね。

デリバリーが先、ディスカバリーが後

作る速度を上げるのがデリバリー、作るものを決めるのがディスカバリーなので、デリバリーは車の整備、ディスカバリーはカーナビの整備と言えると思う。

ナビがポンコツでも、方向ベクトルの角度が 90 度以内に入っていればゴールに近づくことはできるし、稀にドンピシャの方向を示すこともある。でも車が走らなかったらまったく近づかない。高速にアウトプットできるという状態をまず目指したい。

高速にアウトプットできる場合、良い企画に当たったら高速にアウトカムを作れる。企画なんてそもそも当たるも八卦当たらぬも八卦だし*2、当てる確度を上げていくには机上の空論をこねくり回すんじゃなく結果を見て調整するイテレーティブな改善サイクルを繰り返した方が良いよね、というのも経験主義なスクラムをやっている僕らはよく知っている。

なので、良い企画のときには爆速でリリースしてあげたいし、そもそも高速に仮説検証を回せる体制を作っておきたい。デリバリーの強さがディスカバリーの強さを引っ張り上げる。逆は無い。*3

強いチームを作るにはイテレーションが必要

まともに回っていない開発チームのときに、ディスカバリーの問題にしたくないなーとも思っている。これはそもそも論になりすぎるとか、小さな改善が発生しなくなるとかが起きるからです。

まともに回っているとは、ふりかえりが機能して毎イテレーション何かしらの改善が生まれているとか、キックオフが行われたり月例会が行われたりしてチーム全員が自分たちの As Is/To Be を理解しているとか、そういう「ふつうの開発チーム」が普通に備えているべきものを普通にやっている状態を指しています。

チームで同じ方向を目指して開発していくための前提みたいなところですね。この状態になっていないうちに「ディスカバリーにチーム全体がもっと目を向けないと」という風潮になると、本来改善しなければならない開発チームとして機能していない部分が放置される。タックマンモデルで言う統一期、機能期が訪れず、ずっと形成期や混乱期に居る状態。

  • 必要な情報がちゃんと流れる
  • 役割分担が自然とされて阿吽の呼吸で動ける
  • 自分たちの行動規範を持っている
  • チームへの帰属意識がある
  • 目標達成にコミットできる

という良いチームには、イテレーションを回して、コミュニケーションのトラブルを解決した結果、ようやく辿り着ける。改善された実感を得て、このチームなら良くなっていきそうと思えている必要がある。

なので、高速にアウトプットできる、なんでもいいからリリースを繰り返せるチームをまず目指すべきだと思っています。

デリバリーもディスカバリーも 80 点をまず目指したい

80 点を取るまでは 20% の努力でできる。2 つの軸があるならそれぞれ 80 点を取るのをまず目指すべきで、個人の努力とチームの努力で 80 点は数ヶ月で達成できると思う。*4

デリバリーは↑で書いたような普通のチームを作りましょう。ディスカバリーは 90 度以内になりましょう。ここまでは自分のプログラマやプランナーという職種の専門性の話でしょう。

僕はある程度のレベルまではそれぞれ伸ばせが良いと思っている。何よりもコスパが良い。で、ある程度を満たしていないのに反対側に手を出すのは、目の前にある自分が解決すべき問題を他責にする行為なんじゃないかと感じる。

当時もこう書いていた。*5

視座を上げるときのアンチパターン

視座を高く持って視点を探そうとすると,例えば朝会に遅刻をした時に 「そもそもリモートで参加できない状態がおかしくないですか」 みたいな発言をしがちになる。

視座・視点の切り替えは得てして論点ズラしに使われる。 他人の視座に立つというのは,他責にすることではない。

デリバリーが普通になってから、ディスカバリーと高次元にバランスさせた状態を目指しましょう。

相手が自分よりも困っているなら、もちろんチームなので助けに行きたいけど、そうじゃないのに越境してるのは、もうちょっと何かあるだろって思います。

高次元にバランスさせるところが業界の標準的な悩みになっているんだとしたら、そんな素晴らしいことはないですね。逆張りした話をここまで書いてきたけど、当然そうありたいと思っています。

*1:https://qiita.com/hirokidaichi/items/53f0865398829bdebef1

*2:他職種の仕事を貶めるような言い方だけど、プランナー専門職や凄腕の PO が異常に頼りになることは本当によく知っています

*3:当たった結果、金を出して強いチームを作れるようになるかもしれないし、当たり続けることで求心力が高まって強いチームになるかもしれない。そもそも当たらないとチーム解体になるみたいな話も置いてある

*4:何が 100 点で何が 80 点なのかの基準を揃えないと会話が成り立たないので、チームではそういう話をしなきゃいけなかったりします

*5:コロナ禍ですっかりリモートワークになっちゃったので例としてはもう微妙

RSpec では context 間の違いを表現するときにのみ let を使う

Test which reminded me why I don't really like RSpec | Arkency Blog (日本語訳:Rails: RSpecが好きでないことを思い出したテスト(翻訳)|TechRacho by BPS株式会社) を見ての感想。

元のコードのイマイチなところは 4 つあって、

  • paramsbefore で書き換えている *1
  • it "will succeed" の文言
  • it { is_expected.to be_success }expect(result.success?).to eq(true) が混ざっている
  • let が不思議な順序で連発されていて事前条件を読み解けない

すべて、これによって何をテストしているのかが分かりづらくなっているという問題を引き起こす。

paramsbefore で書き換えている

let(:params) { { last_name: "something" } }
subject(:result) { described_class.call(current_user: user, params: params) }
...
describe "checking Address update" do
  let(:new_zip_code) { Faker::Address.zip_code }
  before { params[:zip_code] = new_zip_code }

  context "when user has address and want to change something in their address" do
    let!(:address) { create(:address, owner: user) }

    it "will succeed" do
      expect(result.success?).to eq(true)
      ...

この before は上の方で定義してある let との合わせ技で、かなり厳しい。

このクラスでは、異なる結果が得られる主な入力は params である。

この入力がどう変わると出力がどう変わるかは明確にわかるはずだが、これを call に明示的に渡さない理由がわからない。

にまったく同意で、そのために describe で切り分けているのだから、params に何が渡るのかは describe 内で明示する。何をテストしているのかを明確にするというのを意識したい。今は params の違いをテストしているのだから、params が何なのかをそのまま書く。let で定義して subject に渡しているので、let で上書けば良い。

 let(:params) { { last_name: "something" } }
 subject(:result) { described_class.call(current_user: user, params: params) }
 ...
 describe "checking Address update" do
   let(:new_zip_code) { Faker::Address.zip_code }
-  before { params[:zip_code] = new_zip_code }
+  let(:params) { {
+    last_name: "something",
+    zip_code: new_zip_code,
+  } }

   context "when user has address and want to change something in their address" do
     let!(:address) { create(:address, owner: user) }

     it "will succeed" do
       expect(result.success?).to eq(true)
       ...

「他のところで作ったものを before でちょこっと書き換えよう」はだいたい悪手。あと多分ココは last_name: "something", を残す必要なくて、zip_code のテストをしたいので zip_code のみになっている方が望ましいと思う。

it "will succeed" の文言

describe "checking Address update" do
  ...
  context "when user has address and want to change something in their address" do
    ...
    it "will succeed" do
      ...
    end
  end

  context "when user has not have any address and want to change something in their address" do
    ...
    it "create address with given params" do
      ...
    end
  end
end

これは何のテストをしているのか読み解くのが難しいと思うけど、よくよく読むと、やりたいことはただの upsert である。おそらく実装側はこうなっている。

if params[:zip_code]
  address = current_user.address || current_user.build_address
  address.zip_code = params[:zip_code]
  address.save!
end

upsert のテストをしているというのが分かるように説明を書く。なお、テスト対象や事前条件と context の説明は一致していて、context 直下に before もしくは let が来るのが良い分割。テストの違いを contextbefore, let で表現するのだ。

ついでに何をテストしているのか分かりやすいように「params[:zip_code] を渡したとき」というのも説明文として表現するとより親切だろう。

context "with params[:zip_code]" do
  let(:params) { {
    zip_code: new_zip_code,
  } }
  ...
  context "when address exists" do
    before { create(:address, owner: user) }
    ...
    it "will be updated" do
      ...
    end
  end

  context "when address does not exists" do
    ...
    it "will be created" do
      ...
    end
  end
end

describe, context, it の説明文を駆使して何をテストしているのかを明らかにするのは、可読性の高いテストを書くための基本です。

it { is_expected.to be_success }expect(result.success?).to eq(true) が混ざっている

context "when address exists" do
  ...
  it "will be updated" do
    expect(result.success?).to eq(true)
    expect(address.reload.zip_code).to eq(new_zip_code)
  end
end

context "when address does not exists" do
  ...
  it { is_expected.to be_success }
  it "will be created" do
    expect { result }.to change(Address.all, :count).from(0).to(1)
    expect(Address.last.zip_code).to eq(new_zip_code)
    expect(user.address).to eq(Address.last)
  end
end

update 側では result.success? と、更新されたことの 2 個の expect があるが、 create 側では result.success? は単独のテストとして切り出されているし、書き方も違う。

書き方が違うと「この違いに何か意味があるのか?」という疑問が湧いてしまい、テスト対象がぼやける。テストの粒度は揃える。

また、success? であることはあまり主眼ではないので、目立たないように定型文っぽくなっているとより良い。つまり eq(true) とハッキリと対象をテストするよりも、読み下して読み飛ばせる方が望ましい。

 context "when address exists" do
   ...
+  it { is_expected.to be_success }
   it "will be updated" do
-    expect(result.success?).to eq(true)
     expect(address.reload.zip_code).to eq(new_zip_code)
   end
 end
 
 context "when address does not exists" do
   ...
   it { is_expected.to be_success }
   it "will be created" do
     ...
   end
 end

こうすると、全 context で「result.success?」と「詳細な副作用」をテストしている、と整理できる。

let が不思議な順序で連発されていて事前条件を読み解けない

context "when user has parent and want to change something" do
  let(:parent) { create(:parent) }
  let(:user) { create(:user, profession: student) }
  let(:student) { create(:student, parent: parent) }

  it { is_expected.to be_success }
  ...

ここでは parentupsert するときの update 側をテストしたい。そのため、 テスト対象 (subject) である Api::Students::Update.call(current_user: user, params: params)user 側に「parent を持っている User である」という事前条件がある。

最終的に欲しいのは user なので、せめてこの順に並んでいて欲しい。

  let(:parent) { create(:parent) }
  let(:student) { create(:student, parent: parent) }
  let(:user) { create(:user, profession: student) }

また、事前条件なんだから before で整える、というのも自然な考え方だろう。let と違い遅延評価されず、スコープが絞られるため、怖れずに上から順に読めるようになり、処理を追うのが簡単になる。

context "when user has parent and want to change something" do
  before {
    parent = create(:parent)
    student = create(:student, parent: parent)
    @user = create(:user, profession: student)
  }
  let(:user) { @user }

  it { is_expected.to be_success }
  ...

let(:user) { create(:user, profession: @student) } でも良いんだけど、私は before で作ったインスタンスlet にパスするだけが好きです。この方が事前条件を整えた後に context に受け渡している感がある。

僕なら最初の 5 分でこうリファクタリングする

RSpec.describe Api::Students::Update do
  let(:user) { create(:user) }
  let(:params) { { last_name: "something" } }
  subject(:result) { Api::Students::Update.call(current_user: user, params: params) }

  context "when user is a student" do
    it { should be_success }
  end

  context "when user is teacher" do
    let(:user) { create(:user, :teacher) }
    it { should be_failure }
  end

  context "with params[:zip_code]" do
    before { @new_zip_code = Faker::Address.zip_code }
    let(:params) {
      {
        last_name: "something",
        zip_code: @new_zip_code,
      }
    }

    context "when address exists" do
      before { @address = create(:address, owner: user) }

      it { should be_success }
      it "will be updated" do
        result
        expect(@address.reload.zip_code).to eq(@new_zip_code)
      end
    end

    context "when address does not exists" do
      it { should be_success }
      it "will be created" do
        expect { result }.to change(Address.all, :count).from(0).to(1)
        expect(Address.last.zip_code).to eq(@new_zip_code)
        expect(user.address).to eq(Address.last)
      end
    end
  end

  context "with params[:parent]" do
    before { @parent_first_name = Faker::Name.female_first_name }
    let(:params) {
      {
        parent: { first_name: @parent_first_name },
      }
    }

    context "when parent exists" do
      before {
        @parent = create(:parent)
        student = create(:student, parent: @parent)
        @user = create(:user, profession: student)
      }
      let(:user) { @user }

      it { should be_success }
      it "will be updated" do
        result
        expect(@parent.reload.first_name).to eq(@parent_first_name)
      end
    end

    context "when parent does not exists" do
      it { should be_success }
      it "will be created" do
        expect { result }.to change(Parent.all, :count).from(0).to(1)
        expect(Parent.last.reload.first_name).to eq(@parent_first_name)
        expect(user.profession.parent).to eq(Parent.last)
      end
    end
  end
end

こうしておくと -f d で実行したときにも読み解きやすい。

Api::Students::Update
  when user is a student
    is expected to be success
  when user is teacher
    is expected to be failure
  with params[:zip_code]
    when address exists
      is expected to be success
      will be updated
    when address does not exists
      is expected to be success
      will be created
  with params[:parent]
    when parent exists
      is expected to be success
      will be updated
    when parent does not exists
      is expected to be success
      will be created

change はあんまり使いたくないなー (expect { result }.to はちょっと読みづらくない?) とか、多分 create の方のテストを先に書くだろうな (条件が複雑な方をより後でテストしたいので、レコードが存在している場合は後ろに書きたい) とか、一つのテストの中で複数の expect を書くなら aggregate_failures を使えとかの思いはあるけど、シュッと直すならこうかなー

元記事のリファクタリング後と何が違うか

  • describe (context) のネストは維持している
    • テスト対象が明確に違うので、ネストしておく方が自然
    • params[:zip_code]params[:parent] があるときの、それぞれの upsert を確認している
  • context の説明はより実装寄りに
    • ユニットテストなので実装寄りの言葉で問題無いし、その方が実装を書き換えるときにテストの対応を取りやすい
  • subject を引き続き使う
    • テストしたいのは params の違いなので、params に着目したい。
    • 元記事のリファクタリング後だと params は 70 文字目辺りから始まっていて、どこに違いがあるのかを読み解くのが困難
    •     result = Api::Students::Update.call(current_user: user, params: { parent: { first_name: parent_first_name } })
      
    • subject に執着しているわけではなく、テスト間の違いをもっと目立たせろと思っています
  • letcontext 間の差を表現するために使う
    • よりテスト間の違いに目を向けさせるためです
    • let は遅延評価&後勝ちなところが大きなメリットで、まさに context 内で上書きしたいときに使う語彙です
    • そのため、私は積極的に before やローカル変数、インスタンス変数を使い、let を温存します
      • 今回の書き換えでも user, params のみが let になっていて、何をテストしているのか読み解きやすいんじゃないか?
      • テーブルテストと同じことを contextlet でやっているのです

subjectインスタンス変数に反対の派閥があるのは知っていて、異論はあると思うけど、私は「テスト対象が分かりやすくあった上で Ruby/RSpec の表現力にあやかりたい」と考えているので、こう書いています。

合わせて読みたい:Rails Developers Meetup 2017 で RSpec しぐさについて話した - onk.ninja

*1:公開当初は「params を merge している」と書いていたのでこういう反応があった https://twitter.com/qsona/status/1629840312389742593

YAPC::Kyoto 2023で話します! そしてチケットを今すぐに購入しましょう!!

YAPC::Kyoto 2023の採択トークが決まったようですね。面白そうなトークが沢山あってすごいですね。

blog.yapcjapan.org

私のトークも採択されました。ありがてぇ!

こういう話をします。

ORM - Object-relational mapping

はてなPerl プロダクトは薄いフレームワークを志向して、データベースとのやり取りに DBIx::Sunny や DBIx::Handler::Sunny を用い、主に SQL を書いて暮らしていました。最近、私はこの世界に ORM を持ち込みました。 PofEAA によるデータソースのアーキテクチャの 4 分類、我々が何を考えてどのパターンを選んだか、必要になって書いたプラグイン等、ORM の無い世界に ORM を入れていくに当たって考えたことと、その実践。 Perl Monger なら一生に一度は書くといわれる ORM を書いていく様子を、RailsActiveRecord に慣れ親しんだ現代の目線も交えつつお送りします。

プロポーザルは 40 分枠のつもりで出していたのですが、備考に「20分でも構いません」と書いておいたら連絡が来たので快諾しました。 話したい人が沢山いてめでたい!ちょっと駆け足で話します!

裏話

タイトルは Kyoto.pm Tech Talks #01 リスペクトです。

このときの「ORM - Object-relational mapping」というトーク@nekokak さんによるものですが、今回もゲストスピーカーとしていらっしゃるので、目の前で同じタイトルのトークをするのはなんだかすごくアレですね……!頑張ります。

blog.yapcjapan.org

チケットを買ってくれ

それはそうとして、そんなYAPC::Kyoto 2023ですがチケット販売が今月1月の31日までとなっています。

passmarket.yahoo.co.jp

今月中にチケットを買わないと参加ができないのです! 今、まさにこの瞬間、すぐに買いましょう!!!!!

買いましたか? 買いましたね。それでは会場でお会いいたしましょう!

私は京都は住んでいるので、地元で YAPC を開催することができて感動しています。来る方を精一杯歓迎いたします!

オマージュ元

moznion.hatenadiary.com

ストーリー性のあるプレゼン

このツイートの「文字を組み合わせる」のところについて、もうちょっと掘り下げてみる。*1

この記事は はてなエンジニア Advent Calendar 2022 の1月2日の記事です。昨日は id:stefafafan『UNIXという考え方―その設計思想と哲学』を読んだ - stefafafan の fa は3つです でした。

3 つのポイント

  • 知っていること 7 割、聞いたことがあること 2 割、知らないこと 1 割
  • 引用しやすいワーディング
  • ストーリー作り

この 3 つはとても意識しているポイント。

知っていること 7 割、聞いたことがあること 2 割、知らないこと 1 割

大半は頷きながら聞けるし、やった方が良いって聞いたことはあるけど本当なんだと感じることもできる。そして知らないことを知って興奮する部分もある、というバランスを気にしている。

知っていることから始めることで、従来と違うところや、一番注目させたいポイントにちゃんと目を向けさせることができる、がこの割合の重要な点。これが逆転していると聞き手が途中で振り落とされてしまう。せっかくなら全員を最後まで連れて行きたい。

引用しやすいワーディング

Twitter で実況しやすいとか、ブクマコメントにしやすいとか、一部を切り出して「そうそうコレ!!」と流通させてくれるような聞き手との対話を考えながら作っている。これがあると無いとでは後から思い返される率がガラッと変わる。「引っかかり」を感じさせるキーワードを考え抜いて、色んな軸で引っかけに行くイメージ。

2-3 分おきに繰り出せたら飽きないトークになるので、緩急を考えながら散りばめていく。

いいワードが出たら懇親会とかでも話題にして貰いやすい。自分のトークをきっかけにコミュニケーションを生みたいので、話題になるのは目指したい姿。

ストーリー作り

上 2 つはテクニックっぽい *2 けど、ストーリーはもっと根幹なので、しっかり掘り下げてみる。

自分のプレゼンをふりかえる

Hatena Engineer Seminar #22 「会社説明資料に載らないはてな」

www.slideshare.net

20 分トーク。会のテーマは「カジュアル面談」。僕は主にアウトプットをテーマにしたカジュアル面談を行ってきたので、その話をしたい。

話してきた内容をザッと書き出した後に、書き手側と読み手側という 2 つの軸があるんじゃないかというのが見えてきた。

  • 技術記事を書く文化
  • 技術記事を読むのを楽しむ文化

「片方に注力するのではなく両輪である」は普遍的に使える構造。

書く側にフォーカスした記事はよく見るが、読み手として楽しむ文化作りの方はあまり見ないので、これをセットにして両輪構造であると伝えるのは新規性がありそう。この構造を組み立てられたので、このトークは成り立つと考えた。

両輪のそれぞれが単独のトークとしても成り立つ自信がある内容だったので割と安産。

id:onk 技術力向上にアウトプットが効くと聞いているが始め方が分からない人から、アウトプット文化の無いところに文化を創りたい人まで、色んな カジュアル面談 をしてきました。その中で明らかになった 2 つの階段についてお届けします。

Hatena Engineer Seminar #19 「カクヨム編」

www.slideshare.net

20 分トーク。会のテーマは「カクヨム」。その中でも、技術の話は他の人がやってくれるので、僕は開発チームの暮らしぶりにフォーカスしたい。

一番話したいのは昼会が 1h あること。これを短くできないか悩みながら 1h を維持することを選択しているので、この悩んだ過程を伝えたい。

リモートワーク下での雑談の必要性を「毛づくろい」に例えた話を以前どこかで見たことがあったので、タイトルと開始直後に「社会的グルーミング」というワードを打ち出して、必要性に権威があるっぽく見せかけることにした。*3

毛づくろいと雑談の対比構造だけだと LT で消化してしまう程度の話題なので、20 分トークにするにはもう 1-2 個盛り上がりを入れたい。

そこで

  • 時間軸の違うそれぞれの雑談
    • デイリー、スプリントごと、四半期ごとで、それぞれ喋る時間軸の違い
  • 雑談とはつまり何なのか
    • 相手の反応を想像できるようになる行為
    • ボケ、ツッコミ、客

という 2 つの話題を追加した。

時間軸を変えると視座が変わり、視点を変えられる、というのは以前ブログに書いたが、この視座の変化を日常の中で回しているのは、安定して技術ロードマップを敷けるチームの特徴なので語っておきたい。

tech.drecom.co.jp

雑談から生まれる関係性というのは、記憶にあったこのツイートをイメージしていた。

我々はハイコンテキストな会話をよく行う。つまりコンテキストを理解していないと盛り上がらない、話す側も安心して話題を放り込めない。ボケ、ツッコミ、客の役割が成立している安心感が必要。

「雑談」というワードから、そもそも雑談は社会的グルーミング行為なので必要なのだと断言し、技術ロードマップ、メンバー間の関係性と、色んな種類の雑談の必要性を畳みかける、というストーリー。膝を打つシーンが何度かあるんじゃないかと思う。*4

id:onk カクヨムの開発チームではスクラムベースの開発プロセスを回していますが、昼会、スプリント会 (プランニングとレトロスペクティブを同時に行っています) に特徴的な工夫があります。現在のアジェンダに落ち着くまでの変遷と、このチームだから成立している「グルーミング」についてお話しします。

YAPC::Japan::Online 2022

www.slideshare.net

20 分トーク

かなり難産だった。作ったアプリケーションはある *5 んだけど、単発の施策であって、ストーリーが無い。

2 週間ぐらいこねくり回し続けていたら「フロー情報をまとめて放流し直すことで定着させる」が、このアプリケーションでのみ行われているわけではなく、他の施策にも適用されていて、繰り返し構造があることに気づけた。

社内勉強会によるアウトプットも、ブログによるアウトプットも、オープンソース活動も、すべて複数回触れる機会があるようにしている、というのは面白いテーマだろう。

これで

  • アプリの説明
  • 繰り返し構造の説明

で 2 テーマ、もう 1 テーマ欲しいので、最近の技術ブログの動向の一つでもある「ハブ」をくっつけて、全体を「個人か会社かは対立構造ではない」を軸としながら整えた。

僕らはインターネット上で開発の知見を得ることによってサービスを開発・運営できているので、インターネットに還元したい。そんな気持ちから、会社でもアウトプット (登壇や技術ブログ、執筆、OSS 活動等) が推奨されています。 社としての技術ブログも存在しますが、スタッフ個人のブログを通じて発信するのも同じように推奨していきたい。エンジニアの個人ブランディングも大事だと考えているし、自分の場所の方が書きやすいというのも感じているからです。 その上で、社内外の色んなサービスに散らばった技術 Tips を上手くまとめて再放流することで、手軽に情報を摂取し、技術的好奇心を満たし、成長し続けられる環境を用意したい。 そんな、個人の集合体としての技術コミュニティを運営する方法と、そのために開発したアプリケーションについて紹介します。

発表時間とテーマ数

以上のように、僕は 20 分トークでは 3 テーマを盛り込んでいることが多い。

LT だと 1 テーマで駆け抜けられる。20 分トークは LT 4 本分の時間があるので、素朴には 4 つの LT をすれば完成する。が、テーマ間の関連性を示すためにも時間を使いたいので、3 テーマが良いんじゃないかと思っている。

テーマ間の関連が薄いただの羅列だと聞き手がトークの現在位置を見失うので、上手い関連付け (これを特にストーリーと僕は呼んでいそう) が必要だとも考えている。

僕の好きな構成

5 分トークだと

  1. 問題提起
  2. 考えられる解決法
  3. 実際にやったこと
  4. 途中で出会った問題
  5. ふりかえりとまとめ

いわゆる STAR フレームワークに、やった人しか語れない話題をひとつまみ。

20 分トークだと

  1. 問題提起
  2. 問題を 3 つに分解する *6
  3. 問題 A について
    1. 考えられる解決法
    2. 実際にやったこと
    3. 途中で出会った問題
  4. 問題 B について
    1. 考えられる解決法
    2. 実際にやったこと
    3. 途中で出会った問題
  5. 問題 C について
    1. 考えられる解決法
    2. 実際にやったこと
    3. 途中で出会った問題
  6. それぞれの問題に共通していた特性
  7. ふりかえりとまとめ

特に最後の「それぞれの問題に共通していた特性」がアハ体験になるような、事実を積み上げていったら新たな発見があった!という構成が書いていて/聞いていてテンションが上がる。問題や解決法の再定義とか、つまりどういうことか、とかが決まるとカッコイイ。*7

この流れが好きなのは、聞き手の脳内モデルと一致しやすいと思っているから。聞き手が知っている、想定できる解決策から開始して、思ってなかった着地点に連れて行かれる。着地点はいいワーディングになっていてインパクトが残る。

クライマックスをしっかり盛り上げるような構成が好きです。*8

まとめ

  • 聞き手を置いてきぼりにしない
  • 聞き手をダレさせない
  • 長いトークでも、5 分語れるテーマの組み合わせで考える
  • 羅列ではなく、テーマ間に関連性を見出せるように選ぶ
    • 僕は最終稿の 3 倍の量を文字にしてから、テーマに合うものだけ絞り出している *9
  • 聞き手の感情グラフを考えながら、一本通ったストーリーにする
    • クライマックスに計算した感動を用意する

*1:以前 登壇資料の作り方 - id:onk のはてなブログ にも書いたが、このときはあまり「考えていること」にフォーカスできていなかったので

*2:よく言われる「聞き手に持ち帰って貰いたいものを明確にする」を更にテクニックっぽくするとこの 2 つになりそう

*3:「毛づくろい」をカッコ良く言いたかったんだけど、「グルーミング」によくない意味が乗っかっちゃったので「毛づくろい」とそのまま言う方が望ましそう https://twitter.com/__gfx__/status/1598644538306097155

*4:id:seiunsky が輸入してくれたのは最高の出来事でした チーム内にテックな話題を話す場を作っておよそ半年が経ちました - SmartHR Tech Blog

*5:ブログから技術記事を抽出・集約してワイワイする - id:onk のはてなブログ

*6:やった順にフェーズを分けるのが一番簡単。納得感のある分け方になるように頭を捻る

*7: は綺麗に決まったプレゼンたち

*8:似た話題を id:Pasta-K がしていた。この感情グラフのイメージを僕も意識している Nota Tech Conf 2022 Springの"全員登壇"を支えた技術 - Helpfeel Developers' Blog

*9:懇親会でトークの関連として語れる話題が 2 倍あるので、引き出しの多い人っぽい振る舞いができてお得

今年買ったもの2022

毎年書いていて 4 回目なのでカテゴリ作った。今年買ったもの

さすがにリモートワーク始めて 3 年経つとだいたい揃っちゃったな。今年はあんまり買ってない (と思う)。買った順です。

git push -f が更に安全になる --force-if-includes

歴史改変、してますか?

私は歴史改変が大好きで、毎日 rebase しています。なので割と毎日 git push -f することになっています。

口で -f と言っても、実際には --force-with-lease --force-if-includes をしているので、これらのオプションのご紹介。

この記事は はてなエンジニア Advent Calendar 2022 の 18 日目です。昨日は id:rokoucha さんで 壊れたデータベースとの向きあいかた - rokoucha でした。

qiita.com

-f の危険性

...--F--G--H   <-- main

という状態で push した後、H をコミットし直したとしよう。

...--F--G--H'  <-- main
         \
          H   <-- origin/main

このまま H' (main) を origin/main に push したいが、改変しているので「fast-forward ではない」と怒られます。

$ git push origin HEAD
To github.com:onk/foo.git
 ! [rejected]        HEAD -> main (non-fast-forward)
error: failed to push some refs to 'github.com:onk/foo.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

ここで push -f をするんですが、もし origin 側に他人のコミット I が更に積まれていたら。

...--F--G--H'  <-- main
         \
          H--I   <-- origin/main

この状態で push -f すると I は消えて無くなります。H を書き換えるつもりはあったが、I を闇に葬るつもりは無かった。

...--F--G--H'  <-- main, origin/main
         \
          H--I   <-- ???

このように、-f はローカルを強制的にリモートに同期するので、安易に使うと共用リポジトリを頻繁に破壊できます。

少し安全になる --force-with-lease

自分の知らないコミットがあると失敗するようになるオプションです。

先ほどと同じように、origin/main 側に気づかないうちに I が積まれている場合。

...--F--G--H'  <-- main
         \
          H--I   <-- origin/main

この状態で push --force-with-lease すると、以下のように怒ってくれます。

$ git push --force-with-lease origin HEAD
To github.com:onk/foo.git
 ! [rejected]        HEAD -> main (stale info)
error: failed to push some refs to 'github.com:onk/foo.git'

stale info、古くて使えないと言われています。何を比べて古いのかというと、実はローカルブランチ main、リモートブランチ origin/main だけではなく、ローカルの .git の中に remotes/origin/main というブランチがあり、この remotes/origin/mainorigin/main より古いと怒られているのです。

...--F--G--H'  <-- main
         \
          H    <-- remotes/origin/main
           \
            I  <-- origin/main
  • ローカルブランチ: H'
  • ローカルのリポジトリが知っているリモートブランチ: H
  • リモートブランチ: I

この「ローカルのリポジトリが知っているリモートブランチ」は .git/refs/remotes/origin/ に並んでいるので、眺めてみてください。オススメは .git の中で git init して、 git fetchgit push したときに何が変わるのかを diff の形で眺めてみることです。コミットハッシュへの参照なんだな、というのがよく分かると思います。*1

ローカルのリポジトリが知っているリモートブランチは git fetch で origin と同期されます。

$ git fetch
remote: Enumerating objects: 4, done.
remote: Counting objects: 100% (4/4), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 3 (delta 0), pack-reused 0
Unpacking objects: 100% (3/3), 283 bytes | 283.00 KiB/s, done.
From github.com:onk/foo
   8f51241..f953a86  main       -> origin/main

fetch したことで、remotes/origin/mainorigin/main と同じく I を指すようになりました。

...--F--G--H'  <-- main
         \
          H--I  <-- remotes/origin/main, origin/main

ローカルのリポジトリが知っているリモートブランチと実際のリモートブランチとが揃っていると、push --force-with-lease は成功し、 I は闇に消えていきます。

...--F--G--H'  <-- main, remotes/origin/main, origin/main
         \
          H--I  <-- ???

このように、手癖で push -f 前に git fetch してしまった場合等では、--force-with-lease は他人のコミットを守ってくれません。

更に安全になる --force-if-includes

Git v2.30.0 で増えたオプションです。更に安全になります。

...--F--G--H'  <-- main
         \
          H--I  <-- remotes/origin/main, origin/main

--force-if-includes の場合、--force-with-lease では push できてしまっていた remotes/origin/mainorigin/main が揃っている場合でも、reflog に remotes/origin/main (I) が含まれていること、をチェックして撥ねてくれます。面白い条件だと思う。

$ git push --force-with-lease --force-if-includes origin HEAD
To github.com:onk/foo.git
 ! [rejected]        HEAD -> main (remote ref updated since checkout)
error: failed to push some refs to 'github.com:onk/foo.git'
hint: Updates were rejected because the tip of the remote-tracking
hint: branch has been updated since the last checkout. You may want
hint: to integrate those changes locally (e.g., 'git pull ...')
hint: before forcing an update.

ここまでやっておくと、日常的に rebase && push -f していても闇に飲まれる他人のコミットは無くなり、安全になります。

というわけで、口では push -f と言っているけど、安全なチーム開発を意識している人は --force-with-lease --force-if-includes のことを指していて、本当に push -f していることは無いんだよ、という説明でした。

はてなエンジニア Advent Calendar 2022、明日は id:tokizuoh さんです。

参考 URL

*1:この技はドリコムの社内勉強会で id:sue445 に教えて貰いました https://sue445.hatenablog.com/entry/2013/06/26/012409