最近、一見ささいなアーキテクチャの問題に直面しました。私のコードには、次のように呼び出される単純なリポジトリがありました(コードはC#です)。
_var user = /* create user somehow */;
_userRepository.Add(user);
/* do some other stuff*/
_userRepository.SaveChanges();
_
SaveChanges
は、データベースへの変更をコミットする単純なラッパーでした:
_void SaveChanges()
{
_dataContext.SaveChanges();
_logger.Log("User DB updated: " + someImportantInfo);
}
_
その後、しばらくすると、システムでユーザーが作成されるたびに電子メール通知を送信する新しいロジックを実装する必要がありました。システムの周りで_userRepository.Add()
およびSaveChanges
への呼び出しが多かったため、SaveChanges
を次のように更新することにしました。
_void SaveChanges()
{
_dataContext.SaveChanges();
_logger.Log("User DB updated: " + someImportantInfo);
foreach (var newUser in dataContext.GetAddedUsers())
{
_eventService.RaiseEvent(new UserCreatedEvent(newUser ))
}
}
_
このように、外部コードはUserCreatedEventをサブスクライブし、通知を送信する必要なビジネスロジックを処理できます。
しかし、SaveChanges
の変更が単一責任の原則に違反していること、およびSaveChanges
がイベントを発生させずに保存するだけでよいことが指摘されました。
これは有効なポイントですか?ここでイベントを発生させることは基本的にロギングと同じであるように見えます。関数にいくつかのサイド機能を追加するだけです。また、SRPは、関数でのイベントのロギングまたは発生の使用を禁止していません。そのようなロジックは他のクラスにカプセル化する必要があるとだけ述べており、リポジトリがこれらの他のクラスを呼び出しても問題ありません。
はい、Add
やSaveChanges
などの特定のアクションで特定のイベントを発生させるリポジトリがあることは有効な要件となる可能性があります。ユーザーを追加してメールを送信する具体的な例は、少し不自然に見えるかもしれません。以下では、この要件がシステムのコンテキストで完全に正当化されていると仮定します。
したがって、yes、イベントメカニズムのエンコード、ロギング、および1つのメソッドへの保存は、SRPに違反します。 。多くの場合、特に「変更の保存」と「イベントの発生」のメンテナンス責任を別のチーム/メンテナに分配したくない場合は、おそらく許容できる違反です。しかし、誰かがこれを正確に実行したいと思ったある日を考えてみましょう。単純に、おそらくそれらの懸念のコードを別のクラスライブラリに置くことによって解決できますか?
これに対する解決策は、元のリポジトリにデータベースへの変更をコミットする責任を負わせ、それ以外は何もせず、まったく同じパブリックを持つproxyリポジトリを作成することですインターフェイス、元のリポジトリを再利用し、メソッドに追加のイベントメカニズムを追加します。
// In EventFiringUserRepo:
public void SaveChanges()
{
_basicRepo.SaveChanges();
FireEventsForNewlyAddedUsers();
}
private void FireEventsForNewlyAddedUsers()
{
foreach (var newUser in _basicRepo.DataContext.GetAddedUsers())
{
_eventService.RaiseEvent(new UserCreatedEvent(newUser))
}
}
@Peterの 投票数の多い回答 の行に沿って、必要に応じてプロキシクラスをNotifyingRepository
またはObservableRepository
と呼ぶことができます(実際の解決方法はわかりません) SRP違反、違反は問題ないと言っているだけです)。
新旧のリポジトリクラスは両方とも、 クラシックプロキシパターンの説明に示されている のような共通のインターフェイスから派生する必要があります。
次に、元のコードで、新しいEventFiringUserRepo
クラスのオブジェクトによって_userRepository
を初期化します。このようにして、元のリポジトリをイベントのメカニズムから分離します。必要に応じて、イベント起動リポジトリと元のリポジトリを並べて配置し、呼び出し元に前者と後者のどちらを使用するかを決定させることができます。
コメントで言及された懸念に対処するために:それはプロキシーの上にプロキシーの上にプロキシーにつながるなどではないのですか?実際、イベントメカニズムを追加するとイベントをサブスクライブするだけで「メールを送信する」タイプの要件をさらに追加し、追加のプロキシを使用せずに、これらの要件もSRPに固執します。ただし、ここで一度追加する必要があるのは、イベントのメカニズム自体です。
この種の分離がシステムのコンテキストで本当に価値がある場合は、あなたとレビュー担当者が自分で決定する必要があります。おそらく、可能であれば、ロギングを元のコードから分離せず、別のプロキシを使用して、ロガーをリスナーイベントに追加することもしません。
永続データストアが変更されたという通知を送信することは、保存するときに賢明なことのように思えます。
もちろん、Addを特別なケースとして扱うべきではありません。ModifyとDeleteのイベントも発生させる必要があります。これは、匂いがし、なぜ匂いがするのかを読者に説明させ、最終的にコードの一部の読者をSRPに違反しなければならないと結論付ける「追加」ケースの特別な扱いです。
照会、変更、および変更時にイベントを発生させることができる「通知」リポジトリは、完全に通常のオブジェクトです。きちんとしたサイズのほとんどのプロジェクトで、その複数のバリエーションを見つけることが期待できます。
しかし、「通知」リポジトリは実際に必要なものですか?あなたはC#について言及しました:多くの人はSystem.Collections.ObjectModel.ObservableCollection<>
の代わりに System.Collections.Generic.List<>
後者が必要なのは、あらゆる種類の悪い点と間違った点だけですが、すぐにSRPを指すものはほとんどありません。
あなたが今やっていることはあなたのUserList _userRepository
とObservableUserCollection _userRepository
。それが最善の行動であるかどうかは、アプリケーションによって異なります。しかし、それは間違いなく_userRepository
かなり軽量ではありませんが、私の考えでは、SRPに違反していません。
はい、それは単一責任原則の違反であり、有効なポイントです。
別のプロセスでリポジトリから「新しいユーザー」を取得し、メールを送信することをお勧めします。メール、失敗、再送信などが送信されたユーザーを追跡します。
このようにして、エラーやクラッシュなどを処理できるだけでなく、「データベースに何かがコミットされたときに」イベントが発生するという考えを持つリポジトリがすべての要件を取得することを回避できます。
リポジトリは、追加したユーザーが新しいユーザーであることを認識していません。その責任は単にユーザーを保存することです。
以下のコメントを拡張する価値があるでしょう。
リポジトリは、追加したユーザーが新しいユーザーであることを認識していません-はい、そうです。Addというメソッドがあります。そのセマンティクスは、追加されたすべてのユーザーが新規ユーザーであることを意味します。 Saveを呼び出す前にAddに渡されたすべての引数を結合します-そして、すべての新しいユーザーを取得します
不正解です。 「リポジトリに追加」と「新規」を融合しています。
「リポジトリに追加されました」とは、その内容を意味します。ユーザーをさまざまなリポジトリに追加、削除、および再追加できます。
「新規」は、ビジネスルールによって定義されたユーザーの状態です。
現在、ビジネスルールは「新規==リポジトリに追加されたばかり」である可能性がありますが、それは、そのルールについて知って適用することは別個の責任ではないという意味ではありません。
この種のデータベース中心の考え方を回避するように注意する必要があります。新しいケースではないユーザーをリポジトリに追加するEdgeケースプロセスがあり、それらにメールを送信すると、すべてのビジネスは「もちろん、これらは「新しい」ユーザーではありません!actualルールはXです」
この答えは私見のポイントがかなり欠けています:リポジトリは新しいユーザーが追加されたときに知っているコードの中心的な場所です
不正解です。上記の理由から、イベントを発生させるだけでなく、実際にメール送信コードをクラスに含めない限り、これは中心的な場所ではありません。
リポジトリクラスを使用するアプリケーションはありますが、メールを送信するコードはありません。これらのアプリケーションにユーザーを追加すると、メールは送信されません。
これは有効なポイントですか?
はい、そうですが、コードの構造に大きく依存します。私は完全なコンテキストを持っていないので、一般的に話そうとします。
ここでイベントを発生させることは基本的にロギングと同じであるように見えます。関数にいくつかのサイド機能を追加するだけです。
それは絶対に違います。ロギングはビジネスフローの一部ではなく、無効にすることができます。何らかの理由でログに記録できなかった場合でも、(ビジネス)副作用を引き起こしたり、アプリケーションの状態や状態に影響を与えたりしてはなりません。もう何でも。追加したロジックと比較してください。
また、SRPは、関数でのイベントのロギングまたは起動の使用を禁止していません。そのようなロジックは他のクラスにカプセル化する必要があるとだけ言っており、リポジトリがこれらの他のクラスを呼び出しても問題ありません。
SRPはISPと連携して機能します(SOLIDではSとI)。結局、非常に具体的なことだけを行い、他には何もしないクラスやメソッドがたくさんできます。それらは非常に集中しており、更新または置換が非常に簡単で、一般にテストが簡単です。もちろん実際には、オーケストレーションを処理するいくつかのより大きなクラスもあります。それらは多数の依存関係を持ち、アトマイズされたアクションではなく、複数のステップを必要とするビジネスアクションに焦点を当てます。ビジネスコンテキストが明確である限り、それらは単一の責任と呼ぶこともできますが、正しく言ったように、コードが大きくなるにつれて、その一部を新しいクラス/インターフェイスに抽象化したい場合があります。
ここで、特定の例に戻ります。ユーザーの作成時に必ず通知を送信する必要があり、さらに他のより特殊なアクションを実行する必要がある場合は、UserCreationService
のような、1つのメソッドAdd(user)
、ストレージ(リポジトリの呼び出し)と通知の両方を1つのビジネスアクションとして処理します。または、元のスニペットで_userRepository.SaveChanges();
の後に実行します
ボブおじさんが彼の記事で説明しているように、SRPは理論的にはpeopleについてです The Single Responsibility Principle 。 コメントで提供してくれたRobert Harveyに感謝します。
正しい質問は:
その利害関係者がデータの永続化も担当している場合(可能性は低いですが可能です)、これはSRPに違反しません。それ以外の場合はそうです。
技術的にはイベントを通知するリポジトリには何の問題もありませんが、その便利さを懸念する機能的な観点からそれを検討することをお勧めします。
ユーザーの作成、新しいユーザーとは何か、その永続性は3つの異なるものです。
私の前提
(SRPに関係なく)リポジトリがビジネスイベントを通知する適切な場所であるかどうかを決定する前に、前の前提を検討してください。 UserCreated
にはUserStored
やUserAdded
とは異なる意味があるため、ビジネスイベントと言ったことに注意してください 1。また、これらの各イベントをさまざまな対象者に向けて検討することも検討します。
一方では、ユーザーの作成はビジネスに固有のルールであり、永続性を伴う場合と伴わない場合があります。それはより多くのデータベース/ネットワーク操作を含むより多くのビジネス操作を含むかもしれません。永続層が認識しない操作。永続層には、ユースケースが正常に終了したかどうかを判断するための十分なコンテキストがありません。
反対に、_dataContext.SaveChanges();
がユーザーを首尾よく持続させているとは限りません。データベースのトランザクションスパンによって異なります。たとえば、トランザクションがアトミックであるMongoDBなどのデータベースには当てはまる可能性がありますが、関与するトランザクションがまだあり、まだコミットされていない可能性があるACIDトランザクションを実装する従来のRDBMSには当てはまりません。
これは有効なポイントですか?
かもしれない。ただし、SRP(技術的に言えば)の問題だけでなく、便利さ(機能的にも)の問題でもあります。
ここでイベントを発生させることは本質的にロギングと同じであるように思えます
絶対違う。ただし、イベントUserCreated
によって他のビジネスオペレーションが発生する可能性があることが示唆されたため、ロギングは副作用がないことを意味します。通知のような。 3
そのようなロジックは他のクラスにカプセル化する必要があるとだけ言っており、リポジトリがこれらの他のクラスを呼び出すことは問題ありません
必ずしもそうではありません。 SRPはクラス固有の問題ではありません。レイヤー、ライブラリ、システムなどの抽象化のさまざまなレベルで動作します!それはまとまりについて、同じ理由で何が変わるかをまとめることについてです 同じ利害関係者の手によって 。ユーザーの作成(ユースケース)が変更された場合、それはおそらく瞬間であり、イベントが発生する理由も変更されます。
1:適切に名前を付けることも重要です。
2:_dataContext.SaveChanges();
の後にUserCreated
を送信しましたが、接続の問題または制約違反のため、データベーストランザクション全体が後で失敗したとします。イベントの早期ブロードキャストには注意が必要です。その副作用は元に戻すのが難しい場合があります(それが可能な場合でも)。
3:通知プロセスが適切に処理されないと、元に戻すことができない通知を起動する可能性があります/ sup>
単一責任の原則について心配する必要はありません。ここでは、特定のコンセプトを「責任」として主観的に選択できるため、良い判断を下すのに役立ちません。クラスの責任はデータベースへのデータの永続性の管理であると言うことも、ユーザーの作成に関連するすべての作業を実行することの責任と言うこともできます。これらはアプリケーションの動作の異なるレベルであり、どちらも「単一の責任」の有効な概念的表現です。したがって、この原則は問題の解決には役立ちません。
この場合に適用する最も有用な原則は 最小の驚きの原則 です。それでは、質問をしましょう:データベースにデータを永続化するという主要な役割を持つリポジトリが電子メールも送信するのは驚きですか?
はい、驚くべきことです。これらは2つの完全に異なる外部システムであり、SaveChanges
という名前は通知の送信も意味しません。これをイベントに委任すると、動作がさらに驚くべきものになります。これは、コードを読んでいる人が、呼び出された追加の動作を簡単に確認できないためです。間接参照は可読性を損ないます。場合によっては、利点は読みやすさのコストに見合うものですが、エンドユーザーに観察可能な影響を与える追加の外部システムを自動的に呼び出す場合はそうではありません。 (ロギングは、デバッグの目的で本質的にレコードを保持するため、ここで除外できます。エンドユーザーはログを消費しないため、常にロギングすることに害はありません。)さらに悪いことに、これはの柔軟性を低下させます。電子メールを送信するタイミング。保存と通知の間に他の操作をインターリーブすることを不可能にします。
通常、ユーザーが正常に作成されたときにコードで通知を送信する必要がある場合は、それを行うメソッドを作成できます。
_public void AddUserAndNotify(IUserRepository repo, IEmailNotification notifier, MyUser user)
{
repo.Add(user);
repo.SaveChanges();
notifier.SendUserCreatedNotification(user);
}
_
しかし、これが付加価値を与えるかどうかは、アプリケーションの仕様によって異なります。
実際には、SaveChanges
メソッドの存在をまったく思いとどまらせます。このメソッドはおそらくデータベーストランザクションをコミットしますが、他のリポジトリが同じトランザクションでデータベースを変更した可能性があります。 SaveChanges
は特にユーザーリポジトリのこのインスタンスに関連付けられているため、これらすべてがコミットされるという事実は驚くべきことです。
データベーストランザクションを管理する最も簡単なパターンは、外側のusing
ブロックです。
_using (DataContext context = new DataContext())
{
_userRepository.Add(context, user);
context.SaveChanges();
notifier.SendUserCreatedNotification(user);
}
_
これにより、allリポジトリの変更を保存するタイミングをプログラマーが明示的に制御できるようになり、コミット前に発生する必要がある一連のイベントをコードに明示的に文書化させ、ロールバックが発行されるようにしますエラー(_DataContext.Dispose
_がロールバックを発行すると想定)し、ステートフルクラス間の非表示の接続を回避します。
また、リクエストで直接電子メールを送信したくない場合もあります。キューの通知のneedを記録する方がより堅牢です。これにより、より適切な障害処理が可能になります。特に、メールの送信中にエラーが発生した場合、ユーザーの保存を中断することなく後で再試行することができ、ユーザーが作成されてもサイトからエラーが返されるという事態を回避できます。
_using (DataContext context = new DataContext())
{
_userRepository.Add(context, user);
_emailNotificationQueue.AddUserCreateNotification(user);
_emailNotificationQueue.Commit();
context.SaveChanges();
}
_
context.SaveChanges()
呼び出しが失敗した場合、電子メールを送信する前にキューのコンシューマーがユーザーが存在することを確認できるため、最初に通知キューをコミットすることをお勧めします。 (それ以外の場合は、ハイゼンバグを回避するために本格的な2フェーズコミット戦略が必要になります。)
一番下の行は実用的です。実際に、コードを書くことの結果(リスクと利益の両方の観点から)を特定の方法で検討してください。 「単一の責任の原則」はそれを行うのにあまり役立ちませんが、「最小の驚きの原則」は他の開発者の頭に(いわば)入り込み、何が起こるかについて考えるのに役立つことが多いと思います。
NoこれはSRPに違反していません。
多くの人は、単一の責任の原則は、機能が「1つのこと」だけを行うべきであり、「1つのこと」を構成するものについての議論に巻き込まれることを意味すると考えているようです。
しかし、それは原則が意味するものではありません。それはビジネスレベルの懸念についてです。クラスは、ビジネスレベルで個別に変更される可能性のある複数の懸念事項や要件を実装しないでください。クラスがユーザーを保存し、ハードコードされたウェルカムメッセージを電子メールで送信するとします。複数の独立懸念があると、そのようなクラスの要件が変更される可能性があります。デザイナーは、メールのhtml /スタイルシートを変更するよう要求することができます。コミュニケーションの専門家は、メールの文言を変更するよう要求することができます。また、UXの専門家は、オンボーディングフローの別の時点で実際にメールを送信するように決定できます。したがって、クラスは独立したソースからの複数の要件変更の対象となります。これはSRPに違反しています。
ただし、イベントはユーザーの保存にのみ依存し、他の問題には依存しないため、イベントを発生させてもSRPに違反しません。リポジトリがメールの影響を受けたり、メールに気づかれたりせずに、保存によってEメールをトリガーできるので、イベントは実際にはSRPを維持するための本当に素晴らしい方法です。
現在SaveChanges
はtwoを実行します:変更を保存しますandログを保存します。次に、メール通知を送信してください。
イベントを追加するという賢いアイデアがありましたが、これは単一の責任原則(SRP)に違反していると批判されましたが、すでに違反されていることに気づきませんでした。
純粋なSRPソリューションを取得するには、firstでイベントをトリガーし、、次にを呼び出しますそのイベントのすべてのフック。そのうち3つがあります。保存、ロギング、最後にメールを送信することです。
最初にイベントをトリガーするか、SaveChanges
に追加する必要があります。あなたのソリューションは、2つの間のハイブリッドです。既存の違反には対応していませんが、3つ以上の増加を防ぐことを推奨しています。 SRPに準拠するように既存のコードをリファクタリングするには、必要以上に多くの作業が必要になる場合があります。彼らがSRPをどれだけ活用したいかはプロジェクト次第です。