概要
この記事はコマンドクエリ分離(以下CQS)に関するMark Seemann氏のブログポストと脳におさまるコードの書き方の内容を筆者が日本語で拾い読みしたメモです。特に断りのない限り、内容はこれらのソースからの引用ないし要約です。以下ではCQSの概要とコード例に触れた後、副作用が伴うCreateメソッドにおいて戻り値を使わずにIDを呼び出し側へ知らせる方法を提案しています。
CQSの目的
メソッドのシグネチャから、APIの意図を理解しやすくすること。
CQSの概要
CQSはコマンドクエリ分離の略で、副作用を持つか持たないかでメソッドを分離しようとします。その名の通り、コマンドと呼ばれる副作用を持つメソッドとクエリと呼ばれる副作用を持たないメソッドを関数のシグネチャレベルで明確に区別しようとするアーキテクチャパターンになります。
コマンド
コマンドは以下のように副作用を持つメソッドで、戻り値を持ってはいけません。つまり戻り値の型はvoidになります。
void SendEmail(T address,U body)
クエリ
クエリは以下のように副作用を持たないメソッドで、戻り値を持ちます。
Email ViewEmail(T id)
何が嬉しいか
筆者の意見として、開発者がメソッドを使うときに副作用があるかどうかは重要な関心ごとです。副作用のあるメソッドは外部の状態を変更する場合があるので、それに依存するロジックへの影響が発生しうるからです。コマンドとクエリを分離すると、この副作用があるかどうかが関数のシグネチャを見るだけで簡単に認識できます。
CQS違反を克服する一例
CQSとは「副作用を持つなら戻り値を持つな、戻り値を持つなら副作用をなくせ」というルールだといえそうです。
ここで以下のようなDBにCreateし、DBがauto incrementで生成したIDを返す関数を考えてみましょう。副作用があるので戻り値を持ってはいけませんが、この関数は戻り値を持っているのでCQSに違反しています。さて、どうすればいいでしょうか?
public interface IRepository<T>
{
int Create(T item);
// other members
}
最も簡単な解決策は以下のように、IDを関数の引数に含めてしまうことです。こうすれば呼び出し側はIDを知りつつ、CQSを守ることができます。
public interface IRepository<T>
{
void Create(Guid id, T item);
// other members
}
なお、CQSもあくまで関数のアーキテクチャパターンの1つであって常にCQSに従えということでは当然ありません。APIの意図をより短時間で把握できるようになるというメリットと、例えばAPIの使いにくさなどのデメリットのトレードオフは常に発生します。Mark Seemann氏はCQS違反を克服する一例を紹介した理由について「選択肢を知らないのと知った上で採用しないのは別物で、選択肢を知っておくのは重要な設計スキルだからだ」と述べています。
筆者の感想
関数のシグネチャを見れば副作用があるかどうかが分かるというのは、一定readabilityに寄与するとは思う。ただこの記事でも触れたDBへINSERTするケースでは、一意なIDの払い出しをアプリケーション側でやらなければいけなくなるので規模にもよるものの責務が混ざるのが気持ち悪いかもしれない。そして「良い名前をつける」「単一責務で疎結合なコードを書く」「クラス内・パッケージ内では関数の仕事の粒度を揃える」などのよく知られたreadabilityのためのプラクティスで十分読みやすくならないのか、とも思った。個人的には関数に良い名前がついていればCQSにこだわらなくても良くね?と思ってしまった。
メモ
CQSとよく似たCQRSは全くの別物である。CQRSはCQSを拡張したアーキテクチャパターンである。