楽水

人々の創造が自由に表現できる舞台づくり

アプリケーション システム開発 デザインパターン

ソフトウェアの設計原則②コマンド・クエリ分離の原則(CQS)

投稿日:2021年6月30日 更新日:


ソフトウェアの設計原則①:SOLIDの原則という記事で、変化に強いソフトウェアの代表的な特徴として以下をあげ、それを実現する、ソフトウェアの設計原則の一つ、SOLIDについて解説しました。

  • 保守性が高いこと
    修正箇所が局所化され、他の部分に影響しないこと(リスクの局所化)。
  • 再利用性が高いこと
    ソフトウェアが部品化され、それを再利用することができる。
  • 拡張性が高いこと
    ソフトウェアを構成する要素同士を疎結合することで、ソフトウェアを様々な実装で拡張することができる。

今回は、プログラミングにおける副作用を最小限に抑えることで保守性と再利用性を確保するための設計原則、コマンド・クエリ分離の原則(Command/Query Separation:CQS)について以下の観点で解説します。

  • プログラミングにおける副作用とは
  • コマンド・クエリ分離の原則(CQS)とは
  • コマンド・クエリ責務分離(CQRS)パターン
  • 契約による設計

SOLIDの原則と合わせてコマンド・クエリ分離の原則(CQS)を適用することで、さらに変化に強いソフトウェアを設計することができます。

プログラミングにおける副作用とは

Wikipediaでは、プログラミングにおける副作用について次のように説明しています。

式の評価による作用には、主たる作用とそれ以外の副作用(side effect)とがある。
式は、評価値を得ること(関数では「引数を受け取り値を返す」と表現する)が主たる作用(主作用)とされ、それ以外のコンピュータの論理的状態(ローカル環境以外の状態変数の値)を変化させる作用を副作用という。

具体的な例で説明します。

この例の場合、Aクラスのaddメソッドが引数aを受け取って、それに1を足して結果を返し、それが評価されます。
これは、addメソッドの主作用です。
なので、Aクラスのオブジェクトaにadd(1)を処理させると2が返ります。
何度やっても結果は同じです。
それでは、次の例の場合どうでしょうか。

この例の場合、Aクラスのaddメソッドが引数aを受け取って、それにメソッドのスコープ(ローカルと呼びます)の外にある変数(ここでは状態変数と呼んでいます)sを足した後、その結果を状態変数sに代入して返し、それが評価されます。
なので、引数を受け取り値を返すことによる主作用とは別に、ローカル外部の状態変数を変化させるという副作用(本来の作用とは別の副次的な作用)を発生させていることになります。
そうすると、Aクラスのオブジェクトaにadd(1)を処理させると、毎回、違う結果になります。

副作用の影響

なぜ、このようになるのか、副作用の影響について考えてみましょう。
メソッドの本来の作用は、引数を受取り、処理をして結果を返すこと(式が評価されること)で得られます。
なので、メソッドが実行された(式が評価された)ら、受け取った引数は不要となり、消えてしまいます。
しかし、後の例の場合、メソッドのスコープ外の状態変数の値を変化させています。
引数は、メソッドのスコープの内部にありますが、状態変数は、メソッドのスコープの外部にあります。
なので、状態変数の値は、メソッドが実行されてもクラスのオブジェクトが残っている間、残り続けます。
なお、ここ場合の状態変数とはクラスのオブジェクトの状態を表す変数という意味で、オブジェクトが存在する間、時間とともに、その値が変化します。
後の例で、Aクラスのオブジェクトaにadd(1)を処理させると、毎回、違う結果になるのは、addメソッドの終了とともに状態変数の値がなくなるわけではなく、残り続けているので、その値が毎回addメソッドに影響するからです。
次の例を見てみましょう。

Aクラスのmultiplyメソッドは、状態変数sを使っているので、その値が変われば影響を受けます。
なので、addメソッドによる副作用の影響を受けることになります。
このように、コンピュータの論理的状態(ローカル以外の状態)を変化させる機能、つまり副作用を起こす機能は、それ以降で得られる結果に影響を与えることになるのです。
なので、プログラミングによる副作用は、予期せぬエラーを発生させるリスクとなり、それを下げるため以下のようなコストを発生させ、結果的に、ソフトウェアの保守性を下げることになります。

  • 副作用によってどこに影響があるか調査しなければならない
  • その影響によって、動作に支障が出ないようにしなければならない

また、副作用が起こるクラスなどのソフトウェアの構成要素は、常に同じ結果を提供するわけではないので、交換可能な部品としてのモジュール性、再利用性が低くなります。

副作用の制御

副作用によって保守性や再利用性が下がるのであれば、副作用が発生しないようにプログラミングすれば保守性は確保できます。
例えば、メソッドであれば、その引数のみ使って処理するようにプログラミングすればよいわけです。
そうすれば、

  • 同じ条件を与えれば必ず同じ結果が得られる
  • 他のいかなる機能の結果にも影響を与えない

ということになり、予期せぬエラーを発生させるリスクもないし、それを下げるためのコストもかかりません。
この

  • 同じ条件を与えれば必ず同じ結果が得られる
  • 他のいかなる機能の結果にも影響を与えない

というメソッドや関数などの性質を参照透過性(Referential transparency)といいます。
関数やメソッドが、スコープ外部の状態変数を使う場合、状態変数の値は時間や記憶場所によって異なるので、どの値を参照するのかを考慮する必要があります。
しかし引数であれば、自分が参照する値が明確に決まります。
この参照する値が明確に決まることを、参照が明白(transparent)で、どの参照を使うか考慮する必要がなくわかりやすい(transparent)という意味で参照透過性といいます。
数学の関数は、ある変数の値が決まれば、それに対応した値が決まるので、同じ条件を与えれば必ず同じ結果を得ることができます。
数学的な意味での関数を主に使うプログラミングを関数型プログラミングといいますが、関数型プログラミングを使えば参照透過で、副作用がなく保守性の高いソフトウェアをつくることができます。
さて、私たちのまわりを取り巻く現実世界には、時とともに状態が変わるオブジェクトがたくさん存在しています。
ライフサイクルがある生物は典型的な例でしょう。
なので、オブジェクト指向プログラミングで、現実の世界にある認識対象(オブジェクト)を、そのままのかたちで写しとりたい場合、どうしてもオブジェクトの状態を変えるメソッドが必要になります。
それでは、副作用をできるだけ少なくするためにはどうすればよいでしょうか。

コマンド・クエリ分離の原則(CQS)とは

コマンド・クエリ分離の原則(CQS)の定義

今回紹介するコマンド・クエリ分離の原則(CQS)は、副作用を最小限に抑えるための設計原則です。
コマンド・クエリ分離の原則(CQS)は、Bertrand Meyer氏がオブジェクト指向入門 (ASCII SOFTWARE SCIENCE Programming Paradigm)で最初に述べたもので、次のような内容になります。

あらゆるメソッドは、何らかのアクションを実行する「コマンド」あるいは呼び出し元にデータを戻す「クエリ」のいずれか一方でなければならず、その両方の機能を兼ね備えてはならない。
つまり、何かの質問をすることで、その質問への答えが変わってしまうようではならない。
もう少しきちんと言うと、メソッドが値を戻すのは、そのメソッドが参照透過性を持ち、何も副作用をおよぼさない場合だけでなければならない。

これをまとめると次のようになります。

  • あるメソッドがオブジェクトの状態を変更するのなら、そのメソッドはコマンドであり、値を戻してはならない。
  • あるメソッドが何らかの値を戻すのであれば、そのメソッドはクエリであり、オブジェクトの状態を変えてはならない。

何かの質問をすることで、その質問への答えが変わってしまう例としては、JavaのIteratorクラスのnextメソッドがあります。
nextメソッドは、Collectionの次の要素を返すと同時に、Iteratorを次の状態に進めます。
また、MeyerもStackのpopを、何かの質問をすることで、その質問への答えが変わってしまう(状態が変わるので次のpopでは前と異なる回答になる)例としてあげた上で、

このメソッドはできるだけ避けたほうがよいが、便利なイディオム(頻出するコードのパターン)である。
私はできるだけこの原則に従うようにはするが、原則を破ることもいとわない心構えでいる。

と述べています。

なお、オブジェクトの状態を変化させるメソッドはコマンドですが、そのメソッドを実行することでオブジェクトが新しく生成されて戻される場合はクエリになります。
同一のオブジェクトの状態を変えるわけではないからです。

オブジェクトを分析するときは、

  • 同一性(Identity)を持ち、同一のものが状態を変えるオブジェクトなのか、
  • 同一性を持つ必要のないオブジェクトなのか

よく見極める必要があります。
同一性を持つ必要のないオブジェクトであれば、状態を更新するコマンドを設計する必要がなく、それだけ副作用を減らすことができます。
また、単に引数を加工して結果を返すだけのロジックはできる限り副作用の無いクエリとして実装し、実際に状態を変化させるコマンドを最小限に留めるようにします。

コマンド・クエリ分離の原則(CQS)の効用

オブジェクト指向開発で著名なMartin Fowlerは、コマンド・クエリ分離の原則(CQS)の効用について、コマンド・問い合わせの分離という記事で、次のように述べています。

この原則が重要なのは、 状態を変更するメソッドと変更しないメソッドとを明確に分けることで、 非常に扱いやすくなるという点だ。
なぜなら、 様々な状況において自信を持って問い合わせを行うことができるからだ。
どこにでも配置することができるし、順番を入れ替えることもできる。
一方、モディファイア(コマンド)の扱いには注意が必要だ。

副作用をともなう機能は、副作用をともなわない問い合わせ機能を設けることで、結果を戻す必要はなくなります。
コマンド・クエリ分離の原則(CQS)は、両者を明確に分けて設計することで、副作用によるリスクを局所化する働きがあります。
つまり、副作用を最小限に抑えるための設計原則なのです。

コマンド・クエリ責務分離(CQRS)パターン

さて、このコマンド・クエリ分離の原則(CQS)ですが、Greg Youngによってアーキテクチャパターンに応用されました。
それが、コマンド・クエリ責務分離 (CQRS: Command and Query Responsibility Segregation)パターンです。

これは、データストアに対する更新操作を集めたコマンド処理と、読み取り操作(クエリ)を集めたクエリ処理を完全に分離するアーキテクチャパターンです。
CQRSでは、クライアントからのコマンドは一方通行で書き込み用データストアに届き、クエリは別の読み込み用データストアに対して発行します。
なお、コマンド処理によって書き込み用データストアを更新するタミングで、読み込み用データストアも更新し、互いの同期をとります。
コマンド・クエリ責務分離(CQRS)パターンを適用することによって、保守性や再利用性を上げるとともに、次のような効果を得ることができます。

  • コマンド処理とクエリ処理を切り離すことで、読み取りによる負荷が減り、更新処理のパフォーマンスを上げることができる。
  • コマンド処理とクエリ処理を切り離すことで、データストアに対するロック競合を減らすことができる。
  • 書き込み側では更新用に最適化されたスキーマ(例えば、リレーショナルデータベース)を使用し、読み取り側ではクエリ用に最適化されたスキーマ(例えば、ドキュメント型データベース)を使用することができる。
  • 具体化されたビューを読み取りデータストアに格納することで、クエリ時の複雑な結合を回避することができる。
  • データの読み込みや書き込みに対するアクセス制限の設計が容易になりセキュリティを上げることができる。

契約による設計

これまで見てきたように、コマンド・クエリ分離の原則(CQS)を適用することによって副作用を最小限に抑えることができます。
しかし、副作用をともなうコマンドを完全になくすことは困難です。
なので、副作用のあるコマンドを少しでも理解しやすい形で表現する必要があります。
ここでは、プログラムコードの中にプログラムが満たすべき仕様についての記述を盛り込む事で設計の安全性を高める技法、契約による設計(Design By Contract)について説明します。
契約による設計では、コードの利用条件を主処理とは別に定義することでエラーの位置を明確にします。
もし、契約違反が発生すると例外などの形で実行は中断されます。
契約による設計をコマンドに適用する場合、契約は、コマンドを実行する側と、コマンドを呼び出す側の間で、次の3種類の利用条件が満たされることによって成立します。

  • 事前条件 (precondition)
    コマンドの開始時に、コマンドを呼び出す側で保証すべき条件。
  • 事後条件 (postcondition)
    コマンドが終了時に、コマンドを実行する側が保証すべき条件。
  • 不変条件 (invariant)
    コマンドの開始から終了まで、両者が一貫して維持すべき条件。


さて、利用条件は、プログラムのその箇所で必ず真であるべき条件を式として表明(assert)することで記述します。
なので、条件が満たされない場合、契約違反となりエラーが発生します。

以上、今回は、コマンド・クエリ分離の原則(CQS)について解説しました。

-アプリケーション, システム開発, デザインパターン

執筆者:


  1. […] 、一時的に整合していない(矛盾している)状態があっても、最終的に整合させることはできます。 結果整合性を実現する方法には、Sagas、CQRS、非同期メッセージングなどがあります。 […]

関連記事

アプリケーションアーキテクチャ(AA)とは【わかりやすく解説】

ここでは、以下の観点で、アプリケーションアーキテクチャ(AA)について解説します。 アプリケーションアーキテクチャとは DXとアプリケーションアーキテクチャ アプリケーションアーキテクチャ設計の進め方 …

MVC vs MVVM

ここでは、MVCとMVC2の違いについて以下の観点で解説します。 MVCとは何か MVVMとは何か MVC vs MVVM MVCについて詳しく知りたいかたは、 MVCとは【本来の仕組を詳しく解説】 …

デザインパターンとは

デザインパターンとは、ソフトウェア設計者(アーキテクト)が、過去に編み出した設計ノウハウを蓄積し、名前をつけて、再利用しやすいようにカタログ化したものです。 デザインパターンといえばオブジェクト指向に …

ドメイン駆動設計入門【DDDをわかりやすく解説】

突然ですが、エンジニアの皆さま、Javaで開発したWebアプリケーションの構成、このようになっていませんか? データとgetter/setterだけのオブジェクト(JavaBean) 画面のコントロー …

ソフトウェアの設計原則③GRASP

ソフトウェアの設計原則①:SOLIDの原則という記事で、変化に強いソフトウェアの代表的な特徴 保守性が高いこと 修正箇所が局所化され、他の部分に影響しないこと(リスクの局所化)。 再利用性が高いこと …