はじめに
久しぶりの記事執筆になります。ご無沙汰しております。
思い返すと昨年度、2022年度の個人開発について全く記事を書いていないと思い、個人開発者としての活動実績を残しておこうということで重い腰を上げて記事を書きます。
2022年の7月末から8月にかけての、時間という時間のほぼすべてをかけて開発をしておりましたアプリケーションの成果発表です。
夏休みでフルコミットできる期間の1ヶ月をまるまる投入したプロダクトということで、今のところ自分史上の最高傑作かなぁというプロダクトのため、色々なところで話のネタにさせてもらっています。
例えば、だいぶ前の話になってしまいましたが(記事を書くのが遅い!)、未完Foundationさん主催のオンラインLT会「U25自主開発エンジニアミートアップ 「App Dev Meetup Vol.1」
」の方でも話させていただきました。地元の開発者の皆様の話を聞けるよい機会でありました。
こちらでは、主に事前設定テーマ「個人開発で得た知見」に沿って、また時間の都合上、特定の技術ドメインに依存しないような運用フェーズでの体験談が中心でしたが、記事としては成約も少ないため、それ以上に色々なことを書きとめておこうと思います。
アプリケーション概要
話し始めると何時間でも話せてしまいそうな気がするので先に概要を書いておきたいと思います。
もともとスライドだったものを記事に落とし込んでいるので、先に箇条書きか画像が来て、後に説明が来ます。
全体を簡単に説明すると?
- ミステリーツアーがしたい友達4人のための専用設計アプリ
- 通知と抽選で「未知でランダムな」旅行行程を都度提示
- 抽選結果の4端末の間での同期・排他制御が必要!
- 旅行中の時間経過に動作が依存する!
- 通知と抽選で「未知でランダムな」旅行行程を都度提示
- 実際にアプリを使い、友達のみで6日間の旅をしてもらう
- 時間・旅費を使っており一発勝負!致命的なバグを出すと即終了の危機!
- 6日間の間、本番運用・監視もしないといけない!
- 開発に使えるのは夏休みの1ヶ月だけ
- Androidアプリ + バックエンドAPI、すべて C# で記述
- フル構成のアプリ一個作るにしては期間が短い!
機能面
サイコロ形式の抽選とスマホの通知で、次の行程を直前になってはじめて、逐次決定・発表します。本当に目的地・行程は行くまでわかりません。
抽選で行程を決める要素があるため、4端末での同期と、行程の矛盾を防ぐ制御が必要です。
逆に、この2つの達成がロジック的な目標点であるアプリケーションです。
難しさ
また、実際の時刻に動作が依存するため、工夫しないとデバッグがしづらいという難しさがあります。
実際にアプリだけを頼りに、非エンジニアの友人が旅行に出る企画のため、一発勝負で、アプリの致命的なバグがあると旅行自体がおじゃんになる緊張感があります。
もちろん、実際にやる以上、サーバーの運用やログの監視なども必要でした。
このへんは、最初から予想はされていたので設計で解決します。実際、友人4人の時間(6日間の休み)とお金(4万4千円)を使っている割には一発勝負という、かなり緊張感があるというか、何で「やる」って言ったのか今思うとわかんないプロジェクトです。
技術面
アプリの構想からタイムリミットまでちょうど一ヶ月しかなく、工数カットとやりたいことの優先度付けがかなり重要になってきます。
自分の技術スタック的な事情や、フロントエンド+バックエンドを単一の言語で実装したいという事情を考慮して、一番早く実装できると思われる C# 採用です。
全体構成図
フロントエンド-バックエンド接続
フロントエンド主導でデータのやりとりをする場合は普通にバックエンドの web api を叩きます。
バックエンド主体でデータを送りつけたい場合(つまり通知や他ユーザーの操作の反映をする場合)、FCM経由で送りつけることができます。
双方向に結ぶことで自由度は高いアーキテクチャです。ちょっと複雑なのが難点。
フロントエンド
Android の Java の API のかなり薄いラッパーの Xamarin.Android 採用です。最近、ついに Xamarin
という名称の使用終了がアナウンスされ、今度は単にコンパイルターゲットの1つとなるようです。まぁ名前の問題で中身はアレですが、最新の言語バージョンやコンパイラツールチェインに対応することになるのでいい話ではあります。
なんでネイティブ?って話ですが、個人開発で「ネイティブアプリケーション」を選択してる場合、結構苦渋の選択というか、Webアプリケーションという、究極のマルチプラットフォームでインストールやアップデートという概念がなくユーザー側の導入作業がゼロ、配布コストもゼロ(SPAとかなら静的ホスティングで済むので)、という選択がある中でわざわざネイティブを選択してる時点で、プラットフォーム固有のAPI叩きたいとか、まぁいわゆるマルチプラットフォーム系のフレームワークの守備範囲をはみ出す要件が入ってることが多いよね、そうなると結局ネイティブの方が楽じゃない?という意見です。
最終的に情報が手に入るかっていう部分は結構大事で、マイナーAPIとかでも公式のドキュメントが https://developer.android.com/ に大抵まとまっていて、実践的な(〇〇する方法)みたいなのも多く、加えて何か上手くいかなくても解決しやすい、というあたりのコストを込みで評価するとガワはネイティブで作るのが結局確実で早いなぁと、現時点での経験上思っています。まぁこの辺は色々考え方があると思いますので、意見の一つとして。
共通コード
フロントエンド・バックエンドでロジックやモデルを共通化しています。最初からこれがやりたかったなぁというところはあります。
データモデルをクラスで表現しておいて、「これがAPI定義じゃ」みたいなことができます。
あと、フロントエンドとバックエンドの垣根を超えて、「クライアントが圏外だったらフロントエンドでやる」みたいなことができます。
加えて単純にコード行を削れます。もちろん時間も削れます。ソロ開発の場合は特に共通化の恩恵がでかいです。
バックエンド
クライアントが4端末あるのでその同期と調停をとるのが最大の目的です。
リレーショナルデータベースを持っておき、サイコロを振る抽選処理が「以上でもなく以下でもなく、確実に1回だけ」実行されることを担保します。ロックとトランザクションです。
加えて、進捗と生ログを discord に流す機能が入ってます。都合が合わず参加できなかった友人も観衆として一緒に楽しめるほか、
私自身もデバイスを問わず動作状況が簡単に確認できます。これが結構後から効いてきます。
インフラ
運用期間が「旅行6日+その前後」で高々2週間程度と、そんなにコストが気にならないこと、とにかく時間がないこと、小規模であることから、開発効率最重視でクラウド、特に C# 開発環境と相性のいい Azure 採用です。Azure App Service は実行環境まで面倒見てくれるので、IDE上でボタンぽちぽちするだけでデプロイでき便利です。DBのインスタンスも勝手に作ってくれます。慣れれば5分くらいでデプロイできると思います。あと、運用まで本気でやるのは初でしたが、管理コンソールが優秀だなぁと思いました、UI見やすいですし、ちょっとしたSQLエディタとビジュアライザまでついてまして。
実際の画面デモ
以下では、実際に本番運用したときのスクリーンショットを紹介します。
一部想定外の動作(表示の見切れなど)が発生している箇所もありますが、あえてそのまま掲載しています。
全体的な意図として、旅行のエンタメ性を意識した、ワクワクするようなUIを作りたいというのがあります。
ただし、時間はないというか、コアロジック部の時間を削るわけにはいかない以上、UIはある期間で開発打ち切り、という感じになります。
デザイン面は得意ではないこともあり、「それっぽいUIを効率よく作る」という方向性が入ってます。
ホーム
現在の行程を確認できるホーム画面の様子。次の行程を確認できる場合はここから画面遷移できます。
旅行アプリということで、きっぷ風のレイアウトを採用。
タイムライン
過去の行程を時系列で確認できるタイムライン画面の様子。
見逃し確認や旅行の振り返りで使用することを想定。
行程発表
行程発表のテキストを確認する画面の様子。
スクリーンショットでは伝わらないですが、開封時にはアニメーションがつくなど、面白さを意識した構成。
アニメーションの様子はちょっと下の方にある git で確認できます。
行程抽選(サイコロ)
次の行程を抽選する画面の様子。
ここからサイコロを振るCG画面に遷移し、行程が確定すします。
下の画像はアニメーションgifとなっています。
Discord連携
サイコロを振った結果は、discordに自動的に送信されます。
何を得たか
緊張感もある中でアプリケーション全体を一人で設計から運用まで行うのは初めてで、とても多くの学びがあったので、まず一部だけ紹介すると...
反省点
- 機能全体を設計するのは難しい!「フロントエンドができる」 AND 「バックエンドができる」 == 「全体が書ける」ではない!
- フロントエンドとバックエンドの接合部分や両者の役割分担など、全体を考える難しさ
- 一般ユーザーは開発者がテスト時に想定していない操作をすることがある!
- 旅行前日の運用開始時でバグが発生、失敗を覚悟した。久しぶりに「悔しさ」を味わった。結局徹夜で直した。
- ユーザーが動作途中でアプリを終了すると進行不能になるバグやandroidバージョンに起因する不具合が前日に発覚
- テストで意識するとともにロジック層で一貫性を担保すべき!
- 久しぶりに「悔しい!」という味を味わった、まだまだ勉強せねば!とモチベになったともいえる
- 旅行前日の運用開始時でバグが発生、失敗を覚悟した。久しぶりに「悔しさ」を味わった。結局徹夜で直した。
成功点
- 時間依存の処理をDIで抽象化したのは戦略として成功した!
- テストに6日間かかるんじゃ話にならない、時間取得とタイマーを抽象化
- ログ・進捗を自分宛discordに流す機能も正解だった!
- バグ出るかも?という怖さは運用中常に付きまとう
- 6日間の運用中、自分もそれなりに生活がある、スマホ・PC含む全デバイスで閲覧できて通知が飛んでくる discord は結構向いてるかも
アプリケーションの目的
概要は説明しきったので、備忘録的な意味も兼ねて考えていたことやエピソードなどを具体的に書き連ねていきます。
ここから話の脱線・自分語り・主観的な意見などが増えます。
電子的である意味
開発者が何言ってるんだよと言われそうですが、今回の知見の一つに「不必要に電子化するのは得策ではない」があると思います。
今回のアプリケーションは電子化する明確な必要性があったと思っているのですが、それを説明する前に少ししゃべります。
なんだかんだでコードを書いて8年くらいが経ち、まぁそれは結構な数のコードを書いてきました。
しかし、こうも「責任」のあるプロジェクトというのは今回が初めてなのではないかと思いました。
個人開発あるあるではあると思うのですが、対象とするユーザーが大抵自分だけか、そもそも存在しません。
想定ユーザーが広いというのも、また結局誰にも刺さらないプロダクトになって誰も使ってくれない。そもそも目にすら止まらないということになり、結局対象ユーザーはいないということになりがちです。
結果的に、作ったら作りっぱなしというか、コードとビルド済みバイナリと(時には)ドキュメントとをリポジトリに置いておくだけ、という終着点を迎えるプロジェクトがかなり多いです。別にissue飛んでくれば対応するんですが、そうそう飛んで来ないので、おいてあるだけということが多くなります。
ほかのケースだと、何か締め切りがあるタイプ、学内イベント向けとかハッカソンとか、そういう開発もありました。
しかし、これらも成果物だけが求められる、つまりそのアプリケーションが生成するファイルが得られることが要件だったり、発表時点でデモが披露できることが要件だったりというケースが多い。「コードの正しさ」を要求されるシーンって案外少なかったなぁと思います。
話が長くなりましたが、直接的には、ある程度のスパンの期間アプリケーションが正常に稼働すること、すなわち、コード自体が正しいこと、が要求されるというのが斬新っちゃ斬新です。そして、それが想像以上に大変である。今回のアプリケーション、正直そんなにヘンな機能はなくて、通知が飛んでDBのCRUDがあって、くらいのありがちプロジェクトなんですが、それでも動作保証できるだけの自信があるかといわれると、完全にないなぁという認識です。
加えて、今回ユーザーが限定なので特に認証とかはないですし、DB上にも抜かれて困るようなデータはないんですが、実社会プログラミングだとそうもいかない、というのもあるでしょう。ユーザー体験が悪いとかなら謝って済むかもしれませんが、金銭的な損失を出すとか組織の評判を落とすとか、そういう種類の責任も絡むことになりえます。となれば、ますます電子化することにメリットがないならやめといた方がいいんじゃない?というふうに提案すると思います。
昨今DXなどと掲げて、とにかく電子化するのが一部ではやっている気がするのですが、この経験から「本当に必要?」と思うものが増えました。全国旅行支援の電子クーポン券事業とか。
なぜ電子化したのか
少し思い出話と思想強めの話をしてしまいましたが、話題を戻しましょう。なぜ今回電子化したのか?というところですが、大きく2つあります。
まず、「サイコロにある程度干渉したい」というところです。
さて、このプロジェクト、某TV番組にインスパイアされていますが、TVと違っていろいろと制約が付きます。
まず学生の仲間内でやっているので予算要件が厳しいです。ある程度予算に見通しがつくこと、そしてそれが安いことが要求されます。繰り返しにはなりますが、6日間で4.4万円しかないのです。
元ネタのTV番組のように帰れなかったのでとりあえず飛行機で帰る、みたいなことをするとその正規運賃の飛行機代だけで予算が消し飛びます。
あと、とにかく駅前とかその辺にあるホテルで泊まる、みたいなのをしてもやっぱり予算が足りません。
というわけで、「6日後にちゃんと東京に戻っていること」、「ネット予約で安く取っておいた宿泊地点に辿り着くこと」(逆に言えば、予算的に許容できる金額の宿泊施設等が存在する都市で一日が終了すること) をゆるく保障するしくみが欲しいです。
そこで、最終的な目的地や宿泊地点はあらかじめ開発側で候補を出してその中から選んで決めておく、という仕様になっています。ストーリー展開が変わっても結局結末やボスは不変というところで、ゲームのようなイメージをしてもらえると近いと思います。
で、アプリなのでどのサイコロがどういう風に制御されているかをユーザー側から隠ぺいできます。すでに結果が決まっているサイコロがあったとしても、そうでないものと外見は同一、オブジェクト指向的な考え方をしているわけです。
実際に開発中に書き残していた設計資料のmarkdownの文章を引用すると
TVにおける抽選形式の旅番組も、放送以前に収録が行われているという観点では、結果は確定しているが
それがTV局内部以外の人には知るよしがないため、あたかも放送時点に抽選が実行されたかのように視聴者側には振る舞う、
という特性を着想にしたものである。
次に、「全部知っている人間がいると面白くない」という意見が出たことです。
このプロジェクトの少し前、今回の旅行の参加者のうち一人を、どこに行くのか説明しないで予想してもらいながら近場の温泉に連れ出したことがあったのですが(このプロジェクトと直接は関係ないです)、そのフィードバックとして、「隣に全部知ってる人間がいるのが冷める」というようなものでした。少し悲しいところではありますが、わからないでもないので自分不在でやりたいです。
そこで、通知機能に頼っています。次の行程の10分前を目安に、各行程ごとに通知時刻を設定し、その時間にスマートフォンの通知で次の行程があることを知らせ、サイコロを振ってもらったり内容を確認してもらったりします。
当日になっても、即どこに行くのかわかるわけではなく、少しずつ行程を踏みながら予想していく楽しみが提供できるのが利点というわけです。
アプリケーションの設計
今回のプロジェクト、ソロ開発でしかも時間に追われる展開ですが、まず設計から着手することになりました。
理由の一つは、ちゃんと設計しないと結局品質低下や手戻りを招いて時間が増大すること、ほかにはプロジェクトが結構巨大なため、何から優先度をつけて着手するのか明確にすることがありました。
まず境界線から
設計で何から始めるか、という話ですが、個人開発の場合「境界線」から始めることが多いです。
今回のようなアプリケーションのケースであれば、各プラットフォーム(今回はAndroid)が提供するAPIが境界線になるでしょう。何かライブラリや外部APIへの依存関係がアプリケーションの中核になる場合はその仕様が境界線になります。
このような境界線の仕様を調べ、実装の算段を立てておくことが重要です。
今回であれば、正確な時刻にアプリケーションのロジックを開始し、通知を送信したいため、AlarmManager
の仕様などをまず調べます。
すると、端末の省電力機能に影響されず正確な時刻にロジックを起動する setExactAndAllowWhileIdle
メソッドが有用そうであることがわかります。
しかし、こちらはレート制限があるので、次点の方法として Firebase cloud messaging から優先度の高いメッセージを送出してロジックを起動する方法があることがわかります。
ローカルの方が信頼できると判断したため、ローカルを優先し、それで対応できない事例、つまりユーザーが通知に気づかなかった場合のリマインド目的にFCMを使用するアーキテクチャが決定しました。
ある程度慣れてくると、CPU依存の処理や一般的なディスクIO、ネットワークIOなどは当然困ることなくできるので、障害になりうるのは境界線です。この辺を具体的にイメージしておくと、後から修正が必要になる可能性を減らせます。
進行不能バグを産まないために
今回のアプリケーションでは、実際に時間・お金を使って旅行をしてもらう前提のため、旅行が進行不能になるバグ(次の旅程が表示されない、など)を防ぐ必要性があります。
しかし、この規模感のアプリケーションに一個もバグを作り込まないというのが、非常に困難であるということは開発者として常識だと思われ、バグを軽減するように設計・テストを行うと同時に、単一の障害点をへらす工夫を併用することになります。
今回のアプリケーションでは、運用期間・ユーザー数が限定されていることもあり、開発者側がログの監視を行いやすい状況を作成し、異変が発生した場合に開発者が介入するというフローが取りやすい形であり、これも意識していました。
実際の実装として
- 冗長化した通知配信システム。ローカルのalermmanager経由の通知配信を行っても、対応する操作が行われたとバックエンドに通知されなければ、バックエンドからFCM経由で通知を再送する。
- 重要度が高いログはすべてバックエンドと同期され、さらに開発者宛にdiscordで配信される。discordはすべてのデバイスで閲覧でき、プッシュ通知もあるので監視がしやすい。
- バックエンドのDBを優先する設計や、フロントエンドの状態の強制書き換えAPI(FCM経由で即時配信)を有し、開発者が手動介入できる土壌を用意
- 現在時刻の取得やタイマー処理部分をDIで抽象化。本番では6日間の時間経過に従って少しづつ旅程が進んでいくが、開発時は時間経過を早送りしてテストすることを可能に。
などの対策が設計時点で考案されていました。
データのモデル化
次に、アプリケーションで扱うデータをモデリングします。
色々悩んだ結果、今回は
- 旅程
- 東京7:00 => 大阪10:00 XXX号 のようなデータ(実際には構造化されている)
- このアプリケーションが扱うもっとも下層のデータ
- 表示可否などの状態を持つ(全部非表示、到着地は非表示、備考は非表示など)
- 指示
- 「XXXで大阪へ向かえ!」のような、自然言語での指示
- この指示を閲覧することで旅程の状態が変更される(非表示=>表示、など)
- 表示可否の状態を持つ
- 抽選
- 複数の指示の中から1つを選ぶサイコロ(1: 大阪,2: 仙台...など)
- この結果により、どれかの指示が有効化される。
というように、一方向の依存関係を持つデータ構造を採用しました。
JSONかなにかのフォーマットでこれらのリソースを作成したり、状態をRDBで管理する必要があり、循環参照を持ちたくなかったため、そもそも設計時点から一方向の参照で済むようにしてあります。
このデータ構造と、旅程定義json(とそのデコーダ)は、フロントエンド・バックエンド共通ライブラリ部に定義されており、このアプリケーションの中核を担っています。
UIは後の方に
UIの実装・設計は最後になります。時間をかけようと思うとどれだけでも使ってしまいそうですし。
今回は開発期間があまりないこともあり、できるだけ低コストなUI設計を意識しました。全体的に直線的な、直交座標系で表現できるようなデザインを採用したほか、サイコロを振る部分はフリー素材の動画を再生する実装になっており、できるだけ短い時間で、それでもユーザーが楽しめるUIを作成する工夫を入れています。
続き
とても記事が長くなって来ており、編集がしにくくなってきたため、このへんで一旦分割したいと思います。
実装フェーズやテストフェーズ、運用フェーズでの知見やエピソードなどは、次回以降で書き残したいと思いますのでお楽しみに。