最近naoty/todoをGoからRubyで書き直したのだけど、これまで何度もCLIアプリケーションを異なる言語で書き直してきて、設計方針が自分の中で固まってきたので、忘れないうちに言語化しておきたいと思う。
CLIアプリケーションを作るとき、Command、Model、Repositoryの3つの責務を分けて実装している。アプリケーションによってはここに異なる責務を追加している。
Command
Commandはシェルとのやり取りを行う。コマンドライン引数、標準入出力、標準エラー出力、終了ステータス、環境変数などを扱う。Rubyで言うと、ARGV
やSTDIN
、さらにKernel#.exit
といったシステムコールはCommandで扱う。
コマンドライン引数をパースして、実行すべき処理を判定し、RepositoryやModelを呼び出す。RepositoryやModelで標準入出力を扱いたい場合、Commandから入出力のインターフェイスを持つオブジェクトを渡す。RubyだとIO
オブジェクト、Goだとio.Reader
やio.Writer
が該当する。
Model
Modelはアプリケーションが扱うドメインを表現する。CLIアプリケーションであっても、Webアプリケーションであっても、Modelのコードは扱うドメインが変わらない限り不変のはず。
他の責務を持つオブジェクトには依存せず、プレーンな実装になることが多い。ただ、GoだとJSONのためのアノテーションがModelに含まれることがあり、含めるべきか個人的には迷いがある。
Repository
RepositoryはModelが表すオブジェクトをストレージに永続化したり、ストレージからModelを取得したりする。ストレージはファイルシステムかもしれないし、Webサービスかもしれない。CLIアプリケーションを作るときはファイルシステムをストレージに使うことが多い。Modelを永続化可能な形式にエンコードしたり、逆に取得する際にはデコードしたりする実装も含まれる。経験上、Repositoryの実装が一番泥臭くて複雑になりやすい。
RepositoryのおかげでCommandはどうやってModelを永続化し、どこから取得するか詳細を知る必要がなくなり、テストが非常にやりやすくなる。