綴Photo の勉強ノートに戻る
プロダクト設計2026.07.21

分けたはずの作業が分かれていなかった — スライスの独立性は「実装できるか」では測れない

設計同期スライス綴Photo

実装に着手する直前、設計書と現状のコードを AI と洗い直したら「このスライス、単体で入れたら壊れるな」が出てきた話です。手を動かす前に見つかったので実害はゼロ、ただ出てきた場所が自分の予想と違ったので記録しておきます。

ここで言う「web」は、綴ったものを web でも見られるように開発中のアプリ(紡Book)のことです。

何をやろうとしていたか

写真に綴った言葉を、選んだものだけ web 側へ出せるようにしている。この「公開する/やめる」の状態を、端末(手元のスマホアプリ)と web(ブラウザで見る側)の両方から変えられるようにする、と決めたのが発端だった。

いったんは「web は出す先だけ、外すのは端末から」と決めて閉じていた。ひっくり返したのは、web を眺めていて「この写真やっぱり要らない」と思ったときに、その場から外せないのが単純に困ったから。見ている場所と操作できる場所が違う、という状態が使っていて不自然だった。

書き手が2人になると、順番だけでは解けなくなります。設計書は AI と詰めながら書いていて、その冒頭にはこういう表を置いていた。

やり方 壊れ方
突き合わせを先 → 送信を後 端末で外したばかりの未送信の意図が、サーバの「まだ公開中」で上書きされて戻る
送信を先 → 突き合わせを後 web で外したのが、端末の主張で戻ってくる
印は2か所。未送信の意図は3つ目の箱ではない。端末 SQLite(閉じても残る)Photos テーブル / webSelected 列公開中 / してない の印1つserverId突き合わせる相手(サーバ側の番号)突き合わせサーバ DB(公開一覧)いま公開中の写真端末が「どれが公開中か」を聞きにいく先。ここに未送信は無い。「未送信の意図」は別テーブルに保存されない。同じ webSelected 列が、サーバへ届く前に false になった状態。意図ならこの列を上書きしてはいけない。キャッシュならサーバの一覧で上書きしてよい。

解き方も書いてあった。端末側の変更をその場で送ってしまえば、未送信の意図が存在しなくなるので、上の表の1行目が消えます。

スライスの割り方

作業を3つに割った。

  • Slice 1: 突き合わせ本体。サーバの公開一覧を取ってきて端末側を合わせる
  • Slice 2: 取り消しをその場で送る。今は端末のフラグを落とすだけで、サーバへは次の同期でまとめて届く形になっている
  • Slice 3: 送信量の削減(任意)

そして Slice 1 を先にやることにした。理由は「マイグレーションも判断も要らないから」。Slice 2 には決めごとが1つ残っていた——オフラインで取り消しに失敗したときどう見せるか、という世界観の判断で、これは自分が決める側の話だったので保留になっていた。

しかもこの判断は、必要な作業量まで左右した。端末では外して後からサーバへ送る形にするなら、「まだ送っていない取り消し」を見分ける列をデータベースに足す必要があります(突き合わせがその取り消しを踏み潰さないように、除外する目印が要ります)。判断が決まらないと、データベースをいじる作業が要るかどうかも決まらない状態だった。

判断が要らない方から着手する。当時はきれいな順序に見えた。

着手直前に洗い直して出てきたこと

実装に入る前に、設計書と現状のコードを AI と一緒に洗い直した。すると Slice 1 を単体で入れるとその日の未明に合格を確認したばかりの挙動が壊れることが出てきた。

いま取り消しは端末のフラグを落とすだけ。ここに突き合わせを先に入れると、こうなる。

同じ1枚について、同期のあいだにどっちが何を持っているか端末 SQLitePhotos.webSelected 列サーバ DB公開一覧公開やめるfalse(まだ送っていない)別の列には書いていないまだ公開中① 突き合わせtrue に戻される「まだ公開中」と答える② 送信「公開中です」と主張公開中のまま取り消しが、なかったことになる。未送信の意図は別テーブルに無い。突き合わせはサーバの一覧で端末の1列を上書きするので、未送信だと見分けられない。

その日の未明にサーバのデータベースまで降りて「取り消しがちゃんと届いている」を確認したところだった。それが静かに無効化されます。

そして厄介なのは、この壊れ方が設計書の冒頭の表の1行目そのものだということ。壊れ方は分かっていた。分かっていたのに、それを防ぐ仕掛け(取り消しをその場で送る)を Slice 2 に置いて、壊れる順序(突き合わせを先に)を Slice 1 に置いていた。設計書が、冒頭で自分たちが書いた予言を、後半の割り方で自分たちで破っていた。

何を間違えたのか

スライスを割るとき、無意識にこの基準を使っていた。

このスライスは、単体で実装できるか?

Slice 1 は単体で実装できます。サーバに問い合わせて、フラグを書き換えて、処理の順番を入れ替えます。他のスライスに依存するコードは1行もありません。だから独立していると判断した。

でも必要だったのはこっちだった。

このスライスは、単体で入れて安全か?

コードの呼び出しは無い。あるのは状態の前提。Slice 1 突き合わせサーバの公開一覧で端末の列を合わせる端末 SQLite の webSelected 列Slice 2 を呼ぶ行は無い→ 単体で実装できるSlice 2 取り消しをその場で送る未送信の意図を作らない防ぎ方はここに置いていた前提としては要る→ 単体で入れて安全にする側前提依存がコードに出ていないから、コードを見て割ると独立して見える。

この2つの基準は、たいていの場合は一致するので普段は気になりません。ズレるのは、片方が「状態についての前提」を提供しているときだった。

どう直したか

Slice 1 と 2 をまとめて1つにした。保留になっていた判断(オフラインで失敗したときどう見せるか)を先に決める必要があったので、AI に選択肢を2つ出してもらい、選ぶところは自分でやった。

  • A: 失敗したらエラーを出して、端末の表示も公開中に戻す
  • B: 端末では外して、サーバへは次の同期で送る

A にした。決め手は、B だと嘘の状態表示になること。B で「外しました」と出しても、写真はサーバに残ったまま公開され続けます。「引き上げた」と思わせて実際は出たまま、という状態が作れてしまいます。オフラインで外せない不便より、外れたと誤認させる方が重い。

副次的に、そもそもの問題も同時に消えた。A は失敗したら端末の表示も元に戻すので、端末に「まだ送っていない取り消し」が残りません。残らないなら、突き合わせが踏み潰す相手もいません。冒頭の表の1行目が、そもそも成立しなくなります。判断待ちだったマイグレーションも、要らないことが確定した。

学んだこと

  • スライスの独立性は「単体で実装できるか」ではなく「単体で入れて安全か」で判定します。前者はコードの依存を見れば分かりますが、後者は「そのスライスが何を前提にしているか」を見ないと分かりません
  • 着手前に1問だけ確かめます。「これを入れた瞬間、すでに通っている検証が壊れないか」。今回はこれを実装前の洗い直しに入れていたから出てきた。勘で気づいたわけではない
  • 設計書に「壊れ方」を書いたら、その防ぎ方と壊れる順序が同じスライスに入っているかまで確認します。壊れ方を分かっていても、割り方を間違えるとそのまま踏みます
  • 実装も設計書も AI と進めているが、「端末とサーバのどっちを正とするか」「嘘の状態表示を作らないか」は自分が決めるしかない。A を選んだのはそこだった

この記事は2026年7月時点の自分の理解で書いています。仕様や認識が変わってる可能性があります。