Open9

個人や業務で Web アプリケーションを作った際に得た知見

ピン留めされたアイテム
にゃるら / カラクリスタにゃるら / カラクリスタ

ここは何?

この Scrap では過去の業務経験や自身の個人開発を行なっていた際に得た知見について、雑多かつできるだけ迅速に出すことを目的として書いています(そのため文体や言い回しが雑になっています)。

最近作ったものとしては、Perl で開発している、

https://github.com/nyarla/p5-Kalaclista/

https://github.com/nyarla/kalaclista

が上げられますが、ここで書いている内容自体は他の言語でもある程度応用が効くと考えています。

にゃるら / カラクリスタにゃるら / カラクリスタ

コンテキストオブジェクトは適宜分割する

Sinatra(Ruby)とかMojolicious(Perl)、Hono(TypeScript/JavaScript)とかでも同じですが、これらのフレームワークにはいわゆるコンテキストオブジェクトが各ハンドラの引数として生えています。

それでこのコンテキストオブジェクトの生やし方なんですが、自分で静的サイトジェネレーターを作った際に、

リクエストコンテキストグローバルコンテキストのオブジェクト は分けた方が良い

と言う事を強く感じました。

と言うのもリクエスト単位のオブジェクトは HTTP Request 単位で生かす必要のあるオブジェクトですが、グローバルなオブジェクトについてはアプリケーションレベルで共有するオブジェクトなので、特にリクエストコンテキストに生やす必要が無い、という点が今のところの結論です。

んで、それではどうやってグローバル単位のオブジェクトを生やすかについては、

  1. 基本モジュールの読み込み時に初期化をする
  2. その初期化済みのオブジェクトへのアクセサをエクスポートする
  3. エクスポートされたオブジェクトをアプリケーション内で利用する

と言う流れになるかと思います。

またコンテキストオブジェクトのクラス自体も一つにまとめるのでは無く、そのオブジェクトの利用単位で分けた方が良いです。

これは例えば:

  • DB 関連のオブジェクトは WebApp::Context::DB
  • 環境変数などのオブジェクトは WebApp::Context::Environment
  • Web アプリケーションの URI 関連は WebApp::Context::URI

と言ったような感じです。

これによって例えば DBのコンテキストオブジェクトは必要だが URI 関係は必要ない といった様な場合に不必要なモジュールの読み込みを避けることが出来ます。

ただしモジュール単位でグローバルコンテキストもオブジェクトを生やす場合において、スレッドセーフが要求されるか否か、については注意を払う必要があると思っています。

にゃるら / カラクリスタにゃるら / カラクリスタ

テストの分け方はモジュール毎にディレクトリを生やす

これについてはアプリケーションの規模感へ完全に依存するのですが、テストファイルの分け方については最初から基準を決めて分けた方が良いです。

今のところ自分が作ったテストファイルの分け方(Perl の場合)は、

t/
  Module-Name-Bar/
    00_compile.t
    10_method.t
  Module-Name-Foo/
    00_compile.t
    10_method.t

と言う分け方と、

t/
  lib/
    Module/
      Name/
        Bar.t
        Foo.t

と言う分け方を両方試してみましたが、後者の モジュール毎に対応するテストファイルを一つ用意する と言う分け方をすると、モジュールの機能が大きくなって行くにつれて、テストの管理が破綻して行きます。

これはいわゆる ゴッドオブジェクト と呼ばれた事案に近い話ですが、一つのモジュールに対して一つのテストファイル と言うスタイルは、そのモジュールが育つに連れて ゴッドテストファイル になって行くため、新規にテストを作る場合には止めた方が良いです。

そのためモジュールのコードが順当に育つ前提であれば、t/Module-Name-Foo/*.t と言ったテストファイル構成が良いと思うのですが、これにも一部問題があって、モジュール数が大きくなりファイルが増えてくると今度はディレクトリを探すのに苦労する破目になります。

特に業務で製作するアプリケーションでは敢えて冗長なモジュール名を付ける場合もあるかと思うのですが、その冗長な名前を付けるとテストファイルを格納するディレクトリ名も冗長になるので、その辺りも面倒です。

よってテストのディレクトリ構成についてはモジュールの名前空間の切り分けも合わせて一番最初にガイドラインを設けて、その中でテストファイルが肥大化しないように設計する必要があります。

にゃるら / カラクリスタにゃるら / カラクリスタ

テスト対象と ならない モジュールの動作は既に保証されていると考える

これは主にテストファイルのスコープを限定するための話ですが、例えば、

  • Module::AModule::Bに依存する
  • しかしテストの実行順は Module::A が先で Module::B が後になる

と言った場合、Module::A が使う Module::B の機能は既に正しく動作する、と言う前提でテストを書いた方が良いです。

何故かと言うと Module::AModule::B の動作を確認するテストを書いてしまうと、Module::A のテストファイルに Module::B のテストコードが混ざってしまい、テストのスコープがブレてしまうためです。

よってテストの順序が多少前後するとしても、Module::A のテストでは Module::B が正しく動作する、と言う前提でテストを書く方が良いと考えています。

にゃるら / カラクリスタにゃるら / カラクリスタ

データ表現のクラスは基本的に Immutable (不変)にする

Perl に限らず DB のデータなどは ORM 等でオブジェクトへ Map すると思うのですが、この Map する際のデータオブジェクトについては原則として Immutable にして、必要があれば clone して次の処理に回す、と言う手法を取った方が良いです。

この辺りの考え方自体は関数型言語では良く見られるものですが、データクラスが状態を持ってしまうと新規処理の追加などで どのプロパティがどこでどう変化したか を追うのがツラくなるので、データクラスは基本的に不変にしましょう。

ただし、とは言え関数型ではないプログラミング言語でWeb アプリケーションを作る場合、すべてのクラスで状態を持たないと言う実装は結構ハードルが上がる面があるので、

  • 状態を持つクラスは名前空間自体を分ける
  • 状態を持つクラスが所持するデータオブジェクトは不変な物だけにする
  • 状態を持つクラスが状態を持つオブジェクトを持たない様にする

と言った実装を行うことが望ましいと思います。

にゃるら / カラクリスタにゃるら / カラクリスタ

データ表現のクラスは常にモック可能にする

これはデータオブジェクトを不変にした上で、の話ですが、データ表現を行なうクラスについては常にダミーデータによるモックが出来るようにした方が良いです。

と言うのもダミーデータによるモックが容易にできない場合、データクラスを用いるロジックのテスト を書く際にダミーデータを突っ込んだオブジェクトを 毎回テスト内で 組み立てる破目になるので、データ表現を行うクラスについては モックを容易に組み上げられる ようにしましょう。

その方がテストの見通しも良くなります。

にゃるら / カラクリスタにゃるら / カラクリスタ

引数が複雑になる場合には引数自体をオブジェクトにする

何らかのビジネスロジックの引数へ型を付けられる言語である場合でも、ビジネスロジックへの引数が増えてくると関数内のでバリデーションを手書きする率が上がってくる ので、出来れば引数自体をオブジェクトにまとめた方が良いです。

また引数全体をオブジェクトにまとめる場合、引数のバリデーション自体も引数オブジェクトのメソッドとして実装すると再利用性が上がる(と思われる)ため、そう言った意味でも引数が増えてきそうならオブジェクトにまとめましょう。

なおこれは感覚的な話ではあるんですが、引数をオブジェクトにまとめる際の目安として5つ以上の引数を取る場合には引数をオブジェクトにした方が良い と思います。

にゃるら / カラクリスタにゃるら / カラクリスタ

大規模開発が行なわれる前提ならモジュールのドキュメントはしっかり書く

業務で書く Web アプリケーションについては、その規模感や歴史によってドキュメントが コード自体に 書かれていない場合もあるかと思いますが、個人 or 小規模開発ならともかく、大人数で大規模開発される場合には ドキュメントはコード本体へ付随する形で書きましょう

例えば Perl だと POD 、Ruby だと RDoc など、言語によってコード本体へドキュメントを書く手法は違うと思いますが、少なくとコードベースが安定して大きくなる様だったら 最低限のドキュメントは書くべき です。

無論ドキュメントをしっかり書くとなるとコードを書く速度自体は下がりますが、ドメイン知識のないコードベースに処理を追記する 場合ビジネスロジックの把握自体に手間が取られるので、結果として生産能力が落ちます。

また一時的にビジネスロジックを把握していたとしても、しらばらくそのコードから手が離れると 今まで自分が書いていたコードの中身を忘れてしまいます し、また他者の手が加わっていたら尚更にコードを読み込むための時間を取られるので、ドキュメントは本当に書いた方が良いです。

なおドキュメントを書くと言う話については、スピード感が必要なスタートアップや試行錯誤を必要とするプロジェクト初期ならともかく、ある程度の形が定まりつつあるのであれば その段階で書き始めないと、後からドキュメントを書くでは追いつけません

にゃるら / カラクリスタにゃるら / カラクリスタ

テンプレートファイルで HTML や XML 、JSON などは手書きしない

これについては 完全に個人の意見 ですが、外部へ出力する HTML や XML、JSON などについてはテンプレートエンジンで手書きする事は極力避けた方が良いです。

特に HTML がそうですが、HTML の場合、多少のマークアップの崩れはブラウザ側で吸収されてしまうため、HTML 上のマークアップのミスに気が付きにくくなります。また昨今では HTML を minify する場面も多いため、それも含めてマークアップのミスに気がつけなくなります。

さらに XML については少してもマークアップに崩れがあるとそもそもパーザが読み込んでくれませんし、JSON なんて手書きすることそのものが厳しいので、間違ってもテンプレートエンジンを駆使して書くものではありません。

なお JSX や TSX などの、HTML 構造を正しく書かないとコンパイルが通らない系の言語については HTML/XML 構文を手書きしても問題ありません。

しかしそれ以外の言語において HTML/XML を手書きするタイプのテンプレートエンジンはミスが起き易いですし、先にも述べた通りマークアップのミスに気がつけず、RSS や Atom などの XML ファイルが盛大に破壊されていた、なんて事故も起きる(私は起こしました)ため、手書きは出来るだけ避けましょう。

ちなみに私はこの問題に対し、Perl においては、

https://metacpan.org/pod/Text::HyperScript

と言うモジュールを作って解決しました。なおこのモジュールの GitHub リポジトリは、

https://github.com/nyarla/p5-Text-HyperScript

にあります。