以前、Vue.js を使ってマインスイーパーを実装しましたという記事を書きました。そちらでは、ビジネスロジックは Vue に依存しないように作っていたので、ふと思い立って今回はそれをサーバーサイドに移植してみました。
本記事では、実装の細かいところは書ききれないので設計のポイントになる部分を説明します。
作ったもの
API サーバ
API サーバに対応したクライアントプログラム
環境
node 14.16.0
express 4.16.4
MySQL 8.0
1. 基本方針
Vue.js で作成したときの Model はそのまま再利用します。前回の記事で書きましたが、ビジネスロジックは Vue.js に依存しない POJO なクラス群で作成しているため、ほとんどそのまま移植しています。
全体の構成は、大雑把に次のような構成。
説明 | |
---|---|
Controller | HTTPリクエストのハンドリング。HTTPに関する知識はここだけが持つ。 |
Repository | DBとのやりとり。テーブルレイアウトに関する知識はここだけが持つ。 |
Model | Vue で作ったマインスイーパーから移植した部分。ビジネスロジック。 Controller や Repository には依存しない。 |
2. データ設計
基本となるデータ構造は Cell, Game の2つ。
データベース上も2つのテーブルを作成。
Cell
一つのマスを表すデータ。
セルの位置座標 (x, y) と、画面表示に必要な情報を保持。
{
"x": 0,
"y": 0,
"count": 2, // 周囲のセルの地雷数
"isMine": false, // 地雷があるか?
"isOpen": false, // 開かれているか?
"isFlag": false // フラグが立てられているか?
}
Game
ゲーム全体を表すデータ。
盤面の広さや地雷数などの設定値、ゲームのステータスや開始・終了時刻などを持ちます。
{
"id": 999, // ID
"width": 9, // 盤面の横幅
"height": 9, // 盤面の高さ
"numMines": 10, // 地雷数
"status": "PLAY", // ステータス
"startTime": "2021-06-27T08:22:34.470Z", // 開始日時
"endTime": null, // 終了日時
"cells": [ /* 盤面上すべてのセルが入った配列 */ ]
}
3. URL設計
ボツ案
まずは、はじめに考えたもの。
データ構造をそのままURLにした。
HTTP メソッド | パス | 説明 |
---|---|---|
POST | /api/games | Game の作成 |
GET | /api/games/{id} | Game の取得 |
GET | /api/games/{gameId}/cells | Cell 一覧の取得 |
PATCH | /api/games/{gameId}/cells/{id} | Cell の更新 |
イマイチなところ
- なんか REST っぽくない
- セルを開く、フラグを立てる、フラグを外す、という操作がすべて Cell に対する PATCH で行われている
- ゲーム上あり得ないリクエストをどう扱うのか悩むところが増える
このままでも機能的には実現できるんですが、うまいやり方はないかと試行錯誤して次の形に落ち着きました。
最終的なURL設計
ポイントは「セルを開く」という操作を「『開いたセル』を作る」として扱うように変更したことです。これによって、HTTPメソッドとURLの組み合わせで、何に対するどういう操作なのか、を表現できるようになりました。
HTTP メソッド | パス | 説明 |
---|---|---|
POST | /api/games | Game の作成 |
GET | /api/games/{id} | Game の取得 |
POST | /api/games/{gameId}/open-cells | Cell を開く |
POST | /api/games/{gameId}/flags | フラグを立てる |
DELETE | /api/games/{gameId}/flags/{id} | フラグを外す |
4. チート対策
ゲームのAPIサーバーなので一応チート対策として、ゲームが終了するまではどのセルに地雷がセットされているのか、という情報をクライアントに渡さないようにしています。
Express のドキュメントによると res.json()
メソッドは内部で JSON.stringify()
を使っています1。また、オブジェクトに toJSON
メソッドを実装すると JSON.stringify()
の挙動を変更することが可能です2。このことを利用して、ステータスがプレイ中の場合は isMine
フラグを表示しない、などの制御を入れています。
const obj = {
value: 0,
toJSON () {
return {
value: this.value,
text: this.value % 2 === 0 ? 'even' : 'odd'
}
}
}
obj.value = 1
JSON.stringify(obj) // "{\"value\":2,\"text\":\"odd\"}"
obj.value = 2
JSON.stringify(obj) // "{\"value\":2,\"text\":\"even\"}"
さいごに
きっかけは「AWSの勉強をするために簡単な Web アプリケーションを作ってみよう」ということだったんですが、
やってみたら色々学ぶことが多く非常に楽しかったです。
ただやっぱり型が無いのは辛いですね…次は TypeScript に手を出してみようかな…