知らんけど。

主にプログラミングについて書きます

ナンプレSPAを vue + TypeScript で作った話 〜フロントエンドにクリーンアーキテクチャを適用する〜

※本記事は2020/11/30のゆるWeb札幌にてナンプレについて発表させていただいた際の資料をブログ記事に落としたものです。

ナンプレSPA(number place infinity)とは

ブラウザで動作するナンプレのアプリです。 静的なSPAとなっており、ページ取得時以外でサーバーとの通信は行いません。

f:id:hori_chan:20201202191917p:plain
プレイ画面

※注意

  • 選んだ盤のサイズが大きい場合、高い負荷がかかり問題の生成に時間がかかる場合があります。
  • 30秒ほど待っても問題が生成されない場合はブラウザのタブを閉じるか、戻るボタンやリロードボタンを押下し、初期画面に戻りやり直してください。
  • 負荷が掛かるせいでブラウザの他のタブに影響がないと言い切れないため、作業中のタブがある場合は作業を完了するなどしてから問題の生成を行ってください。


始め方

f:id:hori_chan:20201202192106p:plain

  1. まるい数値のボタンで盤の大きさを決める
  2. STARTボタンを押す

9x9や10x10など大きめの問題は、生成に時間がかかります。 生成中はフリーズしているように見えます。20秒くらいは待ってください。 待ちきれない場合はブラウザの当該タブを閉じて最初からお試しください。


ルール

1~nの数値を縦・横・太枠の四角いエリアの中で重複しないように埋めていき、全てのマスを埋められたら成功です。

f:id:hori_chan:20201202191917p:plain
プレイ画面


遊び方

カーソルの動かし方

f:id:hori_chan:20201202192653p:plain
プレイ画面全体

  • マスをタップまたはクリック
  • タッチムーブ(画面上のどこでもタッチしたまま指を動かすことでカーソルを動かせます)
  • キーボードの矢印キー

数値の入力

  • 画面右下の半月状のコントローラの数値をタップ
  • キーボードの数値キー

数値の削除

  • 画面右下の半月状のコントローラの×をタップ
  • キーボードのBackSpaceキー

使った技術

  • Vue.js
    • 2.x
    • vue-class-component
    • Vuetify
    • Vue Router
  • TypeScript
    • 3.7~
  • DIコンテナ

f:id:hori_chan:20201202192302p:plain
Vue.js


デプロイ環境

f:id:hori_chan:20201202192345p:plain
Amazon S3

完全に静的で通信を行うことのないブラウザアプリになってます。


作った経緯

  • 2019/夏

ナンプレを解くライブラリを作ってみよう!

  • 2019/10
    • ゆるゆると作り始める
  • 2019/12
    • 問題を解くロジックが完成 →白紙から解いたら問題を生成できるのでは?
  • 2020/1
    • 問題を生成するロジックが完成 →UIも作ってみるか〜
  • 2020/2
    • UI作って公開

アーキテクチャ

  • クリーンアーキテクチャを意識している
  • CoreとApplicationは純粋なTypeScriptのクラスで構成されている(アノテーションのみTSyringeに依存している)
  • データを管理する部分はDIで実装
  • Viewは表示と入力受付に徹する

f:id:hori_chan:20201202192438p:plain
アーキテクチャ


Core/Application/View 分離のメリット

詳しくは書籍「クリーンアーキテクチャ」参照のこと!

私の感じたメリット

  • 重要かつ複雑なCoreとApplicaitonを扱いやすく作れた
    • Viewの都合による変更が入り込まない
    • UIを無視し単体で試験
    • 永続化処理部分をMock化して開発
  • 決定の遅延
    • 最終的なアプリケーションの形の決定を遅らせることができる。
      • Webアプリ?CLIAPI? 最後に決めた。
      • RDBに保存?API越しに保存? 最初に決めたメモリでの保持のままにした。

f:id:hori_chan:20201202192527p:plain
書籍 Clean Architecture


課題

  • ゲームはできるが楽しさややり込み要素が足りない
  • もうちょい素敵なコントローラーにしたい
  • CoreとApplicationとViewのプロジェクトを別々にしたい
  • 問題生成処理性能改善
  • レガシーコードからの脱却
    • テストを書いて無理やり動かしたクソコードと複雑な設計
    • 理解不足のDDDプラクティス

学んだこと・経験したこと

  • 複雑なロジックにテストを書いて立ち向かう
    • Core部分が複雑でテストがないと完成させられなかった
  • フロントエンドもクリーンアーキテクチャは有用
    • 決定の遅延 / 開発速度UP / テスト容易性UP
  • フロントエンドに DDDのプラクティス は有用か?
    • 本来はFWを有効利用して「表示部⇄API」を実現したい。
    • 大抵の場合はそうならない。表示部ドメインの知識をView部分意外に持たせた方が後々楽になる。
    • 今回のようにフロントエンドで完結するシステムならば有用。

11/30の発表で言わなかったこと

追加で言いたいことを言いまくります。

背景画像はアイヌ文様

背景画像はアイヌ文様のフリー素材のモレウというサイトから拝借しています。 モレウ様ありがとうございます!

ちょっとしたことで使いにくさが解消された

選択中のセルをハイライトする際、初めはセルの背景色を薄いピンク色で点滅させていました。 スタイルでこんなことできるのかーという気持ちで採用したピンク点滅ですですが、これを辞めたことでグッと使いやすくなったと思いました。 ピンク点滅だった頃は、遊んでいると疲れてしまい、楽しむ気になれないほどでした。 これをやめてから、自分自身もしっかり楽しめるようになりました。

おすすめナンプレアプリ

ナンプレアプリを作っておいてなんですが、以下のiPhoneアプリが好きです。おすすめです。

コンセプティス ナンプレ

コンセプティス ナンプレ

  • Conceptis Ltd.
  • ゲーム
  • 無料
apps.apple.com

ナンプレアプリを自分で作るまでは中級までしか解けませんでしたが、アプリを作ってからは超上級の「極」まで解けるようになりました。もちろん自分で。

クリーンアーキテクチャやDDDの「プラクティス」って何?

あえて「プラクティス」と言いました。 クリーンアーキテクチャやDDD「風」に作っており、厳密には違うかもです。 ですが、巷で「軽量DDD」と呼ばれている設計のテクニックが存在しており、ここではそれらを「プラクティス」と表現しました。

クリーンアーキテクチャやDDDやそれらのプラクティスを正しく理解しているかと言われると、僕は全く自信はないです。僕はDDDを経験したことがありません。ま、一人で開発してるってことは、自分がドメインエキスパートなんですが。 いつかは本当にDDDをしてみたいですね。

自分が「良い」と思えば良いんです。このアプリはある程度「クリーン」です。 とは言え、現状に満足しているわけではありません。自己研鑽あるのみです。

DIコンテナのTSyringeについて

実は使用したのはこのアプリでのみです。 実務で使ったことはなく、今後使うこともないように思います。

というのも、このTSyringeはアノテーションでDIする際に、コンストラクタがpublicでなくてはならず、僕の設計方針に合わないからです。 あと、単純に、あまり使い勝手が良くないような。

そして、過去に携わった業務においてTSyringeの使用を拒否された(当時現場でフロントエンド全般の学習コストが問題視されていた)際、自分でコンテナを作りDI(アノテーションは使いませんでしたが)するようにしたところ、とてもシンプルで良いものができたので、それからコンテナを自作するようになりました。 自作コンテナと言ってもただのグローバル変数のようなものなんですけどね。

僕の設計方針:static creation

クラスのほとんど全てを、private constructorとし、そのクラスに定義するpublic static create() などの生成メソッドを使用してインスタンスを取得するようにしています。 同じクラスでも生成するシチュエーションによって生成方法や生成したインスタンスの使用目的が異なる場合が多々あります。 これに対応する名前を付けた静的ファクトリーメソッドを使用することで、可読性が高く、少しだけ柔軟な設計が可能となります。

このちょっとした柔軟性の高まりで後々救われることがあります。 やらない理由はないです。必ずそうします。

このテクニックは「static creation」と呼ばれているそうです。 2019年秋に仙台で行われたTDDBCに参加しt_wadaさんにレビューいただいた際にそう呼ばれておりました。間違いないです。

このテクニックは、例えばこんな感じで使っています。

class User<
  TFor extends 'for-create' | 'for-reconstruct',
  TId extends string | undefined = TFor extends 'for-create'
    ? undefined
    : string
> {
  public static create(name: string): User<'for-create'> {
    return new User(undefined, name);
  }
  public static reconstruct(id: string, name: string): User<'for-reconstruct'> {
    return new User(id, name);
  }
  private constructor(public readonly id: TId, public readonly name: string) {}
}

const userForCreate = User.create('John');
const userForUpdate = User.reconstruct('123', 'Mike');

新規登録時に作るUserはidを持っていませんが、DBやAPIで取得したUserはidを必ず持っています。 同じクラスのインスタンスですが、異なる性質を表現できます。 いちいちID持ってるかどうかの判定をしたくありませんよね。 無い場合は絶対に無い、有る場合は絶対に有るんですから。 (idをクラス化するケースもありますけどまぁそれはおいておいて)