個人開発

「撮ったのに見えない」を構造で防ぐ — Windows スクショツール開発記 #3 アーキテクチャの話

中心と周辺ノードのハブ構造を表現した抽象イラスト
「撮ったのに見えない」を構造で防ぐ、所有関係の話

先に結論

検索でたどり着いた方向けに、要点だけ。

  • スクショアプリで「撮ったはずなのに、なぜか一覧に出てこない」というバグが出た
  • 原因はシンプルで、撮影した画像の保存先と、一覧が見ている保存先がズレていたこと
  • ここを根っこで防ぐために、撮影の取りまとめ役のクラスが、保存先の持ち主になる設計に揃えた
  • 撮影や表示を担当する周辺のクラスは、保存先を自分で抱え込まずに、毎回その取りまとめ役から借りに行くようにした
  • 設定で保存先が変わった瞬間に、取りまとめ役が新しい保存先を持つようにすれば、周辺は自然に追従する

連載の他記事

「撮ったのに見えない」バグ

ある日、こんな状態に遭遇しました。

  • ブラウザのページをスクロール撮影する機能を使う
  • 完了通知は出る、保存処理もエラーにならない
  • でも、ツールのワークスペース(過去のスクショ一覧)を開いても、その画像が出てこない

PNG ファイルはどこかに保存されている、けど一覧には現れない。直感的にはなかなか厄介な部類のバグです。

原因はとてもシンプルだった

調べていくと、原因はわりとシンプルでした。

撮影機能を呼び出す側が、保存先を管理するクラスを独自に作って使っていたんですね。一方で、ワークスペースの一覧側は、別のインスタンスを見ていました。

つまり、「保存先」がどこを指しているかが、撮影側と表示側でバラバラになっていた、ということです。

撮ったあと、ファイル自体は撮影側が思っている保存先にちゃんと書き込まれている。でも、表示側はそこを見ていないので、表示されない。理屈としては「ですよね」という話で、ユーザー目線で見ると「撮ったのに見えない」というだけの体験になります。

場当たり的に直すか、構造で直すか

ここで選択肢が 2 つ出てきます。

ひとつは、今回のバグだけを場当たり的に直すやり方。撮影側の処理を「正しい保存先」に書き換えれば、それだけで現象は止まります。

もうひとつは、そもそもこの種のバグが出にくい構造にするやり方。今回はたまたまブラウザのスクロール撮影でこれが出たわけですが、同じパターン(保存先がパーツごとにズレる)は、機能を増やすたびに再発する可能性があります。

このふたつを比べたとき、長く付き合うアプリだとしたら、後者のほうが結果的に楽になりそうな気がしました。なので、構造で直す方向に倒すことにしました。

撮影の取りまとめ役を「保存先の持ち主」にする

取った方針は、こんな感じです。

ツールの中には、撮影機能の取りまとめ役と呼べるクラスがあります(コード上は Coordinator にあたるクラスです)。撮影のホットキーを受け取ったり、どの撮影方法を呼ぶか振り分けたりする、いわば撮影機能の司令塔のような存在ですね。

このクラスに、保存先を管理するクラスのインスタンスを持たせるようにしました。ツールの中で「保存先はここ」を定義する場所は、ここ 1 つだけ、と決めるイメージです。

そして、撮影や表示を担当するほかのクラスは、自前で保存先を抱え込まずに、必要なときに Coordinator から借りに行くようにしました。トレイ常駐の UI も、ブラウザ撮影の案内ウィンドウも、過去画像の一覧も、保存先を直接知っているのではなく、_coordinator.FileStore のような形で都度受け取るようにしています。

こうしておくと、保存先のズレ自体が そもそも起きえない構造 になります。

設定で保存先を変えたときの挙動

このスクショツールは、ユーザーが設定で保存先のフォルダを自由に変えられます。デフォルトは Windows のローカルアプリデータ配下ですが、好きな場所に変更できる、というやつです。

ここでも、取りまとめ役が保存先の持ち主、というルールが生きてきます。

設定が変わったタイミングで、Coordinator.UpdateSettings(newSettings) のような形で 保存先のクラスを新しいインスタンスに差し替えるだけで済みます。周辺のクラスは、相変わらず「必要なときに借りに行く」だけなので、何もしなくても自然に新しい保存先を見るようになります。

たとえば、撮影直後に「ワークスペースを開いて、過去画像の一覧を見る」ような動きをしたとします。このとき、ワークスペース側はその場で取りまとめ役に「いまの保存先どこですか」と聞きに行くので、設定変更がすぐ反映される、という挙動が自然と手に入ります。

ありがちな落とし穴は「自分で覚えておく」こと

このパターンで意外と大事になるのが、「保存先を自分で覚え込まない」 という意識です。

たとえば、ある UI クラスが起動時に取りまとめ役から保存先を 1 度借りて、それを自分のフィールドにずっと持ち続けてしまうと、設定変更後にそのフィールドが古い参照のままになります。表面的にはちゃんと借りに行っているように見えて、結局またズレが発生する、というやつですね。

なので、内部の緩めのルールとして「保存先を持ちたいときは、_coordinator.FileStore のように getter 経由で毎回取得する」というのを意識しています。実装の手触りで言うと、ローカル変数にキャッシュする書き方をやめる、という感覚に近いです。

構造で直してよかった点

いまのところの体感ですが、この方針に揃えてからは、

  • 新しい撮影経路を足すときに、保存先の取り回しで悩むことがほぼなくなった
  • 設定変更にまつわるバグの再発が減った
  • 撮影、保存、表示、それぞれのクラスの責務がスッキリした

というあたりがありがたく感じます。「1 つだけ起きたバグを、似たバグが起きない構造に翻訳する」という考え方は、機能が増えていくフェーズで意外と長く助けてくれそうだな、という気がしています。

振り返り

今回のバグから持ち帰った、ざっくりした学びはこんな感じです。

  • 「ファイルはあるのに見えない」系のバグは、保存先が複数の場所で別々に管理されているときに出やすい
  • 場当たり修正と構造的な修正、迷ったときは 「機能を増やすときに同じバグが再発するか」 を一度考えてみると、自分の中で判断がつきやすかった
  • 「持ち主は 1 か所、周辺は借りに行く」というパターンは、設定変更まわりの取り回しと相性が良いように感じました

次の #4 では、もう少し全体の話に戻って、MVP で やる / やらないの取捨選択 をどう決めたか、それからリリースに向けて CI でセキュリティ原則をどう守るか、というあたりについて書きます。