この記事について
JavaScriptの学習のために暇な時間にNode.js上で動くCLIツールを作ったりしています。
以前にもパッケージを作ってみたりしていましたが、コードの管理がうまくいかずどんどんコードを書くのが辛くなってしまいました。
これまではClassを使ってオブジェクト指向っぽく書こうとしていましたが、自分自身のオブジェクト指向への理解の浅さやパッケージの規模が大きくないことも影響して
- 一つのClassに機能が集中しすぎて分割単位が分かりずらくなってしまう
- そこまでインスタンスを作らないのでClass自体の必要性があまり感じられなかった
- Classに対してのテストの難しさ
という問題を感じていました。
そんななかで改めてCLIツールの設計手法について自分なりに考えて再度整理をしてみました。
n=1
でかなり主観的な内容が多く、まだまだ改善点も多いですがこれからCLIツールを作ってみようという人や参考程度に見る方の一つのサンプルとして記載させていただこうと思います。
目指したこと
以前のパッケージ開発の反省を踏まえ
- 粒度を細かくするために非常に小さい関数を組み合わせる
- テストが書きやすいコードであることを前提として分割を行う
このように目標を立てました。
作っているパッケージについて
紹介が遅れましたがpicture-lint
という画像データ向けの検査ツールを作っています。
主な機能としては画像容量の上限監視や拡張子とバイナリデータの整合性チェックなどを行う予定で作成を進めています。
実際にコードを書き始めて分かったこと
きちんと目標を決めてコードを書き始めましたがそれでも書いている途中で間違いに気づくことが多くありました。
分類をせずに関数を並べた失敗
とりあえず関数をたくさん作るだけではそれ自体がどのような機能を持っているのかが分かりづらく、ディレクトリが散らかってしまうだけでした。そのため機能ごとに関数をまとめて管理する方向性に修正していきました。
関心ごとにオブジェクトを作って管理する
小さなパッケージですがそれでも機能は複数存在するため、まずは機能毎の分割から考え始めました。
ざっくりと考えても下記のような機能は必要だったため、まずはそれらを目的別に分割しディレクトリを分けました。
- 設定ファイルの読み込み・マージする機能
- 読み込んだデータをチェックする機能
- チェック結果を出力する機能
- コマンドを受け取って各種機能を組み合わせて実行する機能
その後それらの機能のなかで更に操作もしくは定数ごとにディレクトリを分けました。
これにより${機能名}.${どのような処理}()
という形で各種機能を呼び出すことができ機能の呼び出しを簡潔にしつつどのような処理が行われるのかが名前と型情報からある程度推測できるようになり、わざわざ別のファイルを開いて確認するという手間がかなり減りました。
.
┗ src
┣ config // configに関連した操作
┃ ┣ loader // 読み込み
┃ ┃ ┣ index.ts
┃ ┃ ┗ index.test.ts
┃ ┗ merge // マージ処理
┃ ┣ index.ts
┃ ┗ index.test.ts
┣ checker // 各種ルールの確認を行う操作
┃ ┣ strictFormatCheack // バイナリデータと拡張子の整合性の確認
┃ ┃ ┣ index.ts
┃ ┃ ┗ index.test.ts
┃ ┗ fileSizeLimit // ファイルサイズ制限を超過していないか確認
┃ ┣ index.ts
┃ ┗ index.test.ts
┗ index.ts
このように、src
下の階層には機能毎にディレクトリを作成してその下層には具体的な処理についてディレクトリを作成して管理しています。
テストをより書きやすく分かりやすくするための工夫
以前の作成時よりかは幾分かテストは書きやすくなりましたが、それでもまだすっきりとしていなかったので以下のような改善も行いました。
テストファイルをテスト対象の近くに置く
単体テストのテストファイルは前記のディレクトリ構成のようにテスト対象と同じディレクトリに格納するようにしました。
これについては単純にimport
を楽に書きたいというのが一番の動機でしたが結果的にどのファイルに対してのテストなのかが分かりやすかったり、VSCode上でたくさんディレクトリを開いてエクスプローラーがごちゃごちゃしてしまうことが減ったので個人的に気に入っています。
関数型の考えを取り入れて宣言的に書いていく
関数単位で分割を行うと決めたのですが、その関数の中で状態や副作用を持ってしまったりするとあまり意味がないと感じていました。
入力が一定であれば出力も常に一定であったほうがテストは容易になるため、一度手を止め関数型言語の考え方について(本当に表層だけですが)調べてみました。
関数型言語は自分のように数学的下地がない人間にとっては難しいですが、参照透過性であったりそもそも副作用とは何なのかなどを知ることにより、関数の分割基準や入出力値を決める際の判断材料として非常に参考になる見識が得られました。
このパッケージではchecker
と呼んでいる、入力された値が検査項目に合格するかを確認する関数群があります。最初はこの関数内でfs
を使ってファイルにアクセスしたりしていました。
この機能が壊れてしまうと検査結果の信頼性が下がるため、この機能に関しては純粋な関数にしてよりテストも行いやすいよう、責任範囲を考え直したりすることで結果的により適切な分割単位に近づいていったと感じています。
まとめ
おそらくきちんと技術書を読んだり、先人の知恵を借りればそもそも悩むようなことはなかったと思います。
完全に車輪の再発明記事ですが、それでも自分なりにじっくりと設計と向き合い、思いついたことを試して失敗してという経験は自分としては非常に大切な経験になったと感じています。
技術の流行を抑えるだけでなく、その技術・手法が何を解決するためのものなのかを理解できるようになるためにも最短経路以外の道を歩いてみることの楽しさや重要性を改めて感じることができました。