更新メソッドに戻り型を追加すると、「単一責任の原則」に違反しますか?


37

データベース内の従業員データを更新する方法があります。Employee「更新」オブジェクト手段は、実際に新しいオブジェクトをインスタンス化するためのクラスは、不変です。

UpdateメソッドにEmployee更新されたデータを含む新しいインスタンスを返すようにしたいのですが、メソッドの責任は従業員データを更新し、データベースから新しいオブジェクト取得Employeeすることであると言えますが、単一責任原則に違反しますか?

DBレコード自体が更新されます。次に、このレコードを表す新しいオブジェクトがインスタンス化されます。


5
ここで小さな擬似コードが大いに役立つ可能性があります。実際に新しいDBレコードが作成されますか、またはDBレコード自体が更新されますが、「クライアント」コードではクラスが不変としてモデル化されるため新しいオブジェクトが作成されますか?
マーティンBa

Martin Braの質問に加えて、データベースを更新するときにEmployeeインスタンスを返すのはなぜですか?Employeeインスタンスの値はUpdateメソッドで変更されていないため、それを返す理由(呼び出し元は既にインスタンスにアクセスしています...)。または、更新メソッドはデータベースから(潜在的に異なる)値も取得しますか?
トールサル

1
@Thorsal:データエンティティが不変である場合、更新されたデータでインスタンスを返すことはほとんどSOPです。
ミコワク

21
Compare-and-swapそしてtest-and-set、マルチスレッドプログラミングの理論の基本的な操作です。どちらも戻り値型の更新メソッドであり、それ以外の場合は機能しません。これは、コマンドとクエリの分離や単一責任の原則などの概念を壊しますか?はい、それがポイントです。SRPは普遍的に良いことではなく、実際に積極的に有害になる可能性があります。
–MSalters

3
@MSalters:そのとおりです。コマンド/クエリ分離では、べき等として認識できるクエリを発行でき、応答を待たずにコマンドを発行できるはずですが、アトミックな読み取り-変更-書き込みは操作の3番目のカテゴリとして認識される必要があります。
-supercat

回答:


16

他のルールと同様に、ここで重要なことは、ルールの目的、精神を考慮し、教科書でルールがどのように表現されているか、このケースにそれを適用する方法を正確に分析することに夢中にならないことだと思います。弁護士のようにこれにアプローチする必要はありません。ルールの目的は、より良いプログラムを書くのを助けることです。プログラムを書く目的がルールを守ることではないようです。

単一責任ルールの目的は、各機能に1つの自己完結した一貫性のあることを実行させることにより、プログラムの理解と保守を容易にすることです。

たとえば、私はかつて「checkOrderStatus」のようなものを呼び出す関数を作成しました。この関数は、注文が保留中、発送済み、バックオーダー済み、何であるかを判断し、どれを示すコードを返しました。その後、別のプログラマーがやって来て、この関数を変更して、注文品の出荷時に手持ちの数量も更新しました。これは、単一の責任原則に大きく違反しました。後でそのコードを読んでいる別のプログラマーは、関数名を確認し、戻り値がどのように使用されたかを確認し、データベースの更新を行ったことを疑うことはないでしょう。手持ちの数量を更新せずに注文ステータスを取得する必要がある人は、厄介な立場にあります。注文ステータス部分を複製する新しい関数を作成する必要がありますか?db更新を行うかどうかを知らせるフラグを追加しますか?等。

一方で、「2つのこと」を構成するものを軽視しません。私は最近、顧客情報をシステムからクライアントのシステムに送信する関数を作成しました。この関数は、要件を満たすためにデータを再フォーマットします。たとえば、データベースにはnullのフィールドがありますが、nullを許可しないため、「指定なし」のダミーテキストを入力するか、正確な単語を忘れてしまいます。おそらく、この関数はデータを再フォーマットして送信するという2つのことを実行しています。しかし、「再フォーマット」と「送信」ではなく、非常に意図的にこれを単一の関数に入れました。誰かに新しい電話をかけてもらい、彼がリフォーマットしてから送信しなければならないことに気づかないようにしたいのです。

あなたの場合、データベースを更新して、書き込まれたレコードのイメージを返すことは、論理的かつ必然的にうまくいく可能性のある2つのことのように思えます。私はあなたのアプリケーションの詳細を知らないので、これが良いアイデアであるかどうかを明確に言うことはできませんが、もっともらしいようです。

レコードのすべてのデータを保持するオブジェクトをメモリ内に作成し、データベース呼び出しを行ってこれを書き込み、オブジェクトを返す場合、これは非常に理にかなっています。あなたの手にオブジェクトがあります。なぜそれを返さないのですか?オブジェクトを返さなかった場合、呼び出し元はどのように取得しますか?彼はあなたが書いたオブジェクトを取得するためにデータベースを読まなければならないでしょうか?それはかなり非効率的です。彼はどのようにレコードを見つけますか?主キーを知っていますか?誰かが、書き込み関数がレコードを再読み取りできるように主キーを返すことが「合法」だと宣言した場合、なぜレコード全体を返さないのですか?違いは何ですか?

一方、オブジェクトの作成がデータベースレコードの書き込みとはまったく異なる作業であり、呼び出し側がオブジェクトを作成せずに書き込みを実行したい場合、これは無駄になります。呼び出し元がオブジェクトを必要としているが書き込みを行わない場合、オブジェクトを取得する別の方法を提供する必要があります。これは、冗長なコードを記述することを意味します。

しかし、シナリオ1の方が可能性が高いと思うので、おそらく問題はないと思います。


すべての答えに感謝しますが、これは本当に私を本当に助けてくれました。
シポ

再フォーマットせずに送信したくなく、後でデータを送信する以外に再フォーマットする必要がない場合、それらは2つではなく1つのものです。
Joker_vD 16

public function sendDataInClientFormat() { formatDataForClient(); sendDataToClient(); } private function formatDataForClient() {...} private function sendDataToClient() {...}
CJデニス

@CJDennis確かに。実際、それが私がやった方法です。フォーマットを実行する関数、実際の送信を実行する関数、そして、ここでは取り上げない他の2つの関数がありました。次に、すべてを適切な順序で呼び出す1つのトップレベル関数。「フォーマットして送信」は1つの論理操作であり、したがって1つの機能に適切に組み合わせることができます。それが2つだと主張するなら、大丈夫、それでもやるべき合理的なことはそれをすべて行う1つのトップレベル関数を持つことです。
ジェイ

67

SRPのコンセプトは、モジュールを個別に実行することで2つの異なる処理を実行することを停止し、メンテナンスを改善し、時間内のスパゲッティを削減することです。SRPが「変更する1つの理由」と言っているように。

あなたの場合、「更新されたオブジェクトを更新して返す」ルーチンがあれば、オブジェクトを一度変更しているだけで、変更する理由が1つあります。オブジェクトをすぐに戻すことはここでもそこでもありません。あなたはまだその単一のオブジェクトを操作しています。コードには、1つだけの責任があります。

SRPは、すべての操作を単一の呼び出しに減らすことではなく、操作対象と操作方法を減らすことを目的としています。したがって、更新されたオブジェクトを返す単一の更新で問題ありません。


7
または、「すべての操作を1回の呼び出しに減らすことについて」ではなく、問題のアイテムを使用するときに考えなければならないさまざまなケースを減らすことです。
jpmc26

46

いつものように、これは学位の問題です。SRPは、外部データベースからレコードを取得し、それに対して高速フーリエ変換を実行し、その結果でグローバル統計レジストリを更新するメソッドの作成を停止する必要があります。ほとんどすべての人が、これらのことを異なる方法で行うことに同意すると思います。各方法に単一の責任を課すことは、その点を明確にする最も経済的で記憶に残る方法です。

スペクトルのもう一方の端には、オブジェクトの状態に関する情報を生成するメソッドがあります。典型的な人isActiveは、この情報を単一の責任として提供しています。おそらく誰もがこれが大丈夫であることに同意します。

現在、一部の人々は、成功フラグを返すことと、成功が報告されているアクションを実行することとは異なる責任を考慮するように、原則を拡張しています。非常に厳密な解釈の下では、これは事実ですが、代替手段は成功ステータスを取得するために2番目のメソッドを呼び出さなければならないため、呼び出し側を複雑にします。

新しいオブジェクトを返すことは、複数の責任への道のさらに一歩です。オブジェクト全体に対して2回目の呼び出しを行うことを呼び出し側に要求することは、最初の呼び出しが成功したかどうかを確認するためだけに2番目の呼び出しを要求するよりも少し合理的です。それでも、多くのプログラマは、更新の結果を完全に返すことを検討します。これは、わずかに異なる2つの責任として解釈できますが、それは確かに、そもそも原則を刺激したひどい虐待の1つではありません。


15
2番目のメソッドを呼び出して最初のメソッドが成功したかどうかを確認する必要がある場合、2番目のメソッドの結果を取得するために3番目のメソッドを呼び出す必要はありませんか?その後、実際に結果が必要な場合は

3
私は、SRPを過度に熱心に適用すると、無数の小さなクラスにつながり、それ自体が負担になると付け加えます。どのように大きな負担がなど、ご使用の環境、コンパイラ、IDE /ヘルパーツールに依存
エリックAlapää

8

それは単一責任原則に違反しますか?

必ずしも。どちらかといえば、これはコマンドとクエリの分離の原則に違反します

責任は

従業員データを更新する

この責任には操作のステータスが含まれることを暗黙的に理解します。たとえば、操作が失敗した場合、例外がスローされ、成功した場合、更新従業員が返されます。

繰り返しますが、これはすべて程度と主観的な判断の問題です。


では、コマンドとクエリの分離はどうでしょうか?

まあ、その原則は存在しますが、実際には更新の結果を返すことは非常に一般的です。

(1)Java Set#add(E)は要素を追加し、以前のセット内の包含を返します。

if (visited.add(nodeToVisit)) {
    // perform operation once
}

これは、2つのルックアップを実行する必要のあるCQSの選択肢よりも効率的です。

if (!visited.contains(nodeToVisit)) {
    visited.add(nodeToVisit);
    // perform operation once
}

(2)Compare-and-swapfetch-and-add、およびtest-and-setは、並行プログラミングを可能にする一般的なプリミティブです。これらのパターンは、低レベルのCPU命令から高レベルの同時収集まで頻繁に現れます。


2

単一の責任は、クラスが複数の理由で変更する必要がないことです。

例として、従業員には電話番号のリストがあります。電話番号の処理方法が変更された場合(国の呼び出しコードを追加する場合があります)、従業員クラスはまったく変更されません。

従業員クラスは、それがどのようにデータベースに保存されるかを知る必要がありません。それは、従業員の変化やデータの格納方法の変化に応じて変化するためです。

同様に、従業員クラスにCalculateSalaryメソッドがあってはなりません。給与のプロパティは大丈夫ですが、税などの計算は別の場所で行う必要があります。

しかし、Updateメソッドが更新した内容を返すことは問題ありません。


2

Wrt。特定のケース:

Employee Update(Employee, name, password, etc) (私は多くのパラメータを持っているので、実際にはビルダーを使用しています)。

そうですUpdate方法は、既存のかかるEmployee(?)既存の従業員とその従業員の変化にパラメータのセットを識別するための最初のパラメータとして。

私はない、これは考えることができクリーナーを行うこと。次の2つのケースがあります。

(a) Employee実際には、データベースで常に識別できるデータベース/一意のIDが含まれています。(つまり、DB内で検索するためにレコード値セット全体を設定する必要はありませ

この場合、void Update(Employee obj)IDで既存のレコードを検索し、渡されたオブジェクトのフィールドを更新するメソッドを好むでしょう。または多分void Update(ID, EmployeeBuilder values)

私が便利だと思ったこのバリエーションはvoid Put(Employee obj)、(IDによる)レコードが存在するかどうかに応じて、挿入または更新するメソッドのみを持つことです。

(b) DBルックアップには既存の完全なレコードが必要です。その場合、次のようにするとさらに意味がありますvoid Update(Employee existing, Employee newData)

ここでわかる限りでは、新しいオブジェクト(またはサブオブジェクトの値)を作成してそれを保存し、実際に保存する責任は直交的であると言うので、それらを分離します。

他の回答(アトミックセットアンドリトリーブ/比較スワップなど)で言及されている並行要件は、これまで取り組んできたDBコードの問題ではありませんでした。DBと通信するときは、個々のステートメントレベルではなく、トランザクションレベルで通常に処理する必要があると思います。(つまり、「アトミック」ではEmployee[existing data] Update(ID, Employee newData)意味をなさないデザインはないかもしれないと言っているわけではありませんが、DBアクセスでは通常見られないものです。)


1

これまでのところ、ここの全員がクラスについて話している。しかし、インターフェイスの観点から考えてみてください。

インターフェイスメソッドが戻り値の型、および/または戻り値に関する暗黙の約束を宣言する場合、すべての実装は更新されたオブジェクトを返す必要があります。

だからあなたは尋ねることができます:

  • 新しいオブジェクトを返すことを気にしたくない実装の可能性を考えることができますか?
  • 更新された従業員を戻り値として必要としない(インスタンスを挿入することにより)インターフェイスに依存するコンポーネントを考えることができますか?

単体テスト用のモックオブジェクトについても考えてください。明らかに、戻り値なしでモックする方が簡単です。そして、注入された依存関係のインターフェイスが単純であれば、依存コンポーネントのテストが簡単になります。

これらの考慮事項に基づいて、決定を下すことができます。

後で後悔する場合でも、2つの間にアダプターを備えた2番目のインターフェースを導入できます。もちろん、このような追加のインターフェイスは、構成の複雑さを増すため、すべてトレードオフです。


0

あなたのアプローチは問題ありません。不変性は強力な主張です。私が尋ねる唯一のことは、あなたがオブジェクトを構築する他の場所はありますか?オブジェクトが不変ではない場合、「状態」が導入されるため、追加の質問に答える必要があります。また、オブジェクトの状態変化はさまざまな理由で発生する場合があります。次に、あなたのケースを知っておく必要があり、それらは冗長または分割されるべきではありません。

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