web-dev-qa-db-ja.com

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

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

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

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

37
Sipo

他のルールと同様に、ここで重要なことは、ルールの目的、精神を検討することであり、ルールがいくつかの教科書でどのように表現されているかを正確に分析し、それをこのケースに適用する方法に取り憑かれることはありません。弁護士のようにこれに取り組む必要はありません。ルールの目的は、私たちがより良いプログラムを書くのを助けることです。プログラムを書く目的がルールを守ることとは違う。

単一責任ルールの目的は、各機能に1つの自己完結型の一貫した処理を行わせることにより、プログラムを理解し、維持しやすくすることです。

たとえば、「checkOrderStatus」のような関数を書いて、注文が保留中であるか、出荷されているか、バックオーダーされているかを判断し、それを示すコードを返しました。次に、別のプログラマーがやって来て、この関数を変更して、注文の出荷時に手持ちの数量も更新しました。これは単一責任の原則に厳しく違反しました。後でそのコードを読む別のプログラマーは、関数名を確認し、戻り値がどのように使用されたかを確認し、データベースが更新されたことを疑うことはありません。手持ちの数量を更新せずに注文状況を取得する必要がある人は、厄介な立場になります。注文状況部分を複製する新しい関数を作成する必要がありますか?フラグを追加して、db更新を行うかどうかを指示しますか?など(もちろん、正しい答えは関数を2つに分割することですが、それは多くの理由で実用的でない場合があります。)

一方で、「2つのこと」を構成する要素を簡単には取り上げません。最近、私たちのシステムからクライアントのシステムに顧客情報を送信する関数を書きました。その関数は、データの再フォーマットを行って、要件を満たします。たとえば、データベースでnullになる可能性のあるフィールドがいくつかありますが、それらはnullを許可しないため、「指定されていない」ダミーテキストを入力するか、正確な単語を忘れてしまいます。おそらく、この関数は2つのことをしています:データを再フォーマットし、それを送信します。ただし、これを「再フォーマット」および「送信」するのではなく、1つの関数に慎重に入れました。再フォーマットせずに送信したくないからです。私は誰かが新しい通話を書いて、彼が再フォーマットを呼び出してから送信しなければならないことに気付かないようにしたくありません。

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

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

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

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

16
Jay

SRPの概念は、モジュールを個別に実行するときに2つの異なることを行うことを停止することです。これにより、メンテナンスが改善され、時間内のスパゲッティが少なくなります。 SRPが言うように「変更する1つの理由」。

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

SRPは、実際にはすべての操作を1つの呼び出しに削減しようとするのではなく、何を操作していて、どのように操作するかを減らすことを目的としています。したがって、更新されたオブジェクトを返す単一の更新で問題ありません。

67
gbjbaanb

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

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

現在、一部の人はこれまでの原則を拡張して、成功フラグを返すことは、成功が報告されているアクションを実行することとは別の責任であると考えています。非常に厳密な解釈の下ではこれは真実ですが、代替手段は成功ステータスを取得するために2番目のメソッドを呼び出さなければならないため、呼び出し元は複雑になるため、多くのプログラマーは、副作用を持つメソッドから成功コードを返すことで問題ありません。

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

46
Kilian Foth

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

必ずしも。どちらかといえば、これは command-query 分離の原則に違反しています。

責任は

従業員データを更新する

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

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


それでは、コマンドとクエリの分離についてはどうですか?

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

(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命令から高レベルの同時コレクションまで、頻繁に発生します。

8
Paul Draper

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

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

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

同様に、従業員クラスにCalculateSalaryメソッドがあってはなりません。給与のプロパティは大丈夫ですが、税などの計算はどこかで行われるべきです。

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

2
Bent

に関して。特定のケース:

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

それに見えるUpdateメソッドは、既存のEmployeeを最初のパラメーターとして受け取り、既存の従業員とこの従業員で変更するパラメータのセット。

私はdoこれを考えればきれいにできます。私は2つのケースを見ます:

(a)Employeeには、実際にはデータベース/一意のIDが含まれています。これにより、データベースで常に識別できます。 (つまり、DBでそれを見つけるには、レコード値全体を設定する必要がありますしない

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

これのバリエーションとして、レコード(IDによる)が存在するかどうかに応じて、挿入または更新するvoid Put(Employee obj)メソッドのみを使用するというバリエーションがあります。

(b) DBルックアップには既存の完全なレコードが必要ですが、その場合はvoid Update(Employee existing, Employee newData)を使用する方が理にかなっています。

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

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

2
Martin Ba

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

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

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

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

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

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

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

1
donquixote

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

0
oopexpert