一語の掲記 開発の振り返り

一語の掲記というウェブサイトを作成した。役に立つわけではなく、何かを訴えるものでもないけれども、気に入ってもらえたら単純に嬉しい。

プログラミングの練習として作ったのだから、開発から学んだことをまとめてみたい。

全般

開発期間は6/5-14、自主的な3連休2回と平日の夜を使って開発を行った。ガッツリ作業したわけではなく、音楽を聴いたりTwitterを開きながら作業をしていた。とても単純なアプリな上に、ベースは昨年の夏に一度完成していたので、最初の3日ですでにそれらしい形にはなった。それでもいろいろ修正したので2週間かかったし、仕事が忙しければもっとゆっくり開発していたので1か月はかかったと思う。1か月に土日は8日しかない。ちゃんと設計すれば効率的に作れるだろうか。

アーキテクチャのメモ

(Routes) --ReqParam--> (Validation) --> (Logic/Module) --> (Database)
(View) <--viewParam--- (Logic/Module) ------------------------/
  1. Routes: どのようなRoutesがあり、パラメータを渡すとどのような効果があるのか
  2. Validation: パラメータは正しいか。省略されている場合、初期値は?
  3. Logic: これから実行しようとしている処理はどのようなものか
  4. Database: 論理矛盾が発生しないよう、安全にSQLを発行できるか。
  5. Logic/Module: データを適切に加工する。Viewにどのようなパラメータを渡すのか
  6. View: Logicからパラメータを受け取って正しく表示すること
  7. Error: 例外が発生したら500。エラーページを表示したり、ProblemJsonエラーを返したり。

言うは易し、行うは難し。WebアプリにはView, Framework, Logic, Databaseの4領域があると勝手に思っている。

View

Template Engine

Next.js(React)を辞めてテンプレートエンジンを使用した。JSXが恋しくも感じられたが、予めテンプレートエンジンに変数を渡しておけば、テンプレートファイルの中でその変数は自由に使うことができる。単純だが素直に動いてくれる。Reactをビルドする謎のツール群も不要で、プロジェクトがクリーンに保たれるのもよかった。

React vs jQuery

jQueryを使ったWebアプリを完成させたのは今回が初めてだ。ただ、UIを動的に動かそうとするとVanillaJS/jQueryでは辛いだろうと感じた部分もあった。Node.jsのオブジェクトをJSONにシリアライズしてHTMLに埋め込み、Javascriptのコードでそれを受け取るのが王道なのだろうか。

Reactを使えば動的なUIは確実に作りやすい。でも初期データを供給するためにはAjax通信とAPIが必要だし、SSRするNext.jsでもサーバーサイドでfetchすることに変わりはない。React/VueはVirtualDOMのレイヤを提供することでjQueryが苦手とする領域をカバーできるが、その管理コストを考えるとReact/Vueを避けた方がよいアプリはたくさんある。それにReact/Vueを使うとフロントエンドとバックエンドを疎結合になり、開発工数は一般的に増える。複雑な要求に対応するために最初から疎結合に作るのもいいが、"MonolithFirst"と言われるようにまずは密結合でもプロトタイプを作るのは悪くない。個人的にはNext.jsはかゆい所に手がとどきそうなフレームワークだと思うんだけど、ReactをSSRに寄せたライブラリなので、純粋なバックエンドアプリケーションを作るフレームワークとしてはイマイチな点もあった(と記憶している)。

CSS Framework

SemanticUI for ReactからSemanticUIに移行した。SemanticUIのSearchBoxはajax通信による検索候補表示機能が組み込まれていてめちゃくちゃ楽だった。ボタンにdiv, a, buttonのいずれを使っても表示が統一される点もGood。逆にGrid/Columnを使ったモバイル端末でのレスポンシブ対応をうまく実現できず、最終的に取り除く手戻りが発生した。使っていて文句はいっぱいあったが、総合的に見ると使ってよかったと思う。

CSSフレームワークのいいところはそれなりに使えるUI要素が簡単に利用できること。CSSフレームワークの悪いところは、設計意図と異なる使い方をしたときに思ったように動いてくれないこと。使い勝手が悪くてクソだといいたくなるが、使い勝手がいいCSSフレームワークなんて存在しないのが辛い。tailwindcssは使いやすいけど、あれは自力でやらないといけないことが多すぎて辛い(CSSは本職じゃないし)。CSSはその仕様から予想できない挙動を示したり、それを逆に活用するハック要素があるので辛い。結論として、CSSはどうあがいても辛い。

Framework

Koa

Koaフレームワークを使用した。Expressと違い、async/awaitを使った非同期処理が簡単にかける。特に不満はなく、総合的に満足している。何よりTypescriptの型支援が快適。Python/Ruby/PHPだとこうはいかないだろうし、ガチガチの型付き言語ではないのでcやGo言語のように型付けに苦労することもない。ビジネスレイヤやUIを直に扱うからだろうか。Webアプリは入出力が曖昧で、静的型付け言語の良さが発揮しにくいのではないだろうか

Koa middlewares

一般的なWebアプリが必要とする機能はたいていOSSとして用意されている。楽そうに思えるんだけど、実際にはドキュメント化されていない仕様がいっぱいあって、簡単に泥沼にハマる。例えば今回はkoa-sessionsession.save()がセッションデータをCookieに保存するものと知らず、セッションが消えない現象に苦しめられた。また、同ミドルウェアを2回初期化できないことを知らずに2回初期化し、2回目の設定が反映されない現象に苦しめられた。koa-routerはpath-matchingに順序があることを知らなかったので/about/a(.*)にヒットし、意図せずに認証チェックが走ったので原因解明に時間を要した。

Modules

良く使う機能を予めモジュールにしてフレームワークを独自に拡張していたのだが、結局半分くらいは邪魔になって消した。どうするのが正解なのだろうか。個人的にはデータベース&認証機能があれば満足。メンテナンス機能もモジュール化していたが、使う予定がないのに改造の工数だけがかかりそうだったのでバッサリ消した。

ポンと持ってきて導入できるライブラリを作るのは大変だし、別プロダクトのコードをコピペするのが現実的だが、そのコードの使い方・設計意図・依存関係を正確に把握していないと問題を起こすリスクある。OSSライブラリについても同じだが、何をしたらこのリスクを低減できるのかまだよくわかっていない。ドキュメントとテストだと思うのだけれども。

Model

ここではフレームワークと演算・外部入出力を繋ぐロジックをモデルと呼んでいる。今回は手を抜いたのでRouterにそのままロジックを記述した。Webアプリは基本ステートレスで、ステートはCookieやセッションに保存される。ロジックをフレームワークから切り離すにはまずロジック独自のステートを用意し、コンテキストからそのステートを構築する作業が必要。疎結合にすると開発工数が増えるのは自然の流れである。まあ、次改善するならここかな?テストも増やしていきたいし。

Database

SQLiteを使用した。書き込むのが自分だけなので問題ないと願いたい。

Next.jsを使ったときはTypeORMというORマッパーを使った(Entityは1つ)が、ORマッパーの関数が何をしているか1年経ったらよくわからなかったので、今回は素直にSQLを書くことにした。

フレームワークとロジックは分離しなかったが、DBへのIOは分離した。ロジックにSQLite依存のSQLを書きたくなかったし、例外ハンドリングを追い出したかったからだ。しかし、やはりというか、オブジェクト指向で書かれたコードとSQLは相性が悪い。SQLの書き方次第で欲しいデータの列の数、データの数が簡単に変化する。これを抽象化してライブラリにするのは大変ではなかろうか。

テーブルに対してアトミックな操作を呼び出せるように関数を定義し、その中でSQLを記述した。テーブルが一つあれば「INSERT・SELECT・UPDATE・DELETE」の4つが必要になるし、「行数の取得」「検索」機能も必要。複数のテーブルを必要とするアトミックな操作(トランザクション)もよく使う。テーブル1つにつき例外処理も含めて10~15行×操作N種類の関数を定義するので、コードがあっという間に増える。このあたりをライブラリに任せたい!と期待してしまうのだが、きっと何を使ってもコレジャナイと感じるような気もしていた。なんとかできないかな?

SQLの直書きでちょっと面倒だったのは検索クエリの動的な構築だった。クエリは先頭から確定させてゆくが、例えばSELECT fieldと書いた後でテーブルをjoinしたら、選択する列の名前を変えないといけない。今回は気合で乗り切ったけれども、あまりスマートとは言えないやり方だった。