CLIアプリケーションの設計

最近naoty/todoをGoからRubyで書き直したのだけど、これまで何度もCLIアプリケーションを異なる言語で書き直してきて、設計方針が自分の中で固まってきたので、忘れないうちに言語化しておきたいと思う。

CLIアプリケーションを作るとき、Command、Model、Repositoryの3つの責務を分けて実装している。アプリケーションによってはここに異なる責務を追加している。

diagram
責務と依存関係

Command

Commandはシェルとのやり取りを行う。コマンドライン引数、標準入出力、標準エラー出力、終了ステータス、環境変数などを扱う。Rubyで言うと、ARGVSTDIN、さらにKernel#.exitといったシステムコールはCommandで扱う。

コマンドライン引数をパースして、実行すべき処理を判定し、RepositoryやModelを呼び出す。RepositoryやModelで標準入出力を扱いたい場合、Commandから入出力のインターフェイスを持つオブジェクトを渡す。RubyだとIOオブジェクト、Goだとio.Readerio.Writerが該当する。

Model

Modelはアプリケーションが扱うドメインを表現する。CLIアプリケーションであっても、Webアプリケーションであっても、Modelのコードは扱うドメインが変わらない限り不変のはず。

他の責務を持つオブジェクトには依存せず、プレーンな実装になることが多い。ただ、GoだとJSONのためのアノテーションがModelに含まれることがあり、含めるべきか個人的には迷いがある。

Repository

RepositoryはModelが表すオブジェクトをストレージに永続化したり、ストレージからModelを取得したりする。ストレージはファイルシステムかもしれないし、Webサービスかもしれない。CLIアプリケーションを作るときはファイルシステムをストレージに使うことが多い。Modelを永続化可能な形式にエンコードしたり、逆に取得する際にはデコードしたりする実装も含まれる。経験上、Repositoryの実装が一番泥臭くて複雑になりやすい。

RepositoryのおかげでCommandはどうやってModelを永続化し、どこから取得するか詳細を知る必要がなくなり、テストが非常にやりやすくなる。