はじめに
お疲れ様でございます。ハム二郎です![]()
本記事は、かつて自身の技術力不足により開発破綻した「短歌対戦アプリ」を、正しい設計思想とアーキテクチャに基づいて再構築した記録の 前編(設計・環境構築編) です。
以前、私はアイデア先行で開発に着手しましたが、コードの複雑化(スパゲッティ化)によりプロジェクトを凍結しました。その後、基礎的なアプリ開発を経て設計スキルを習得し、今回満を持してリベンジを行います。
本記事では、単に動くアプリを作ることではなく、「過去の失敗(技術的負債)をどう分析し、どのような設計思想で堅牢なシステムへ昇華させたか」 というエンジニアリングの過程に重点を置いて解説します。
開発背景
本アプリの開発に至るまでには、以下の3つのフェーズ(失敗・修行・再挑戦)がありました。ここで改めて経緯をまとめ、開発目的を再定義します。
1. 【失敗】短歌対戦アプリ(第1期)
〜アイデア先行による自滅〜
当初、私は既存のSNS等では代替できないニッチで独自的な体験(短歌特有のコミュニケーション)を提供したい、という思いから、いきなり短歌対戦アプリの開発に着手しました。
しかし、当時の私には設計の知識が皆無でした。
結果、機能追加が不可能な状態に陥り、開発を断念。「アイデアだけではシステムは完成しない」 という教訓を得ました。
2. 【修行】学習記録トラッカー(第2期)
〜「当たり前」を作るための基礎固め〜
前回の反省から、まずはシステム開発の定石を学び、自身の資格勉強で実用するためのツールとして、あえて一般的な「CRUDアプリ」を開発しました。
ここではMVCアーキテクチャやRDB設計(正規化) といった、業務システムに不可欠な基礎技術の習得に注力しました。
3. 【現在】短歌対戦アプリ(第3期・本作)
〜習得した設計力で「正解のない仕様」に挑む〜
学習記録トラッカーアプリの開発にて基礎を固めた、かつて挫折した「短歌アプリ」に再挑戦します。
本作のテーマは、ネットに正解コードがない 「独自の複雑な仕様」 を、習得した設計力を駆使して 「堅牢なアーキテクチャ」 に落とし込むことです。
目次
要件定義とスコープ
1. アプリケーション基本定義
| 項目 | 定義内容 |
|---|---|
| 名称(仮) | 大人短歌 |
| コンセプト | 伝統的な「短歌」の形式美 × 現代的な「風刺」 × 戦略的カードゲーム |
| ターゲット | 言葉遊びや大喜利を好む層 |
| プラットフォーム | Desktop App (Python/KivyMD) ※将来的なWeb API連携・マルチプラットフォーム化を前提とした設計 |
2. ゲームサイクルとルール設計
本アプリのコアとなるゲームルールは以下の通りです。
①基本フロー
1. 配札:
データベース上の語彙マスタから、各プレイヤーに手札が配られる。
2. 詠唱:
手札を組み合わせて「5・7・5・7・7」の短歌を作成する(字余り・字足らず・自由律も許容)。1枚場に出すごとに1枚山札から引く。
3. 判定:
完成した短歌に対し、システムと対戦相手が採点を行う。
4. 次局への反映:
相互評価の結果が、次のターンのバフ(有利効果)として還元される。
②勝敗判定ロジック(スコアリング)
勝敗は「システム採点」と「人間採点」の合計点で決定します。
| 採点項目 | 詳細仕様 | 実装方針 |
|---|---|---|
| ① 役ボーナス | ・特定語彙コンボ: 指定タグ(例: "社畜")揃いで加点 ・形式点: 57577の定型通りなら加点 ・レアリティ: レアカード使用で加点 |
if文の羅列を避け、独立した評価クラスとして実装しカプセル化する。 |
| ② AI採点 | 外部生成AI API (Gemini等) を使用し、短歌の「芸術性」「面白さ」を100点満点で評価。 | APIコスト制御のため、日次制限(Rate Limit)やサーキットブレーカーを設ける。 |
| ③ 相互評価 | 対戦相手が、その短歌の「納得感」を0〜100点で評価する。 ※ここでの評価値が後述の「カルマシステム」に影響する。 |
UI入力値をスコア計算ロジックへ注入する。 |
カルマシステム(AI共感度ボーナス)
相互評価の形骸化を防ぐため、**「AI審査員とのシンクロ率」**をゲームの鍵とします。
背景と課題:
AIの審美眼は完璧ではありません。しかし、人間同士の評価もまた、勝利への執着や好みの偏りによって歪められます。
そこで本アプリでは、AIを「絶対的な正解者」としてではなく、「公平だが、独自の癖を持つ第三の審査員」 として定義します。
実装ロジック:
プレイヤーには、対戦相手の短歌そのものの良し悪しに加え、「このAI審査員なら、この歌に何点をつけるか?」を予測する洞察力が求められます。
① 乖離度(Divergence)の算出
プレイヤーの入力した評価点($P_{score}$)と、AIが算出した評価点($AI_{score}$)の絶対差分を取ります。
$$Diff = | P_{score} - AI_{score} |$$
② 共感レート(Sync Rate)の決定
差分が小さい(=AIの感性を読み切り、近い評価を下した)ほど、次ターンのスコア倍率が高くなります。
$$SyncRate = 1.0 + \frac{(100 - Diff)}{200}$$
ゲーム性の変化:
プレイヤーは単に自分の感性で採点するのではなく、「AIはこういう古風な言い回しを好む傾向がある(から高得点だろう)」といったメタ的な読み合いを行うことになります。
これにより、「相手を蹴落とすための0点」は「AIの評価と乖離し、自分が損をするだけ」となるため、結果として極端な不正評価が抑制されます。
3. 機能要件
エンジニアリング観点での実装項目定義です。
【 ドメインロジック層 】
-
日本語形態素解析:
読み仮名変換およびモーラ(拍)計算ロジックの実装。 -
ステート管理:
ターン制進行を管理するStateMachineの実装。
【 データ永続化層 】
-
マスタ管理:
語彙、読み、タグ、レアリティを管理する正規化されたスキーマ設計。 -
トランザクション管理:
試合結果(Users, Matches, Scores)の整合性担保。
【 UI/UX層 】
-
ホットシート対応:
1台の端末での交互操作に対応したViewの切り替え制御。
4. 非機能要件
実務品質を意識し、以下の品質特性を担保します。
-
保守性:
将来的なWebアプリ化(APIサーバー分離)を見据え、ビジネスロジックとUIを疎結合にするMVCアーキテクチャを採用。 -
可用性・耐障害性:
AI APIのレートリミット超過やタイムアウト時、システム全体を停止させず、デフォルトスコアで代替するフォールバック処理の実装。 -
性能効率性:
重い処理(API通信、DB集計)によるUIフリーズを防ぐため、Pythonのasyncioを用いた非同期処理の実装。
技術スタック
本プロジェクトでは、「型安全性」 と 「非同期処理による高スループット」 を重視し、以下の技術を選定しました。
Backend & API
- Language: Python 3.12
- Framework: FastAPI (Asynchronous Web Framework)
- ORM: SQLAlchemy 2.0 (Async Session / Mapped Types)
- Validation: Pydantic v2
- Migration: Alembic
Database
- RDBMS: PostgreSQL
- Driver: asyncpg
Infrastructure & DevOps
- OS: Windows 11 (WSL2 / Ubuntu 22.04 LTS)
- Container: Docker / Docker Compose
- Design: Mermaid.js (Design as Code)
💡 技術選定のポイント (Architecture Decision)
1. FastAPI + SQLAlchemy (Async):
I/O待ちが発生しやすいDBアクセスや外部API(AI審査員)との通信において、ブロッキングを防ぎリソース効率を最大化するために完全非同期構成を採用しました。
2. Strict Type Hints (Pydantic & SQLAlchemy 2.0):
開発時のバグ混入を防ぎ、保守性を高めるため、Pythonの動的型付けに依存せず、静的型チェックとランタイムバリデーションを厳格に適用しています。
3. Design as Code (Mermaid):
設計ドキュメントの陳腐化を防ぐため、ER図やシーケンス図をコード管理し、Gitによる差分追跡を可能にしました。
アーキテクチャとデータベース設計
システム構成
本プロジェクトでは、設計の可視化とバージョン管理を両立するため、「Design as Code」 の手法を採用しました。
従来のGUIツールによる作図ではなく、Markdownベースの Mermaid記法 を用いることで、設計図の変更履歴(Diff)をGit上でコードと同様に追跡可能にしています。
以下は、Mermaidで定義した本システムの論理構成図です。物理的なサーバー配置ではなく、アプリケーション内部の 「責務の分離(Separation of Concerns)」 とデータフローを可視化しています。
FastAPIの実装において、保守性とパフォーマンスを最大化するために以下の設計方針を適用しました。
1. 3層アーキテクチャの徹底
処理を Presentation (Router) / Business Logic (Service) / Data Access (CRUD) の3層に明確に分離しました。これにより、将来的にAIモデルやDBが変更された場合でも、他層への影響を最小限に抑える「疎結合」な設計を実現しています。
2. AI連携における完全非同期I/O
「AI審査員(Gemini API)」との通信にはレイテンシが伴います。Pythonの async/await を活用してI/O待機時間をノンブロッキング化することで、AIの回答待ち中もサーバーリソースを解放し、他のユーザーのリクエストを並列処理できる高スループットな構成としました。
ディレクトリ構成
Otona-Tanka/
├── .vscode/
├── docs/
├── frontend/
│
└── backend/
├── .venv/
├── .env
├── .gitignore
├── Dockerfile
├── requirements.txt
│
└── app/
├── __init__.py
├── main.py
│
├── core/
│ ├── __init__.py
│ ├── config.py
│ └── database.py
│
├── models/
│ ├── __init__.py
│ ├── base.py
│ ├── user.py
│ ├── match.py
│ ├── card.py
│ └── game.py
│
├── schemas/
│ ├── __init__.py
│ └── ...
│
├── crud/
│ ├── __init__.py
│ └── ...
│
├── services/
│ ├── __init__.py
│ ├── ai_judge.py
│ └── karma.py
│
└── routers/
├── __init__.py
└── ...
データベース設計 (ER図)
初期の概念設計(Draw.io)から、Mermaid記法による実装レベルの設計へ落とし込む過程で、「競技の公平性」と「データの整合性」 をシステム的に担保するため、を担保するために以下の2つの構造変更を行いました。その内容とともに、最終的なER図を記載します。
1. 状態管理テーブル (hands) の新設による不正防止
初期案ではマスタデータである words のみが存在していましたが、これでは「ユーザーが今どのカードを所有しているか」という状態(State)を管理できず、APIを直接操作された場合に「持っていない単語を使用する」というチート行為を許す脆弱性がありました。
この課題に対し、matches と words の中間テーブルとして hands(手札) を新設しました。 これにより、投稿処理時に「そのカードは当該ユーザーに配られたものか?」「既に使用済み(is_used)ではないか?」という厳密なバリデーションが可能となり、ゲームの公平性をDBスキーマレベルで担保しています。
2. 意図的な非正規化による制約の強制 (votes)
投票データの設計において、理論的な正規化よりも実務的な整合性を優先した意図的な非正規化を行いました。
理論(第3正規形)の視点:
本来、match_id は target_poem_id を経由して特定可能(推移的関数従属)であるため、votes テーブルに match_id を持たせることは冗長です。
実践(アプリケーション要件)の視点:
しかし、本アプリには 「1試合につき、1ユーザーは1回しか投票できない」 というルールが存在します。 これをアプリケーションロジックに依存せず、RDBMSの機能として保証するため、あえて votes テーブルに match_id を持たせ、複合ユニーク制約 (match_id, voter_id) を設定しました。
これにより、万が一アプリケーションコードにバグがあっても、物理的に不正投票(二重投票)が発生しない堅牢な設計としています。
環境構築
WSL2上のUbuntuで、以下の手順で「Hello World」およびDB接続確認までを実装し、FastAPIの環境構築を行いました。
1. プロジェクト構成の作成
バックエンド用のディレクトリを作成し、そこを起点とします。
mkdir -p Otona-Tanka/backend
cd Otona-Tanka/backend
2. 仮想環境の構築と有効化
システム環境を汚さないよう、プロジェクト専用の仮想環境を作成します。
# 仮想環境(.venv)の作成
python3 -m venv .venv
# 有効化
source .venv/bin/activate
3. ライブラリのインストール
FastAPIとASGIサーバー(Uvicorn)、およびPostgreSQL用ドライバ等をインストールします。
pip install fastapi uvicorn sqlalchemy psycopg2-binary python-dotenv
4. アプリケーションの実装
app/main.py を作成します。
今回は学習のため、単純なHello Worldに加え、DBへの疎通確認を行うエンドポイント も実装しました。
※DB設定(database.py)やモデル定義は別ファイルに切り出していますが、ここではメインロジックのみ抜粋します。
# backend/app/main.py
from fastapi import FastAPI, Depends
from sqlalchemy.orm import Session
from sqlalchemy import text
from app.database import get_db # 別途定義したDBセッション取得関数
app = FastAPI()
@app.get("/")
def read_root():
return {"message": "Hello World"}
@app.get("/health/db")
def check_db_connection(db: Session = Depends(get_db)):
"""DB接続確認用エンドポイント"""
try:
# 実際にクエリ(SELECT 1)を投げて疎通を確認
db.execute(text("SELECT 1"))
return {"status": "success", "message": "Database connection established"}
except Exception as e:
return {"status": "error", "message": str(e)}
Hello World が表示されたため、開発のスタートラインに立つことができました。
試行錯誤
開発初期段階において、環境構築とツール選定でいくつかの壁に直面しました。これらは単なるトラブルシューティングではなく、今後の開発効率を左右する重要な意思決定のプロセスでした。
1. WSL2におけるファイルシステムとI/Oパフォーマンス
Windows上のVS Codeで開発を進める中で、pip install の極端な速度低下や、ホットリロード(ファイルの変更検知)が機能しない問題が発生しました。
【原因】
WSL2からWindowsファイルシステム(/mnt/c/...)へのアクセスは、OS間のプロトコル変換によりI/Oパフォーマンスが著しく低下します。また、ファイルシステムの違いによりイベント検知が正常に動作しないケースがあります。
【解決策】
プロジェクトディレクトリをWindows管理下から、WSL2のLinuxネイティブ領域(/home/user/...)へ完全に移行しました。これにより、ディスクI/Oの高速化と安定したファイル監視を実現しました。
2. VS Code「Remote - WSL」の導入意義
当初は「WSL2で開発する」ことの定義を曖昧に理解しており、Windows側のVS CodeからWSL上のファイルを直接編集しようとしていました。その結果、Python仮想環境(venv)が認識されず、LintやIntelliSenseが機能しないトラブルに見舞われました。
【解決策】
VS Codeの拡張機能「Remote - WSL」を使用し、「エディタのサーバープロセス自体をWSL環境下で動作させる」 構成に変更しました。これにより、Linux側のPythonインタプリタやライブラリをVS Codeがネイティブに認識できるようになりました。
3. エコシステムを重視したバージョン選定(Python 3.12)
開発着手時点で最新の Python 3.13 の導入を検討しましたが、Kivy や一部の依存ライブラリがバイナリ対応しておらず、ビルドエラーが多発するリスクがありました。
【判断】
「最新バージョンの使用」自体を目的にせず、「開発の確実性」を優先しました。主要なライブラリが追従しており、情報量も豊富な Python 3.12 を採用することで、環境起因のトラブルを最小化する設計としました。
4. バックエンド始動における「環境の罠」
FastAPIの起動からPostgreSQLへの接続確認を行う過程で、初学者が陥りやすいミスを経験しました。自戒として記録します。
① ディレクトリ構造の認識不足(ModuleNotFoundError)
uvicorn app.main:app --reload を実行した際、ModuleNotFoundError が発生しました。
原因は、CLI(ターミナル)におけるカレントディレクトリの意識欠如です。GUI上では構造が見えていても、コマンド実行位置がプロジェクトルート(~/projects)のままであり、対象モジュール(backend/app)へのパスが通っていませんでした。
【解決策】
物理的に正しいディレクトリ(backend)へ移動することで解決しました。
② 仮想環境(venv)のネストと分離
【課題】
プロジェクトルートとサブディレクトリ(backend)で仮想環境(venv)が混在し、ライブラリの参照エラーやバージョン競合が発生しました。
【解決策】
「1コンポーネント・1環境」の原則を徹底しました。
親階層のvenvを破棄し、backend/ 配下にのみ環境を構築することで、バックエンド固有の依存関係(FastAPI, SQLAlchemy等)を完全に分離・管理下におきました。
③ 「Hello World」の再定義
当初はWebサーバーからのレスポンス {"message": "Hello World"} だけで満足していましたが、バックエンドとして機能させるには不十分であると気づきました。
今回は、「データベースへの接続確立」までを含めて「疎通確認完了」と定義 し直しました。
具体的には、ヘルスチェック用エンドポイント /health/db を実装し、SQLAlchemy経由で SELECT 1 を発行してDB接続を保証する仕組みを構築しました。
さいごに
ここまで読んでいただき、ありがとうございました。
今回策定した「整合性重視のER図」と「Design as Codeの思想」といった設計は、実装されて初めて価値を持つので、次回では実際のコードへと昇華させます。
具体的には、FastAPI + SQLAlchemy (Async) を用いたバックエンド実装において、いかにしてトランザクション整合性を保ちながら、Pydanticによる厳格な型安全性でバグを未然に防ぐか。その技術的アプローチとコーディングの勘所を解説していきます。