単一責任の原則の適用可能性


40

私は最近、一見些細な建築上の問題に直面しました。私のコードには、次のように呼ばれるシンプルなリポジトリがありました(コードは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は、関数でのログ記録またはイベントの使用を禁止していません。そのようなロジックは他のクラスにカプセル化する必要があると述べているだけで、リポジトリがこれらの他のクラスを呼び出すことは問題ありません。


22
あなたのレトルトは、「わかりました。それでは、SRPに違反せずに、単一の変更ポイントを許可するように、どのよう記述しますか?」
ロバートハーヴェイ

43
私の観察では、イベントを発生させても追加の責任は追加されません。実際、まったく逆です。他のどこかに責任を委任します。
ロバートハーヴェイ

あなたの同僚は正しいと思いますが、あなたの質問は有効で役に立つので、賛成です!
アンドレスF.

16
単一の責任の明確な定義などはありません。それがSRPに違反していると指摘した人は、個人的な定義を使用して正しく、あなたは自分の定義を使用して正しいです。このイベントは、他の同様の機能がさまざまな方法で行われる1回限りのイベントではないことに注意してください。一貫性は、システムで作業を行う方法を誰も知らない非常に理解しやすいクラスのトンで終わるSRPのような曖昧なガイドラインよりもはるかに重要です。
ダンク

回答:


14

はい、Addまたはのような特定のアクションで特定のイベントを起動するリポジトリを持つことが有効な要件になる可能性がありますSaveChanges-そして、ユーザーを追加してメールを送信する特定の例が少し不自然です。以下では、この要件がシステムのコンテキストで完全に正当化されると仮定します。

だから、はい、一つの方法にイベントの仕組みをコードするだけでなく、ログだけでなく、貯蓄はSRPを違反します。多くの場合、特に「変更の保存」と「イベントの発生」の保守責任を別のチーム/メンテナーに分配したくない場合は、おそらく許容される違反です。しかし、いつか誰かがまさにこれをやりたいと思っていると仮定すると、おそらくそれらの懸念のコードを異なるクラスライブラリに入れることで、簡単な方法で解決できますか?

これに対する解決策は、元のリポジトリがデータベースへの変更をコミットする責任を負うことであり、まったく同じパブリックインターフェイスを持つプロキシリポジトリを作成し、元のリポジトリを再利用して、メソッドにイベントメカニズムを追加します。

// In EventFiringUserRepo:
public void SaveChanges()
{
  _basicRepo.SaveChanges();
   FireEventsForNewlyAddedUsers();
}

private void FireEventsForNewlyAddedUsers()
{
  foreach (var newUser in _basicRepo.DataContext.GetAddedUsers())
  {
     _eventService.RaiseEvent(new UserCreatedEvent(newUser))
  }
}

プロキシクラスを呼び出すことができます。NotifyingRepositoryまたは、必要にObservableRepository応じて、@ Peterの高投票の回答の行に沿って呼び出すことができます(実際には、SRP違反を解決する方法はわかりません。

新しいリポジトリクラスと古いリポジトリクラスは、両方とも、古典的なプロキシパターンの説明に示されているように、共通のインターフェースから派生する必要があります。

次に、元のコード_userRepositoryで、新しいEventFiringUserRepoクラスのオブジェクトで初期化します。このようにして、元のリポジトリをイベントメカニズムから分離した状態に保ちます。必要に応じて、イベント起動リポジトリと元のリポジトリを並べて使用し、呼び出し元に前者と後者のどちらを使用するかを決定させることができます。

コメントで言及された1つの懸念を解決するために、それはプロキシの上にあるプロキシの上にプロキシをもたらしませんか?実際、イベントメカニクスを追加すると、イベントにサブスクライブするだけで「メールを送信する」タイプの要件を追加するための基盤が作成されます。ただし、ここで1つ追加する必要があるのは、イベントメカニズム自体です。

この種の分離がシステムのコンテキストで本当に価値がある場合は、あなたとあなたのレビュー担当者が自分で決定する必要があります。可能であれば、別のプロキシを使用することも、ロガーをリスナーイベントに追加することも、元のコードからロギングを分離することはないでしょう。


3
この答えに加えて。AOPのようなプロキシの代替手段があります。
ライヴ

1
イベントを発生させるとSRPが破損するわけではなく、「新規」ユーザーのみのイベントを発生させるには、「新規追加」ではなく「新規」ユーザーを構成するものを知ることがレポの責任であることが必要です"ユーザー
ユアン

@ユアン:質問をもう一度読んでください。それは、そのオブジェクトの責任の範囲外の他のアクションと結合する必要がある特定のアクションを行うコード内の場所についてでした。そして、アクションとイベントレイズを1か所に置くことは、SRPを破るとして査読者によって質問されました。「新しいユーザーの保存」の例はデモンストレーションのみを目的としており、必要に応じて考案された例を呼び出しますが、それは私見ではありません。
ドックブラウン

2
これが最良/正解IMOです。SRPを維持するだけでなく、オープン/クローズの原則も維持します。そして、クラス内の変更が壊れる可能性のある自動テストのすべてを考えてください。新しい機能が追加されたときに既存のテストを変更することは大きな悪臭です。
キースペイン

1
進行中のトランザクションがある場合、このソリューションはどのように機能しますか?それが行われているとき、SaveChanges()実際にデータベースレコードを作成するわけではなく、最終的にロールバックされる可能性があります。AcceptAllChangesTransactionCompletedイベントをオーバーライドまたはサブスクライブする必要があるようです。
ジョン・ウー

29

永続データストアが変更されたという通知を送信することは、保存時に行うべき賢明なことのように思えます。

もちろん、Addを特別なケースとして扱うべきではありません。ModifyとDeleteのイベントも発生させる必要があります。これは、「追加」ケースの特別な処理であり、匂いを嗅ぎ、読者に匂いがする理由を説明させ、最終的にコードの一部の読者に、SRPに違反する必要があると結論付けさせます。

照会、変更、および変更時にイベントを発生させることができる「通知」リポジトリは、完全に正常なオブジェクトです。ほぼすべての適切なサイズのプロジェクトで、その複数のバリエーションを見つけることができます。


しかし、実際に必要なのは「通知」リポジトリですか?あなたはC#について言及しました:多くの人々は、後者があなたが必要とするすべてではSystem.Collections.ObjectModel.ObservableCollection<>なくaを使用するSystem.Collections.Generic.List<>ことはあらゆる種類の悪いことと間違っていることに同意しますが、すぐにSRPを指す人はほとんどいません。

あなたが今していることはあなたUserList _userRepositoryを交換することObservableUserCollection _userRepositoryです。それが最善の行動かどうかは、アプリケーションによって異なります。しかし、間違いなく_userRepositoryかなり軽量になりますが、私の謙虚な意見では、SRPに違反していません。


使用に問題がObservableCollectionこのような場合のためにはそれがないの呼び出しで同等のイベントをトリガーする、であるSaveChangesが、への呼び出しで、Add例に示したものとは非常に異なる行動につながります、。元のリポジトリを軽量に保ち、セマンティクスをそのままにしてSRPに固執する方法についての私の答えをご覧ください。
Doc Brown

@DocBrown既知のクラスObservableCollection<>List<>、比較とコンテキストのために呼び出しました。内部実装または外部インターフェースのいずれかに実際のクラスを使用することを推奨するつもりはありませんでした。
ピーター

わかりましたが、OPが「変更」と「削除」にイベントを追加したとしても(簡単にするためにOPは質問を簡潔にするために省略したと思います)、レビューアは簡単に結論を出すことができると思いますSRP違反。おそらく許容範囲内ですが、必要な場合に解決できないものはありません。
ドックブラウン

16

はい、それは単一の責任原則の違反であり、有効なポイントです。

より良い設計は、別のプロセスがリポジトリから「新しいユーザー」を取得し、電子メールを送信することです。どのユーザーにメールが送信されたか、失敗、再送などを追跡します。

このようにして、エラーやクラッシュなどを処理できるほか、イベントが「データベースにコミットされたときに」発生するという考えを持つすべての要件をリポジトリが取得することを回避できます。

リポジトリは、追加したユーザーが新しいユーザーであることを認識しません。その責任は、単にユーザーを保存することです。

以下のコメントを拡大する価値があるでしょう。

リポジトリは、追加したユーザーが新しいユーザーであることを認識していません。はい、認識しています。Addというメソッドがあります。そのセマンティクスは、追加されたすべてのユーザーが新しいユーザーであることを意味します。Saveを呼び出す前に、Addに渡されたすべての引数を組み合わせます-すべての新しいユーザーを取得します

間違っています。「リポジトリに追加されました」と「新規」を統合しています。

「リポジトリに追加」とは、まさにそれが言うことを意味します。ユーザーをさまざまなリポジトリに追加、削除、再追加できます。

「新規」とは、ビジネスルールで定義されたユーザーの状態です。

現在、ビジネスルールは「新しい==リポジトリに追加されたばかり」かもしれませんが、それはそのルールを知って適用するのが別の責任ではないという意味ではありません。

このようなデータベース中心の考え方を避けるように注意する必要があります。リポジトリに新規ユーザー以外を追加するエッジケースプロセスがあり、それらにメールを送信すると、すべてのビジネスは「もちろんそれらは「新規」ユーザーではありません!実際のルールはXです」と言うでしょう。

この答えは私見ではまったくポイントがありません:リポジトリは、新しいユーザーがいつ追加されたかを知るコードの中心的な場所です

間違っています。上記の理由から、イベントを発生させるのではなく、実際にクラスにメール送信コードを含める場合を除き、中心的な場所ではありません。

リポジトリクラスを使用するアプリケーションがありますが、電子メールを送信するコードはありません。これらのアプリケーションにユーザーを追加すると、メールは送信されません。


11
リポジトリは、追加したユーザーが新しいユーザーであることを認識しません -はい、そうです、というメソッドがありAddます。そのセマンティクスは、追加されたすべてのユーザーが新しいユーザーであることを意味します。Add呼び出す前に渡されるすべての引数を結合しSaveます-そして、すべての新しいユーザーを取得します。
アンドレ・ボルヘス

私はこの提案が好きです。ただし、プラグマティズムは純度よりも優先されます。状況によっては、ユーザーが追加されたときに文字通り1つの電子メールを送信するだけでよい場合、まったく新しいアーキテクチャレイヤーを既存のアプリケーションに追加することを正当化するのは困難です。
アレクサンダー

しかし、イベントはユーザーが追加されたことを言っていません。ユーザーが作成したと表示されます。適切に名前を付けることを検討し、addとcreateのセマンティックの違いに同意する場合、スニペットのイベントは間違った名前であるか、場所を間違えています。レビュアーがリポジトリに通知することに反対するものはないと思います。おそらく、イベントの種類とその副作用が心配でした。
ライヴ

7
@Andreレポは初めてですが、ビジネスの意味で必ずしも「新しい」とは限りません。これらの2つのアイデアの融合が、一見して余分な責任を隠しています。私は、「新しいユーザー」は「DBに追加されました」を超えているものについて、ビジネスルールがあるでしょう私の新しいリポジトリに古いユーザーのトンを輸入、またはなどを削除し、ユーザーを再度追加することができます
ユアン・

1
モデレーター注:あなたの答えはジャーナリストのインタビューではありません。編集がある場合は、「ニュース速報」効果全体を作成せずに、自然に回答に組み込みます。私たちはディスカッションフォーラムではありません。
ロバートハーヴェイ

7

これは有効なポイントですか?

はい、そうです。ただし、コードの構造に大きく依存します。私は完全なコンテキストを持っていないので、一般的な話をしようとします。

ここでイベントを発生させることは、ロギングと本質的に同じことであるように思われます:関数にいくつかのサイド機能を追加するだけです。

それは絶対にそうではありません。ロギングはビジネスフローの一部ではなく、無効にすることができます。何らかの理由でログに記録できない場合でも、(ビジネス)副作用を引き起こしたり、アプリケーションの状態や状態に影響を与えたりすることはできません。もう何でも。次に、追加したロジックと比較します。

また、SRPは、関数でのログ記録またはイベントの使用を禁止していません。そのようなロジックは他のクラスにカプセル化する必要があると述べているだけで、リポジトリがこれらの他のクラスを呼び出すことは問題ありません。

SRPはISP(SOLIDのSおよびI)と連携して動作します。最終的に、非常に具体的なことを行うだけの多くのクラスとメソッドになります。それらは非常に集中しており、更新や置換が非常に簡単で、一般的にテストが簡単です。もちろん実際には、オーケストレーションを処理するいくつかのより大きなクラスもあります。それらは多くの依存関係を持ち、アトミック化されたアクションではなく、複数のステップを必要とするビジネスアクションに焦点を合わせます。ビジネスコンテキストが明確である限り、それらは単一の責任と呼ぶこともできますが、あなたが正しく言ったように、コードが大きくなるにつれて、その一部を新しいクラス/インターフェースに抽象化することができます。

次に、特定の例に戻ります。あなたは絶対にユーザーが多分他の多くの専門のアクションを実行して作成されるたびに通知を送信しなければならない場合は、この要件をカプセル化し、別のサービス、のようなもの作成することができますUserCreationService1つのメソッド公開し、Add(user)、ハンドルストレージの両方(の呼び出しを単一のビジネスアクションとしての通知)。または、元のスニペットで次のようにします_userRepository.SaveChanges();


2
ロギングはビジネスフローの一部ではありません -これはSRPのコンテキストでどのように関連していますか?イベントの目的が新しいユーザーデータをGoogleアナリティクスに送信することである場合、それを無効にするとロギングを無効にするのと同じビジネス効果があります。重要ではありませんが、かなり動揺します。関数に新しいロジックを追加/追加しないための経験則は何ですか?「無効にすると、大きなビジネス上の副作用が発生しますか?」
アンドレ・ボルヘス

2
If the purpose of my event would be to send new user data to Google Analytics - then disabling it would have the same business effect as disabling logging: not critical, but pretty upsetting 。偽の「ニュース」を引き起こす時期尚早のイベントを発生させている場合はどうなりますか。DBトランザクションのエラーのために最終的に作成されなかった「ユーザー」を分析で考慮している場合はどうなりますか?会社が不正確なデータに裏付けされた虚偽の前提で意思決定を行っている場合はどうなりますか?問題の技術的な側面に集中しすぎています。「時々 、あなたは木の木を見ることができない」を
LAIV

@Laiv、あなたは有効なポイントを挙げていますが、これは私の質問やこの答えのポイントではありません。問題は、これがSRPのコンテキストで有効なソリューションであるかどうかです。したがって、DBトランザクションエラーがないと仮定しましょう。
アンドレ・ボルヘス

あなたは基本的にあなたが聞きたいことを話すように私に頼んでいます。私はあなたに範囲を与えます。SRPは適切なコンテキストなしでは役に立たないため、SRPが重要かどうかを判断するためのより広い範囲。IMOは、あなたが技術的なソリューションにのみ焦点を合わせているため、懸念へのアプローチ方法が間違っています。コンテキスト全体に十分な関連性を与える必要があります。そして、はい、DBは失敗するかもしれません。起こる可能性がありますが、それを省略するべきではありません。ご存じのように、SRPや他の優れた慣行に対する疑念に関して、事態が発生し、これらのことが心を変える可能性があるからです。
ライヴ

1
とはいえ、原則は石で書かれたルールではないことを忘れないでください。それらは透過性(適応性)です。ご覧のとおり、それらは解釈に対してオープンです。あなたのレビュアーには解釈があり、あなたには別の解釈があります。あなたが見ているものを見て、彼/彼女の疑念や懸念を解決するか、彼/彼女にあなたのものを解決させてください。ここで「正しい」答えは見つかりません。正しい答えはあなたとあなたのレビュアーが見つけるべきであり、最初にプロジェクトの要件(機能的および非機能的)を尋ねます。
LAIV

4

SRPは、理論的には、ボブおじさんの記事「単一責任の原則」で説明されているように、人々に関するものです。あなたのコメントでそれを提供してくれたロバート・ハーベイに感謝します。

正しい質問は次のとおりです。

どの「利害関係者」が「メールの送信」要件を追加しましたか?

その利害関係者がデータの永続性も担当している場合(可能性は低いが可能性がある場合)、これはSRPに違反しません。それ以外の場合は行います。


2
興味深い-このSRPの解釈について聞いたことがない。この解釈に関する詳細情報/文献へのポインタはありますか?
sleske

2
@sleske:アンクルボブ本人から「そして、このシングル責任原則の核心になる。この原則は、人々についてです。 あなたがソフトウェアモジュールを書くとき、あなたは変更が要求されたとき、それらの変更のみ発信できることを確認します単一の人から、というか、厳密に定義された単一のビジネス機能を表す単一の密結合グループから。」
ロバートハーヴェイ

ロバートに感謝します。IMO、「Single Responsibility Principle」という名前は単純に聞こえますがひどいですが、「responsibility」の意図する意味をフォローアップする人が少なすぎます。OOPが元の概念の多くから変化した方法のようなもので、今ではかなり無意味な用語です。
user949300

1
うん。それがRESTという用語に起こったことです。Roy Fieldingでさえ、人々は間違った使い方をしていると言います。
ロバートハーヴェイ

引用は関連していますが、この回答は、「電子メールの送信」要件がSRP違反の質問に関する直接的な要件ではないことを逃していると思います。ただし、「どの「利害関係者」が「イベントを発生させる」要件を追加した」と言うことで、この回答は実際の質問により関連するようになります。これをより明確にするために、自分の回答を少し修正しました。
ドックブラウン

2

技術的にはイベントを通知するリポジトリに問題はありませんが、利便性が懸念される機能的な観点から見ることをお勧めします。

ユーザーの作成、新しいユーザーとその永続性の決定は3つの異なることです。

私の前提

リポジトリがビジネスイベントを通知する適切な場所であるかどうかを決定する前に、以前の前提を考慮してください(SRPに関係なく)。私はビジネスイベントを言ったことに注意してください。なぜなら、私にUserCreated1UserStoredまたはUserAdded 1とは異なる意味合いがあるからです。また、これらのイベントはそれぞれ異なる聴衆に向けられていると考えます。

一方では、ユーザーの作成は、永続性を伴う場合と伴わない場合があるビジネス固有のルールです。より多くのビジネスオペレーション、より多くのデータベース/ネットワークオペレーションが含まれる場合があります。永続化レイヤーが認識しない操作。永続層には、ユースケースが正常に終了したかどうかを判断するための十分なコンテキストがありません。

反対に_dataContext.SaveChanges();、ユーザーが正常に保持されているとは限りません。データベースのトランザクションスパンに依存します。たとえば、MongoDBなどのデータベースはトランザクションがアトミックである場合に当てはまりますが、ACIDトランザクションを実装する従来のRDBMSには当てはまりません。

これは有効なポイントですか?

かもしれない。ただし、SRPの問題(技術的に言えば)だけでなく、利便性の問題(機能的に言​​えば)であるとも言えません。

  • 進行中のビジネスオペレーションを認識しないコンポーネントからビジネスイベントを発生させることは便利ですか?
  • 彼らはそれを行う適切な瞬間と同じくらい適切な場所を表していますか?
  • これらのコンポーネントがこのような通知を通じてビジネスロジックを調整できるようにする必要がありますか?
  • 時期尚早のイベントによって引き起こされる副作用を無効にできますか? 2

ここでイベントを発生させることは、ロギングと本質的に同じことのように思えます

絶対違う。ただし、イベントUserCreatedによって他のビジネスオペレーションが発生する可能性が高いことが示唆されているため、ロギングには副作用はありません。通知のような。3

そのようなロジックは他のクラスにカプセル化されるべきであると言うだけで、リポジトリがこれらの他のクラスを呼び出すことは問題ありません

必ずしもそうではありません。SRPはクラス固有の問題ではありません。レイヤー、ライブラリ、システムなど、さまざまな抽象化レベルで動作します!それは、同じ利害関係者の手によって同じ理由で何が変化するかをまとめることです。ユーザーの作成(ユースケース)が変更された場合、その瞬間である可能性が高く、イベントが発生する理由も変更されます。


1:適切に名前を付けることも重要です。

2:UserCreated_dataContext.SaveChanges();に送信したが、接続の問題または制約違反が原因でデータベーストランザクション全体が後で失敗したとします。イベントの早すぎるブロードキャストには注意してください。その副作用は元に戻すのが難しい場合があるためです(可能な場合でも)。

3:通知プロセスが適切に処理されていないと、元に戻せない/ sup>できない通知を起動する可能性があります>


1
+1トランザクションスパンに関する非常に良い点。ロールバックが発生する可能性があるため、ユーザーが作成されたことをアサートするのは時期尚早です。ログとは異なり、アプリの他の部分がイベントで何かをする可能性があります。
アンドレスF.

2
まさに。イベントは確実性を示します。何かが起こったが、それは終わった。
Laiv

1
@Laiv:そうでない場合を除きます。Microsoftには、接頭辞が付いた、BeforeまたはPreview確実性についてまったく保証しないあらゆる種類のイベントがあります。
ロバートハーヴェイ

1
@ jpmc26:代替手段がなければ、提案は役に立ちません。
ロバートハーヴェイ

1
@ jpmc26:したがって、あなたの答えは、「まったく異なるツールセットとパフォーマンス特性を持つ、まったく異なる開発エコシステムへの変更」です。反対に呼んでください。しかし、これは大部分の開発努力にとって実行不可能だと思います。
ロバートハーヴェイ

1

いいえ、これはSRPに違反しません。

多くの人は、単一責任原則は、機能が「1つのこと」のみを実行し、「1つのこと」を構成するものについての議論に巻き込まれるべきだと考えているようです。

しかし、それは原則が意味するものではありません。それはビジネスレベルの懸念事項です。クラスは、ビジネスレベルで独立して変更される可能性のある複数の懸念事項や要件を実装しないでください。クラスがユーザーを保存し、ハードコードされたウェルカムメッセージを電子メールで送信するとします。複数の独立した懸念により、そのようなクラスの要件が変更される可能性があります。デザイナーは、変更するためにメールのhtml / stylesheetを要求できます。通信の専門家は、メールの文言を変更する必要がある場合があります。また、UXの専門家は、メールを実際にオンボーディングフローの別の時点で送信する必要があると判断できます。そのため、このクラスは、独立したソースからの複数の要件変更の影響を受けます。これはSRPに違反します。

ただし、イベントはユーザーの保存のみに依存し、他の懸念事項には依存しないため、イベントの発生はSRPに違反しません。リポジトリがメールに影響されることなく、またはメールについて知ることなく、保存によってトリガーされる電子メールを持つことができるので、イベントは実際にはSRPを維持するための本当に素晴らしい方法です。


1

単一責任の原則を心配しないでください。特定の概念を「責任」として主観的に選択できるため、ここで適切な決定を下すのには役立ちません。クラスの責任はデータベースへのデータの永続性を管理することであると言うことも、ユーザーの作成に関連するすべての作業を実行することであると言うこともできます。これらはアプリケーションの動作の異なるレベルに過ぎず、どちらも「単一責任」の有効な概念的表現です。したがって、この原則はあなたの問題を解決するのに役立ちません。

この場合に適用する最も有用な原則は、最も驚きのない原則です。それでは、データをデータベースに永続化するという主要な役割を持つリポジトリが電子メールも送信するのは驚くべきことでしょうか?

はい、それは非常に驚くべきことです。これらは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);
}

これにより、プログラマはすべてのリポジトリの変更を保存するタイミングを明示的に制御でき、コミット前に発生する必要のあるイベントのシーケンスをコードに明示的に文書化し、エラー時にロールバックが発行されることを保証し(ロールバックを発行すると仮定DataContext.Dispose)、非表示を回避しますステートフルクラス間の接続。

また、リクエストで電子メールを直接送信しないことを希望します。キューに通知の必要性を記録する方がより堅牢です。これにより、障害処理が改善されます。特に、電子メールの送信中にエラーが発生した場合、ユーザーの保存を中断することなく後で再試行でき、ユーザーが作成されたがサイトからエラーが返されるケースを回避できます。

using (DataContext context = new DataContext())
{
    _userRepository.Add(context, user);
    _emailNotificationQueue.AddUserCreateNotification(user);
    _emailNotificationQueue.Commit();
    context.SaveChanges();
}

キューのコンシューマーは、context.SaveChanges()呼び出しが失敗した場合に、電子メールを送信する前にユーザーが存在することを確認できるため、最初に通知キューをコミットすることをお勧めします。(そうでない場合は、ヘイゼンバグを回避するために本格的な2フェーズコミット戦略が必要になります。)


一番下の行は実用的です。実際に、特定の方法でコードを書くことの結果(リスクと利益の両方の面で)を考えてください。「単一責任原則」はそれを行うのにあまり役に立たないことがわかりますが、「最小の驚きの原則」は私が別の開発者の頭に(いわば)入り込んで何が起こるかを考えるのに役立ちます。


4
データベースにデータを永続化するという主要な役割を持つリポジトリが電子メールも送信するのは驚くべきことです -私の質問のポイントを逃したと思います。リポジトリがメールを送信していません。イベントを発生させるだけで、このイベントの処理方法は外部コードの責任です。
アンドレ・ボルヘス

4
基本的に「イベントを使用しないでください」という議論をしています。
ロバートハーヴェイ

3
[shrug]イベントは、ほとんどのUIフレームワークの中心です。イベントを排除すると、これらのフレームワークはまったく機能しません。
ロバートハーヴェイ

2
@ jpmc26:ASP.NET Webformsと呼ばれます。それは吸う。
ロバートハーヴェイ

2
My repository is not sending emails. It just raises an event因果関係。リポジトリが通知プロセスをトリガーしています。
ライヴ

0

現在はSaveChangesありません2つの物事を:それは、変更を保存し、それがそうすることを記録します。次に、別のことを追加します。メール通知を送信します。

イベントを追加するという賢明なアイデアがありましたが、これは既に違反されていることに気付かずに、単一責任原則(SRP)に違反しているとして批判されました。

純粋SRPソリューション、取得するには最初のトリガイベントを、その後、保存、ログ記録、そして最終的に送信する電子メール:すべての今3があるうちそのイベントのフックを呼び出します。

最初にイベントをトリガーするか、に追加する必要がありますSaveChanges。あなたのソリューションは、2つのハイブリッドです。既存の違反には対応していませんが、3つ以上の違反を防ぐことはできます。SRPに準拠するように既存のコードをリファクタリングするには、厳密に必要な作業よりも多くの作業が必要になる場合があります。彼らがSRPをどの程度受けたいかはあなたのプロジェクト次第です。


-1

コードはすでにSRPに違反しています。同じクラスがデータコンテキストとの通信とロギングを担当していました。

3つの責任を持つようにアップグレードするだけです。

物事を1つの責任に戻す1つの方法は、_userRepository; を抽象化することです。コマンドブロードキャスターにします。

一連のコマンドと一連のリスナーがあります。コマンドを取得し、それらをリスナーにブロードキャストします。おそらくそれらのリスナーは注文されており、おそらくコマンドが失敗したと言うことさえできます(それは、すでに通知されたリスナーにブロードキャストされます)。

現在、ほとんどのコマンドには1つのリスナー(データコンテキスト)しかありません。変更前のSaveChangesには、データコンテキスト、ロガーの2つがあります。

次に、変更により別のリスナーが追加され、変更が保存されます。これは、イベントサービスで新しいユーザー作成イベントを発生させることです。

これにはいくつかの利点があります。これで、残りのコードを気にすることなく、ロギングコードを削除、アップグレード、または複製できます。変更を保存するときにトリガーを追加して、必要なものを増やすことができます。

これらのすべては、_userRepositoryが作成されて接続されたときに決定されます(または、多分、それらの追加機能はその場で追加/削除されます;アプリケーションの実行中にロギングを追加/拡張できるため)。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.