この記事は 食べログアドベントカレンダー2024 の11日目の記事です🎅🎄
こんにちは。食べログ開発本部 アプリ開発部の筒井です。普段は食べログiOSアプリの開発を担当し、日々機能改善に取り組んでおります。
私は新卒として食べログに参画してから2年目になり、今年も様々な機能開発に携わらせていただきました。本当は「全てが順風満帆で何も躓くことがない素晴らしいエンジニアになりました」という記事にしたかったのですが、現実はそう甘くありません。
この記事では恥ずかしながら今年度私が大きく遠回りをしてしまった開発経験と共に、そこから学んだ開発における回り道を減らす方法についての考えをお話しできればと思います。
遠回りした機能実装
始まり -トリミング機能の開発-
私はある日、食べログアプリに投稿写真のトリミングをする機能を実装することとなりました。
機能はざっくり以下のようなものです。
- トリミング範囲をユーザーが指で自由に変更できる
- 画像の回転ができる
- ユーザーが画像をズームしたり、スクロールしてトリミングする箇所を変更できる
特殊なものは1つもなく最近のアプリでは標準的に搭載している機能ばかりです。また、ありがたいことに食べログアプリには既に「画像を回転する機能」がありました。
既にあるなら簡単でしょうと思った私は早速、意気揚々と「画像の回転機能」の開発に取り組み始めました。
つまずき -実装方法に迷い続ける-
機能の画面構成はシンプルで、1つの画面に画像とトリミング範囲を表示するViewが乗っかっているような構成です。
実装にあたって私が真っ先に考えたことは「どのように画像を回転させるか」でした。既存の実装はCoreGraphicsを利用して画像を回転させていたので、これを用いて実装を検討します。
CoreGraphicsを用いた画像の回転は、描画領域の座標を計算し画像を指定した領域内に再描画する方法です。目に見えない座標を想像しながらの実装はなかなかうまく行かなかったため、既存の仕組みを諦めてアフィン変換による画像の回転も試しましたが、これも同じく座標計算が必要なため状況はあまり好転しません。
(イメージが付かなかったため、ノートに座標がどう移動するかを書きまくっていました。)
また、苦労して実装した後に他の要件を満たすよう動かしてみると一発で破綻しました。
さて、このような流れであれこれ検討しましたが最終的には全く異なるアプローチで回転機能を実現しました。
それは、画像やトリミング範囲などを載せた大元の画面自体を90度回転させる という方法でした。
改めて振り返るとかなりの時間、検討違いな方法を試し続けていました。
遠回りを避ける方法 -あるべき姿を考える-
どうすればまっすぐこの方法に辿り着けたでしょうか。
今振り返ると、「画像の回転機能」の実装に取り組み始めた時点で既に大きな遠回りをしていました。
「画像の回転機能」はあくまで機能の名称で、実装方法を示す言葉ではありません。
本来の欲しかったものは「画像を回転させる機能」ではなく「指定したトリミングの範囲はそのままで、トリミング範囲と画像が一緒に回転する機能」です。
実現すべき動作がどうあるべきなのかを明確にしないまま実装に進んだ結果、出口の無い検討を続けることとなったのでした。つまり、いきなり手を動かすのではなく機能のあるべき姿(条件や動作)を整理するところから始めるというのが回り道を防ぐ方法でした。
どうなっていたか想像してみる
まずはじめに実現する機能がどういった動きをするか、事実を整理します。
どうやら画像とトリミング範囲は一体となって動作し、回転の前後で切り抜くべき範囲は(ズームによる見た目の変化はあれど)変化しないことが前提となっているようです。
条件1:回転の前後で範囲は変化しない
条件2:画像とトリミング範囲は同じように回転する
この前提を踏まえて実装するにあたっての大まかな方針2つを検討します。
方針1:画像とトリミング範囲を個別で回転させる
方針2:両方まとめて回転する
つまずいた例とは異なり、条件2からまとめて回転する選択肢があると分かりました。
また、あるべき姿を整理したことで回転の前後で範囲は変化しないという条件が認識でき、方針1では回転前の状態と矛盾を発生させないように調整する必要があると分かります。
面倒に思った私は方針2を選ぶでしょう。
また、まとめて回転することによって画像とトリミング範囲は常に同じ向きとなるため、結果として他の要件を満たすためのロジック(座標制御やユーザーによる操作を実現するためのロジック)では回転状態を考慮する必要がほとんどなくなりそうです。
ありかたを事前に整理することの重要さ
以上のような経験から、事前に機能のあるべき姿を整理することの利点は2つあると学びました。
1. よりスムーズな実装
一度整理するタイミングを設けることで実装に取り掛かるタイミングは後になりますが、結果として手戻りの可能性を小さくできます。あり方を整理するおかげで実装方針も検討しやすく、また方針が間違っていたとしても立ち返る場所が明確であり実装方法を暗中模索する場合よりもスムーズに正しい道へ復帰できます。
実際は回転機能の後に他の要件「トリミング範囲をユーザーが指で自由に変更できる機能」の実装を行うのですがアプローチを変えた結果、再びつまづきながらも効果を実感することとなりました。
(例)座標制御はどうあるべきか
「トリミング範囲をユーザーの操作によって変化させる」と言っても自由に動かせるわけではないので、前回の反省を踏まえてあるべき姿を考え、トリミング範囲が満たすべき2つの条件があることが分かりました。
条件1:トリミング範囲は常に画像の描画範囲内に収まる
条件2:トリミング範囲は常にUI上動かせる領域内に収まる
これを踏まえて、実装方針として思いついた以下の2つを検討しました。
方針1:範囲変化時、それぞれの領域内にトリミング範囲が収まっているかを判定する
方針2:それぞれの領域内に収まるようトリミング範囲の変化量を調整する
実際は複雑な座標計算による計算誤差が実装上どうしても発生してしまうため、条件内の”常に”という部分を満たしづらいという点で方針1には若干問題がありましたが、安易な私は簡単そうな方針1を選択して再び躓くことになりました。ただし回転機能のケースとは違い、つまずいた際の問題点が明確で方針2の方がより適切だと気づくことができ、そちらの方針へとすぐさま切り替えることができました。
このケースでは条件を明確にしたおかげで大きく誤った方針を立てることもありませんでした。条件が不明瞭であれば、例えば無数にあるパターンを思いつくたびに1対1で対応するような実装をして時間を浪費していたかもしれません。また、事前にあるべき姿を明確にしておかなければ「誤差をなくす手段」を求めて永遠に彷徨い続けることとなったかも知れません。
結果として手戻りはあったものの、被害は最小限に食い止められたと考えています。
2. シンプルなものをシンプルなままに実装できる
大前提ケースにもよりますが、そういった場合が多いように感じます。
少なくとも適当に選んだ方法よりも良い方法を採用できることには間違いありません。
(例)トリミング範囲はどう変化するべきか
「トリミング範囲をユーザーの操作によって変化させる」場合について、今度は動作の側面から考えます。 整理した結果この機能は2つの動作から成るようです。
動作1:ユーザー操作によるトリミング範囲の更新
動作2:指定した範囲を拡大して操作しやすいように中央寄せする
動作1は非常にシンプルなので今回は割愛し、動作2について見ていきます。この動作についてあるべき姿を整理すると、回転と同様にトリミング範囲と画像は連動しており動作の前後で範囲は変更しないということにが分かります。
条件1:動作2の前後で範囲は変化しない
条件2:画像とトリミング範囲は同じように拡大+中央寄せされる
回転の時と全く同じような状況です。方針も同じようなものが検討できます。
方針1:画像とトリミング範囲を個別で拡大+中央寄せする
方針2:まとめて操作する
こうして同じように方針2を選択し、画像とトリミング範囲の相対的な位置関係は変化しないこと(条件1)に着目して、詳細は割愛しますが行数でいうとたったの2行でこの”連動”を実現できました。
仮に事前の整理がなければ非常にシンプルな動作1と動作2を合体させ、複雑極まりない実装を行なったかもしれません。また、あるべき動き方の検討がなければ方針2を思いつかないかも知れませんし、あるいは思いついたとしても実現方法が分からず、結局は複雑な方針1を選ばざるを得なかったかもしれません。
結果としてあるべき姿をそのまま実装に落とし込むことができ、非常にシンプルな方法で機能を実現できました。
まとめ
トリミング機能の実装という実例を通して、手を動かす前に実装すべき内容を整理整頓することの重要さについて書かせていただきました。
ここでは一例だけ書かせていただきましたが、他にも様々な案件に取り組ませていただき、日々学びを蓄積しています。当然この経験だけで完璧になるはずもなく、日々手戻りと格闘しながら少しずつ実践して定着を図っている最中です。
当たり前な内容だと感じた方もいらっしゃるとは思いますが、この記事を通して同じような悩みを持つ方の開発が少しでもスピードアップすれば幸いです。
私もこの経験・学びを活かして皆様により早く使いやすいアプリを届けられるよう、これからも日々開発に取り組んでいきます。
明日は 高田さんの「生成AIで自動テストを楽に作りたい!」です。お楽しみに!