ゲッターのロジック


46

私の同僚は、ゲッターとセッターのロジックはできるだけ少なくすべきだと言っています。

しかし、ゲッターとセッターには多くの要素が隠されており、実装の詳細からユーザー/プログラマーを保護できると確信しています。

私がすることの例:

public List<Stuff> getStuff()
{
   if (stuff == null || cacheInvalid())
   {
       stuff = getStuffFromDatabase();
   }
   return stuff;
}

仕事のやり方の例は、物事を行うように私に言っています(彼らはボブおじさんからの 'Clean Code'を引用しています):

public List<Stuff> getStuff()
{
    return stuff;
}

public void loadStuff()
{
    stuff = getStuffFromDatabase();
}

セッター/ゲッターではどのくらいのロジックが適切ですか?データ隠蔽の違反を除いて、空のゲッターとセッターの使用は何ですか?


6
私にはもっとtryGetStuff()のようなこのルックス...
ビル・ミッチェル

16
これは「ゲッター」ではありません。この用語は、誤って名前に「get」を指定したメソッドではなく、プロパティの読み取りアクセサーに使用されます。
ボリスヤンコフ

6
その2番目の例がこのクリーンコードブックの公平な例か、誰かがそれについて間違った目的を持っているかどうかはわかりませんが、壊れやすい混乱ではないことの1つはクリーンコードです。
ジョンハンナ14年

@BorisYankovまあ... 2番目の方法です。public List<Stuff> getStuff() { return stuff; }
R.シュミッツ

正確なユースケースに応じて、キャッシュを別のクラスに分離するのが好きです。作るStuffGetterインタフェースを実装StuffComputer計算を行いいるが、とのオブジェクト内にラップStuffCacherキャッシュにアクセスするかに電話を転送するのいずれかを担当している、StuffComputerそれがラップしていること。
アレクサンダー

回答:


71

仕事が物事を行うように指示する方法は不十分です。

経験則として、私が物事を行う方法は次のとおりです:ものを取得するのが計算上安価な場合(またはほとんどの可能性がキャッシュで見つかる場合)、getStuff()のスタイルは問題ありません。ものを取得することが計算上高価であることがわかっているため、インターフェースでその高価さを宣伝する必要があるため、getStuff()とは呼ばず、calculateStuff()またはそのようなものを呼び出して、やるべきことがいくつかあります。

どちらの場合でも、loadStuff()が事前に呼び出されていない場合getStuff()が爆発するため、基本的に操作の複雑さを導入することでインターフェイスを複雑にするため、作業の方法は不十分です。それに。操作の順序は、私が考えることができる最悪の種類の複雑さに関するものです。


23
作業順序の複雑さについて言及する場合は+1。回避策として、おそらくコンストラクターでloadStuff()を常に呼び出すように求められるかもしれませんが、それは常にロードする必要があることを意味するので、それも悪いでしょう。最初の例では、データは必要な場合にのみ遅延ロードされ、可能な限り良好です。
ローレント

6
私は通常、「本当に安い場合はプロパティゲッターを使用します。高価な場合は関数を使用します」というルールに従います。それは通常私に役立ち、強調するためにあなたが示したように適切に命名することも私にとって良いようです。
デニストローラー

3
失敗する可能性がある場合-ゲッターではありません。この場合、DBリンクがダウンするとどうなりますか?
マーティンベケット

6
+1、間違った答えがいくつ投稿されているかに少しショックを受けています。実装の詳細を隠すためのゲッター/セッターが存在します。そうでない場合は、変数を単に公開する必要があります。
イズカタ

2
loadStuff()関数が関数の前に呼び出されることを要求getStuff()することは、クラスが内部で起こっていることを適切に抽象化していないことも意味することを忘れないでください。
rjzii

23

ゲッターのロジックはまったく問題ありません。

しかし、データベースからデータを取得することは、「ロジック」以上のものです。それは、多くのことがうまくいかない可能性のある一連の非常に高価な操作を、非決定的な方法で伴います。ゲッターで暗黙的にそれを行うことをheします。

一方、ほとんどのORMはコレクションの遅延読み込みをサポートしています。これは基本的にまさにあなたがしていることです。


18

「Clean Code」によれば、可能な限り次のように分割する必要があると思います。

public List<Stuff> getStuff() {
   if (hasStuff()) {
       return stuff;
   }
   loadStuff();
   return stuff;
}

private boolean hasStuff() {
    if (stuff == null) {
       return false;
    }
    if (cacheInvalid()) {
       return false;        
    }
    return true;
} 

private void loadStuff() {
    stuff = getStuffFromDatabase();
}

もちろん、これは完全にナンセンスです。あなたが書いた美しいフォームは、誰でも一目で理解できるほんの一部のコードで正しいことをするからです。

public List<Stuff> getStuff() {
   if (stuff == null || cacheInvalid()) {
       stuff = getStuffFromDatabase();
   }
   return stuff;
}

物がフードの下でどのように取得されるかは、発信者の頭痛であってはならず、特に、任意の「正しい順序」で物事を呼び出すことを覚えておくのは発信者の頭痛であってはなりません。


8
-1。本当の頭痛の種は、単純なgetter呼び出しがなぜ遅いデータベースアクセスにつながったのかを、呼び出し側が突き止めたときです。
ドメニック

14
@Domenic:データベースへのアクセスはとにかく行われなければなりません。そうしないことでパフォーマンスを節約することはできません。これが必要な場合List<Stuff>、取得する方法は1つしかありません。
DeadMG

4
@lukas:ありがとう、私は「きれいな」コードで些細なコードを作成するために使用されたすべてのトリックを覚えていませんでしたが、1行長くなりました;-)今修正されました。
ジョナスプラッカ

2
あなたはロバート・マーティンをしています。彼は、単純なブール論理和を9行関数に決して拡張しませんでした。あなたの関数hasStuffはきれいなコードの反対です。
ケビンクライン14

2
私はこの答えの始まりを読み、「別の本崇拝者がいる」と考えてそれを迂回しようとしました。そして、「もちろん、これは完全にナンセンスです」の部分が目を引きました。よく言った!C-:=
マイクナキス

8

彼らは、ゲッターとセッターのロジックはできる限り少なくすべきだと言っています。

クラスのニーズを満たすために必要なだけのロジックが必要です。私の個人的な好みはできるだけ少ないですが、コードを維持する場合、通常は既存のゲッター/セッターとの元のインターフェイスを残す必要がありますが、新しいビジネスロジック(たとえば、「顧客」 「911以降の環境でのゲッターは、「顧客を知る」およびOFACの規制に適合し、特定の国の顧客の出現を禁止する企業ポリシー(キューバやイランなど)に適合しなければなりません)。

あなたの例では、私はあなたのものを好み、「おじさんボブ」バージョンはユーザー/メンテナーが電話loadStuff()する前に電話することを忘れないようにする必要があるため、「おじさんボブ」サンプルを嫌いますgetStuff()-これはメンテナーの一人が忘れた場合の災害のレシピです(またはさらに悪いことに、知らなかった)。私が過去10年間働いてきた場所のほとんどは、まだ10年以上前のコードを使用しているため、保守の容易さを考慮することが重要です。


6

あなたは正しい、あなたの同僚は間違っている。

getメソッドが何をすべきか、またはすべきでないかについての経験則を忘れてください。クラスは何かの抽象化を提示する必要があります。あなたのクラスは読み取り可能stuffです。Javaでは、「get」メソッドを使用してプロパティを読み取るのが一般的です。をstuff呼び出すことで読み取ることを期待して、数十億行のフレームワークが記述されていgetStuffます。関数fetchStuffまたは以外の名前を付けると、getStuffクラスはこれらすべてのフレームワークと互換性がなくなります。

それらをHibernateにポイントすると、「getStuff()」が非常に複雑な処理を実行でき、失敗するとRuntimeExceptionがスローされます。


HibernateはORMであるため、パッケージ自体が意図を表します。パッケージ自体がORMでない場合、この意図はそれほど容易に理解されません。
FMJaguar

@FMJaguar:それは完全に簡単に理解できます。Hibernateはデータベース操作を抽象化し、オブジェクトのネットワークを提示します。OPは、データベース操作を抽象化して、というプロパティを持つオブジェクトを表示していますstuff。どちらも詳細を非表示にして、呼び出しコードを記述しやすくします。
ケビンクライン14

そのクラスがORMクラスである場合、その意図は他のコンテキストで既に表現されています:質問は残ります:「ゲッターを呼び出すことの副作用を他のプログラマーはどのように知っていますか?」プログラムは1kのクラスと10Kゲッター、それらのいずれかのデータベース呼び出しがトラブル可能性が許可するポリシーが含まれている場合
FMJaguar

4

このような音は、関数名をどのように制御するかによって影響を受ける可能性がある、アプリケーションの議論と純粋主義のどちらかです。適用された観点から、私はかなり見たいです:

List<String> names = clientRoster.getNames();
List<String> emails = clientRoster.getEmails();

とは対照的に:

myObject.load();
List<String> names = clientRoster.getNames();
List<String> emails = clientRoster.getEmails();

またはさらに悪いこと:

myObject.loadNames();
List<String> names = clientRoster.getNames();
myOjbect.loadEmails();
List<String> emails = clientRoster.getEmails();

これは、他のコードをより冗長にし、読みにくくする傾向があります。これは、同様の呼び出しをすべてやり直す必要があるためです。さらに、ローダー関数などを呼び出すと、作業中のオブジェクトの実装の詳細から抽象化されなくなるという点で、OOPを使用するという全体の目的が失われます。clientRosterオブジェクトを持っている場合、getNamesを呼び出す必要がある場合のように、どのように機能するかを気にする必要はありません。クライアントの名前がloadNamesわかるだけgetNamesですList<String>

したがって、問題はセマンティクスとデータを取得する関数の最適な名前に関するものであるように思えます。会社(およびその他)がgetand setプレフィックスに問題がある場合、retrieveNames代わりに次のような関数を呼び出すのはどうですか?何が起こっているかを示していますが、getメソッドに予想されるように操作が瞬間的であることを意味するものではありません。

アクセッサメソッドのロジックに関しては、変数との名目上の相互作用のみが発生するため、一般に瞬間的であると暗示されているため、最小限に抑えてください。ただし、それは一般に単純型、複雑なデータ型にのみ適用されます(つまりList、プロパティに適切にカプセル化するのが難しく、一般的に厳密なミューテーターとアクセサーではなく他のメソッドを使用してそれらとやり取りします)


2

ゲッターを呼び出すと、フィールドを読み取るのと同じ動作を示す必要があります。

  • 値を取得するのは安いはずです
  • セッターで値を設定してからゲッターで読み取る場合、値は同じである必要があります
  • 値を取得しても副作用はないはずです
  • 例外をスローすべきではありません

2
私はこれに完全に同意しません。副作用がないはずであることに同意しますが、フィールドと区別する方法で実装することは完全に問題ないと思います。.Net BCLを見ると、ゲッターを見るときにInvalidOperationExceptionが広く使用されています。また、操作順序に関するMikeNakisの回答を参照してください。
最大

最後の点を除くすべての点に同意します。値を取得するために、設定されていない可能性のある他の値またはリソースに依存する計算または他の操作を実行する必要がある可能性があります。そのような場合、ゲッターが何らかの例外をスローすることを期待します。
TMN

1
@TMN:最良の場合のシナリオでは、ゲッター例外を発生できる操作を実行する必要がないように、クラスを編成する必要があります。例外をスローできる場所を最小化すると、予期しない驚きが少なくなります。
hugomg

8
具体的な例を挙げて、2番目のポイントには同意しませんfoo.setAngle(361); bar = foo.getAngle()bar361である可能性がありますが1、角度が範囲にバインドされている場合も合法である可能性があります。
zzzzBov

1
-1。(1)この例で安価です-遅延読み込み後。(2)現在、この例には「セッター」はありませんが、誰かが後から追加し、設定するだけのstuff場合、ゲッター同じ値返します。(3)例に示されている遅延読み込みでは、「目に見える」副作用は発生しません。(4)議論の余地があり、おそらく有効なポイントです。「遅延読み込み」を導入すると、以前のAPIコントラクト変更される可能性がありますが、決定を下すにはそのコントラクトを確認する必要があります。
Doc Brown

2

独自の値を計算するために他のプロパティとメソッドを呼び出すゲッターも、依存関係を意味します。たとえば、プロパティがそれ自体を計算できる必要があり、そのために別のメンバーを設定する必要がある場合、すべてのメンバーが必ずしも設定されていない初期化コードでプロパティにアクセスすると、誤ったnull参照を心配する必要があります。

これは、「ゲッター内のプロパティバッキングフィールドではない別のメンバーにアクセスしない」ことを意味するものではなく、オブジェクトの必要な状態が何であるかについて何を示唆しているかに注意を払うことを意味しますアクセスするこのプロパティ。

ただし、2つの具体的な例では、どちらかを選択する理由はまったく異なります。ゲッターは最初のアクセスで初期化されます(例:Lazy Initialization)。2番目の例は、Explicit Initializationなど、以前のポイントで初期化されると想定されています。

正確に初期化が行われると、重要な場合とそうでない場合があります。

たとえば、非常に遅くなる可能性があり、ユーザーが最初にアクセスをトリガーしたときに予期せずパフォーマンスが低下する(つまり、ユーザーの右クリック、コンテキストメニューが表示される、ユーザーが遅延を予想するロードステップ中に実行する必要がある)もう一度右クリックしました)。

また、キャッシュされたプロパティ値に影響を与える/汚いものがすべて発生する、実行中の明らかなポイントがある場合があります。依存関係が変更されていないことを確認し、後で例外をスローすることもあります。この状況では、コード実行がより複雑になり、精神的に従うのが難しくなることを避けるために、計算するのに特に費用がかからない場合でも、その時点で値をキャッシュすることも理にかなっています。

そうは言っても、Lazy Initializationは他の多くの状況で非常に意味があります。そのため、プログラミングでよく起こることですが、ルールに要約するのは難しいのですが、具体的なコードに帰着します。


0

@MikeNakisが言ったようにそれをやるだけです...ものを手に入れただけなら大丈夫です...何か他のことをしたら、仕事をする新しい関数を作成し、それを公開します。

プロパティ/関数が名前のとおりのことだけをしている場合、複雑さの余地はあまりありません。凝集力が重要なIMOです


1
これに注意してください、あなたはあなたの内部状態のあまりにも多くを公開することができます。クラスの初期実装がそれらを必要としていたからといって、たくさんの空のloadFoo()or preloadDummyReferences()createDefaultValuesForUninitializedFields()メソッドを使いたくはありません。
TMN

確かに...私はちょうどあなたが多くの問題があってはならないことを何名状態行う場合...しかし、何を言うことはabsolutly真実であることを言っていた...
イワンCrojachKaračić

0

個人的には、コンストラクターのパラメーターを介してStuffの要件を公開し、どのクラスがインスタンス化されている場合でも、それがどこから来たのかを把握する作業を行えるようにします。スタッフがnullの場合、nullを返す必要があります。OPのオリジナルのような巧妙なソリューションは、実装の奥深くにバグを隠す簡単な方法なので、何かが壊れたときに何が間違っているのかがまったくわからないので、それを試みたくないのです。


0

ここには単に「適切」というより重要な問題があり、それらに基づいて決定する必要があります。主に、ここでの大きな決定は、人々がキャッシュをバイパスできるようにするかどうかです。

  1. 最初に、必要なすべてのロード呼び出しとキャッシュ管理がコンストラクター/イニシャライザーで行われるようにコードを再編成する方法があるかどうかを考えます。これが可能な場合は、パート1の複雑なゲッターの安全性を使用して、パート2の簡単なゲッターを不変式にできるクラスを作成できます(win-winシナリオ)。

  2. そのようなクラスを作成できない場合は、トレードオフがあるかどうかを判断し、コンシューマがキャッシュチェックコードをスキップできるようにするかどうかを決定する必要があります。

    1. 消費者がキャッシュチェックをスキップしないことが重要であり、パフォーマンスのペナルティを気にしないことが重要な場合は、ゲッター内でチェックを組み合わせて、消費者が間違ったことをできないようにします。

    2. キャッシュチェックをスキップしてもよい場合、またはゲッターでO(1)パフォーマンスを保証することが非常に重要な場合は、別の呼び出しを使用します。

既にお気付きかもしれませんが、私は「クリーンコード」、「すべてを小さな機能に分割する」という哲学の大ファンではありません。任意の順序で呼び出すことができる多数の直交関数がある場合、それらを分割すると、少ないコストで表現力が高まります。ただし、関数に順序依存性がある(または特定の順序でのみ本当に役立つ)場合、それらを分割しても、ほとんど利益を加えずに、間違ったことができる方法の数が増えるだけです。


-1、コンストラクターは初期化ではなく構築する必要があります。データベースロジックをコンストラクターに配置すると、そのクラスは完全にテストできなくなり、これらのクラスが少数を超えると、アプリケーションの起動時間が膨大になります。そして、それは初心者向けです。
ドメニック

@Domenic:これはセマンティックおよび言語依存の問題です。オブジェクトが使用に適し、完全に構築された後にのみ、適切な不変条件を提供するポイント。
hugomg

0

私の意見では、ゲッターには多くのロジックを含めるべきではありません。それらには副作用があってはならず、例外から例外を受け取ることはありません。もちろん、あなたが何をしているのか知っている場合を除きます。私のゲッターのほとんどにはロジックがなく、フィールドに移動するだけです。しかし、それに対する注目すべき例外は、使用するためにできるだけシンプルにする必要があるパブリックAPIを使用したことです。そのため、別のゲッターが呼び出されなかった場合に失敗するゲッターが1つありました。ソリューション?var throwaway=MyGetter;依存するゲッターのようなコード行。私はそれを誇りに思っていませんが、それを行うためのよりきれいな方法を見ていません


0

これは、遅延読み込みを使用したキャッシュからの読み取りのように見えます。他の人が指摘したように、チェックとロードは他の方法に属する場合があります。すべてのロードが同時に20スレッドにならないように、ロードの同期が必要になる場合があります。

getCachedStuff()実行時間に一貫性がないため、ゲッターの名前を使用することが適切な場合があります。

cacheInvalid()ルーチンの動作方法によっては、nullのチェックが不要な場合があります。stuffデータベースからデータが取り込まれていない限り、キャッシュが有効であるとは思わないでしょう。


0

リストを返すゲッターで見られると思われる主なロジックは、リストが変更不可能であることを確認するロジックです。あなたの例の両方がカプセル化を破る可能性があるためです。

何かのようなもの:

public List<Stuff> getStuff()
{
    return Collections.unmodifiableList(stuff);
}

ゲッターでのキャッシングに関しては、これは大丈夫だと思いますが、キャッシュの構築にかなりの時間がかかった場合、キャッシュロジックを削除したくなるかもしれません。すなわちそれは依存します。


0

正確なユースケースに応じて、キャッシュを別のクラスに分離するのが好きです。作るStuffGetterインタフェースを実装StuffComputer計算を行いいるが、とのオブジェクト内にラップStuffCacherキャッシュにアクセスするかに電話を転送するのいずれかを担当している、StuffComputerそれがラップしていること。

interface StuffGetter {
     public List<Stuff> getStuff();
}

class StuffComputer implements StuffGetter {
     public List<Stuff> getStuff() {
         getStuffFromDatabase()
     }
}

class StuffCacher implements StuffGetter {
     private stuffComputer; // DI this
     private Cache<List<Stuff>> cache = new Cache<>();

     public List<Stuff> getStuff() {
         if cache.hasStuff() {
             return cache.getStuff();
         }

         List<Stuffs> stuffs = stuffComputer.getStuff();
         cache.store(stuffs);
         return stuffs;
     }
}

この設計により、キャッシングの追加、キャッシングの削除、基礎となる派生ロジックの変更(例:DBへのアクセスとモックデータの返却)などを簡単に行うことができます。


-1

私見では、契約による設計を使用する場合は非常に簡単です。ゲッターが提供するものを決定し、それに応じてコーディングするだけです(単純なコードまたはどこかに関与または委任される可能性のある複雑なロジック)。


+1:賛成です!オブジェクトが何らかのデータを保持することのみを目的としている場合、ゲッターはオブジェクトの現在のコンテンツのみを返す必要があります。この場合、データをロードするのは他のオブジェクトの責任です。オブジェクトがデータベースレコードのプロキシであるとコントラクトで指定されている場合、ゲッターはその場でデータをフェッチする必要があります。データがロードされているが最新ではない場合、さらに複雑になる可能性があります。データベースの変更をオブジェクトに通知する必要がありますか?この質問に対するユニークな答えはないと思います。
ジョルジオ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.