私はこの質問に対する答えを何週間も探していました。これは、ほとんどすべてのアプリケーションに共通であり、したがってCQSの最前線にある問題のように思えます。
CQSは、関数が次のいずれかであることを指示します。
そこで、新しいデータエンティティを作成し、それをデータベースに永続化して、自動インクリメントされたIDの通知を発行するとします。
まず、オブジェクトを作成します。この時点では、一意の識別子はありません。次に、それをデータベースに保存します。これにより、自動インクリメントされたIDフィールドを介して一意の識別子が生成されます。クエリ実行結果がアプリケーションに返され、結果のデータベース行が返されます。これを調べてIDを特定できます。 (q.v。 PostgreSQL INSERT ... RETURNING )
コマンドとして挿入を発行していることを考えると(それはisオブジェクトのIDをnull
から "something"に変更しています) 、およびコマンドはvoidを返す必要があります。生成されたIDはどのように決定しますか?
挿入をコマンドとして発行し(オブジェクトのIDをnullから "何か"に変更している)、コマンドがvoidを返さなければならない場合、生成されたIDはどのように決定しますか?
缶に書かれているように、クエリを発行する:
_object.save();
id = object.getAutoIncrementedId();
_
トリッキーな部分は、この場合save()が何をしているかを理解することです。
_void DB::addRow(state)
void Object::setAutoIncrementedId(id);
_
しかし、これらの署名は完全には正しくありません。それらの間でデータを交換するために必要なアフォーダンスはありません。 callbackでそれを実現できます
_void DB::addRow(state, callback)
void Object::setAutoIncrementedId(id);
_
したがって、save()
コマンドは次のようになります。
_void Object::save() {
db.addRow(this.state(), this::setAutoIncrementedId);
}
_
スペルを少し変更すると、これはより身近に見えるかもしれません
_void DB::addRow(state, onSuccess, onError)
_
つまり、DBにメッセージを送信し、データベースが追加のメッセージを転送するために使用する必要があることをメッセージの受信トレイに記述します。
コメントで述べたように、Seemannはかなり良い本です
彼は、Palmをカードだと思いますが... GUID
はどこから来たのですか? Guid.NewGuid()
は意味的には safe ですが、実際には副作用がないわけではありません。したがって、本当にすべてをCQSに入れる場合、シグネチャは次のようになります。
_void Guid::NewGuid(callback)
_
アプリケーションとその副作用 を参照してください。 Stack
の例と同様に、「純度」を「明快さ」と「精通度」と交換することは理にかなっています。
厳密に言えば、自動生成されたIDを返すと、 [〜#〜] cqs [〜#〜] の原則に違反します。あなただけではそれを壊すことができないことができない場合があります。スタックは別の例です。 pop操作は常にコマンドとクエリです。これはスタックが間違っていることを意味するのではなく、本当にスタックが必要な場合に適しています。
この細かい原則を破りたくない場合は、GUIDまたはUUIDをエンティティIDとして使用し、自動生成されたIDを surrogate IDとしてのみ使用できるようにすることができます。このようにして、返品する必要はありません)。
私もこの質問に苦労しています。問題が発生する可能性のあるあらゆる種類の回答を見てきました。 GUIDでは、選択して戻すためにそれを保存するためにDBを変更する必要があります。つまり、CQSに準拠するためだけにDBモデルを変更しています。 HiLoはそれができれば機能しますが、すべての人が新しいデータモデルをゼロから操作できるわけではなく、ERPそれを許可しないシステムを使用している可能性があります。送信する必要があると言う人もいます機能する可能性のある通知イベントですが、たとえばWeb API呼び出しからの応答でその項目を直接使用する場合は非常に煩わしく感じます。別のオプションは、コマンド自体をデータで更新することです。最後に、データを取得するために最初に何かを変更する必要があるキュー/スタック全体の問題があります。
最終的に、Command、Query、およびCommandQueryオプションを使用できるという結論に達しました。すべて3のハンドラーインターフェイスがあります。これはCQCQSであるため、技術的にはCQSを中断しません。 :)とはいえ、コードでCommandQueryを使用する場合は、非常に適切な理由があり、その理由を文書化する必要があります。 CQSは優れたパターンですが、パターンが最終的に達成しようとしていることの邪魔になってはいけません。発生する可能性のあるすべてのシナリオに対してガイドラインを設定するだけでよいので、選択肢がなく、同じ問題を処理するための10の異なる方法が存在する場合の解決策があります。
しばらくライブファイアシナリオでこの問題に取り組み、2、3か月考えた結果、CQSの原則に違反したり、@ VoiceOfUnreasonとして「カードを手放す」ことを含まない、きちんとした解決策があると思います。私が受け入れた答えに入れてください。
結果を返すサービス(データベース、サードパーティAPIなど)に操作をコミットしていると仮定します。この操作は実質的にはコマンドです。しかし、結果が得られることはわかっています。これは、操作をコミットすることの事前にを知っています。
伝統的に、CQSに関係なく、次のようなことをするかもしれません(PHP構文を許してください、それは私の毒です):
_interface UserPersistenceService
{
public function saveUser(User $user): int;
}
$userID = $this->userPersistenceService()->saveUser($user);
_
これで、コマンドを使用する場合、saveUser()
メソッドにint
戻り値の型を指定することはできません(これによりクエリになります)。コマンドは、定義により、既存のオブジェクトを変更する可能性がありますが、戻り値の型がvoidでなければなりません。この操作の結果があることはすでにわかっているので、なぜ空白の操作結果を作成し、それをsaveコマンドの2番目のパラメーターとして渡し、操作後にuserID
についてその結果を問い合わせます。コミットされていますか?
_interface UserPersistenceService
{
public function saveUser(User $user, SaveOperation $saveOperation): void;
}
class SaveOperation()
{
private $isProcessed = false;
private $result;
public function processResult($result): void
{
$this->Result = $result;
$this->isProcessed = true;
}
public function result(): DatabasePersistenceResult
{
if($this->isProcessed() === false) {
throw new ResultNotProcessedException();
} else {
return $this->result;
}
}
}
$saveOperation = new SaveOperation();
$this->userPersistenceService()->saveUser($user, $saveOperation);
if($saveOperation->result()->wasSuccessful()) {
$userID = $saveOperation->result()->lastInsertID();
} else {
throw new SaveOperationFailedException();
}
_
私にはこれは良いアプローチのように見えます-私たちはCQSの原則に違反しておらず、GUIDの使用に伴う問題を回避しています。さらに、脆弱な可能性のある外部の結果を処理するときに自分自身をはるかに制御できるようにしています操作(つまり、APIに到達できない、サードパーティのAPIのバグ、データベースの競合など)