OOPのドキュメントでは、「getter」が計算を実行するかどうかの指定を避ける必要がありますか?


39

私の学校のCSプログラムでは、オブジェクト指向プログラミングについては一切言及していません。そのため、私はそれを補足するために独力で読んでいます。具体的には、Bertrand MeyerによるObject Oriented Software Constructionです。

Meyerは、クラスが実装に関する可能な限り多くの情報を隠すべきであると繰り返し指摘していますが、これは理にかなっています。特に、彼は、属性(つまり、クラスの静的で計算されていないプロパティ)とルーチン(関数/プロシージャ呼び出しに対応するクラスのプロパティ)を互いに区別できないと繰り返し主張しています。

たとえば、クラスPersonに属性がある場合、定数属性として定義されている場所またはのようなものに内部的に対応するageかどうかを表記法から判断Person.ageすることは不可能であると主張します。これは私にとって理にかなっています。ただし、彼は次のように主張し続けています。return current_year - self.birth_datereturn self.ageself.age

クラスの短い形式として知られるクラスの標準クライアントドキュメントは、特定の機能が属性または関数のどちらであるかを明らかにしないように考案されます。

つまり、クラスのドキュメントでさえ、「getter」が計算を実行するかどうかを指定することを避けるべきだと主張しています

これ、私は従わない。この区別をユーザーに知らせることが重要になるのは、ドキュメントだけではありませんか?Personオブジェクトで満たされたデータベースを設計する場合Person.age、高価な呼び出しかどうかを知ることは重要ではないので、何らかのキャッシュを実装するかどうかを決定できますか?彼が言っていることを誤解していませんか、それとも彼はOOP設計哲学の特に極端な例ですか?


1
興味深い質問。ごく最近、非常によく似たものについて質問しました。どのプロパティが値を変更し、どのプロパティが一定のままであるかが明確になるように、どのようにインターフェイスを設計しますか?。そして、私はドキュメンテーション、すなわちバートランド・マイヤーが反論するように見えるものに向けて良い答えを得ました。
stakx

私は本を​​読んでいません。Meyerは、彼が推奨するドキュメントのスタイルの例を挙げていますか?あなたがどんな言語で機能するかを説明することを想像するのは難しいと思います。
user16764

1
@PatrickCollins「名詞の王国での実行」を読んで、動詞と名詞の概念を理解することをお勧めします。第二に、OOPはゲッターとセッターに関するものではありません
。AlanKay

@AndreasScheinert- これについて言及していますか?私は「馬蹄形のネイルが欲しい」と笑いましたが、それはオブジェクト指向プログラミングの悪についての暴言のようです。
パトリックコリンズ

1
@PatrickCollinsはい、これ:steve-yegge.blogspot.com/2006/03/…!それは熟考するためのいくつかのポイントを与え、他のポイントは次のとおりです。セッターを使用して(ab)によってオブジェクトをデータ構造に変換する必要があります。
アンドレアスシャイナート

回答:


58

Meyerのポイントは、高価な操作がある場合にユーザーに伝えるべきではないということではありません。関数がデータベースにアクセスするか、ウェブサーバーにリクエストを送信し、コンピューティングに数時間かかる場合、他のコードはそれを知る必要があります。

ただし、クラスを使用するコーダーは、実装したかどうかを知る必要はありません。

return currentAge;

または:

return getCurrentYear() - yearBorn;

これら2つのアプローチ間のパフォーマンス特性は非常に小さいため、問題ではありません。クラスを使用しているコーダーは、実際にあなたが持っているものを気にするべきではありません。それがマイヤーのポイントです。

ただし、常にそうとは限りません。たとえば、コンテナにサイズメソッドがあるとします。それを実装できます:

return size;

または

return end_pointer - start_pointer;

または、次のようになります。

count = 0
for(Node * node = firstNode; node; node = node->next)
{
    count++
}
return count

最初の2つの違いは実際には重要ではありません。しかし、最後のものはパフォーマンスに深刻な影響を及ぼす可能性があります。そのため、たとえばSTL .size()はそうO(1)です。サイズの計算方法は正確には文書化されていませんが、パフォーマンスの特性はわかります。

そのため、パフォーマンスの問題を文書化します。実装の詳細を文書化しないでください。std :: sortが適切かつ効率的に処理する限り、どのようにソートするかは気にしません。また、クラスは物事の計算方法を文書化するべきではありませんが、予期しないパフォーマンスプロファイルがあるものがある場合は、それを文書化します。


4
さらに、時間/空間の複雑さを最初に文書化し、次に関数にこれらのプロパティがある理由を説明してください。例:// O(n) Traverses the entire user list.
ジョンパーディ

2
Pythonのは、とささいなように=(何かlenこれを行うには失敗した...(少なくともいくつかの状況では、それはO(n)私が長さを格納代わりに、それをすべてのループの繰り返しを再計算提案したときに私たちは大学でのプロジェクトで学んだように、)
Izkata

@Iskata、好奇心が強い。どんな構造だったのO(n)か覚えていますか?
ウィンストンユワート

@WinstonEwert残念ながらそうではありません。それは4年以上前のデータマイニングプロジェクトで、別のクラスでCを使っていたので、友人に提案するだけ
でした。– Izkata

1
@JonPurdy通常のビジネスコードでは、big-Oの複雑さを指定することはおそらく意味がありません。たとえば、O(1)データベースアクセスは、O(n)インメモリリストトラバーサルよりもはるかに遅くなる可能性が高いため、重要なことを文書化します。しかし、ドキュメント化の複雑さが非常に重要な場合は確かにあります(コレクションまたは他のアルゴリズムが重いコード)。
-svick

16

アカデミックまたはCSの純粋主義者の観点からは、当然ながら、機能の実装の内部についてドキュメントに何かを記述することは失敗です。これは、クラスのユーザーがクラスの内部実装について想定しないことが理想的だからです。実装が変更された場合、理想的にはユーザーが気付かないことはありません。この機能は抽象化を作成し、内部は完全に非表示のままにしてください。

ただし、実際のプログラムのほとんどは、Joel Spolskyの「漏れやすい抽象化の法則」に苦しんでいます。

「自明でない抽象化はすべて、ある程度漏れやすい。」

つまり、複雑な機能を完全にブラックボックスに抽象化することは事実上不可能です。そして、この典型的な症状はパフォーマンスの問題です。したがって、実際のプログラムでは、どの呼び出しが高価であり、どれがそうでないかが非常に重要になる可能性があり、優れたドキュメントにはその情報を含める必要があります(またはクラスのユーザーがパフォーマンスについて仮定することが許可されている場所とそうでない場所を言う必要があります) )。

したがって、私のアドバイスは次のとおりです。実世界のプログラムのドキュメントを作成する場合は高価な呼び出しの可能性に関する情報を含め、パフォーマンスの考慮事項を保持する必要があるため、CSコースの教育目的のみで作成するプログラムの場合は除外します意図的に範囲外です。


+1に加えて、作成されるドキュメントのほとんどは、次のプログラマがプロジェクトを維持するためのものであり、次のプログラマがそれを使用するためのものではありません。
jmoreno

12

特定の呼び出しが高価かどうかを書くことができます。ベターは、のような命名規則を使用してgetAgeすばやくアクセスし、loadAgeまたはfetchAge高価な検索のため。メソッドがIOを実行しているかどうかをユーザーに確実に通知する必要があります。

ドキュメントで提供するすべての詳細は、クラスが尊重しなければならない契約のようなものです。重要な動作について通知する必要があります。多くの場合、大きなO表記で複雑さを示します。しかし、あなたは通常短く、要点になりたいです。


1
+1は、ドキュメントがそのインターフェイスと同じくらいクラスのコントラクトの一部であることに言及したためです。
バートヴァンインゲンシェナウ

これを支持します。さらに一般に、メソッドに動作を提供することにより、ゲッターの必要性を最小限に抑えようとします。
セブンフォース

9

Personオブジェクトで満たされたデータベースを設計する場合、Person.ageが高価な呼び出しであるかどうかを知ることは重要ではないでしょうか?

はい。

そのため、Find()関数を使用して呼び出しに時間がかかることを示すことがあります。これは何よりも慣習です。(それはユーザーに対するのかもしれませんが)、それは返す関数や属性にかかる時間は、プログラムに違いはありません、プログラマの間で存在があることを属性として宣言されている場合、それを呼び出すためのコストがあるべき、という期待は、低い。

いずれにせよ、何かが関数か属性かを推測するのに十分な情報がコード自体にあるはずなので、ドキュメントでそれを言う必要性は本当にありません。


4
+1:その規則はかなりの場所で慣用的です。さらに、ドキュメントは、インターフェイスレベルで行われるべきである-その時点であなたはありません知っている Person.Ageがどのように実装されますか。
テラスティン

@Telastyn:ドキュメンテーションについてまったくこのように考えたことはありません。つまり、インターフェイスレベルで実行する必要があります。今では明らかです。その貴重なコメントに対して+1。
stakx

私はこの答えがとても好きです。パフォーマンスがプログラム自体の問題ではないと説明する完璧な例は、PersonがRESTfulサービスから取得されたエンティティである場合です。GETは固有のものですが、これが安価か高価かは明らかではありません。もちろん、これは必ずしもOOPではありませんが、ポイントは同じです。
maple_shaft

Get属性に対するメソッドを使用して、より重い操作を示すための+1 。開発者がプロ​​パティを単なるアクセサーと見なし、値をローカル変数に保存する代わりに複数回使用するコードを十分に見てきたため、非常に複雑なアルゴリズムを複数回実行しています。そのようなプロパティを実装しない慣習がなく、ドキュメントが複雑さをほのめかさないなら、私はそのようなアプリケーションを幸運に維持しなければならない人を望みます。
enzi

このコンベンションはどこから来たのですか?Javaについて考えると、逆の方法が予想されます。つまり、getメソッドは属性アクセスと同等であり、それほど高価ではありません
sevenforce

3

この本の初版は、OOPの初期の1988年に書かれたことに注意することが重要です。これらの人々は、今日広く使用されている、より純粋なオブジェクト指向言語で作業していました。今日最も人気のあるオブジェクト指向言語であるC ++、C#、Javaは、初期のより純粋なオブジェクト指向言語が機能する方法とはかなり大きな違いがあります。

C ++やJavaなどの言語では、属性へのアクセスとメソッド呼び出しを区別する必要があります。instance.getter_methodとには違いのある世界がありinstance.getter_method()ます。1つは実際に価値を獲得し、もう1つは獲得しません。

SmalltalkまたはRubyの説得力のあるより純粋なOO言語(この本で使用されているEiffel言語がそうであるように見える)を使用する場合、完全に有効なアドバイスになります。これらの言語は、暗黙的にメソッドを呼び出します。との間に違いはinstance.attributeありませんinstance.getter_method

私はこの点に汗をかいたり、独断的に考えたりしません。意図は良いです-クラスのユーザーに無関係な実装の詳細を心配させたくない-しかし、それは多くの現代言語の構文にきれいに翻訳しません。


1
提案が行われた年を考慮することに関する非常に重要なポイント。Nit:SmalltalkとSimulaは60年代と70年代に遡るので、88はほとんど「早い日」ではありません。
luser droog

2

ユーザーとして、何かがどのように実装されているかを知る必要はありません。

パフォーマンスが問題になる場合は、クラス実装内ではなく、クラス実装内で何かを行う必要があります。したがって、正しいアクションは、クラスの実装を修正するか、メンテナーにバグを報告することです。


3
ただし、計算コストの高い方法がバグであるということは常にありますか?簡単な例として、文字列の配列の長さを合計することに関心があるとしましょう。内部的には、私の言語の文字列がPascalスタイルかCスタイルかはわかりません。前者の場合、文字列はその長さを「知っている」ため、文字列の数に応じてlength-summing-loopが線形時間をとると予想できます。また、文字列の長さを変更する操作には、変更のstring.lengthたびに再計算されるため、オーバーヘッドが関連付けられていることも知っておく必要があります。
パトリックコリンズ

3
後者の場合、文字列はその長さを「知らない」ので、長さ加算ループに2次時間がかかると予想できます(文字列の数と長さの両方に依存します)が、長さを変更する操作文字列の方が安くなります。これらの実装はどちらも間違っておらず、バグ報告にも値しませんが、予期しないしゃっくりを避けるために、わずかに異なるスタイルのコーディングが必要です。ユーザーが少なくとも何が起こっているのか漠然とした考えを持っている方が簡単ではないでしょうか?
パトリックコリンズ

したがって、文字列クラスがCスタイルを実装していることがわかっている場合は、その事実を考慮してコーディングの方法を選択します。しかし、文字列クラスの次のバージョンが新しいFooスタイルの表現を実装する場合はどうでしょうか?それに応じてコードを変更しますか、それともコードの誤った仮定に起因するパフォーマンスの低下を受け入れますか?
ムーヴィシエル

そうですか。「特定の実装に依存して、コードから余分なパフォーマンスを絞り出すにはどうすればよいですか?」に対するオブジェクト指向の応答 「できません」です。そして、「私のコードは予想よりも遅いのに、なぜですか?」「書き直す必要がある」です。それは多かれ少なかれアイデアですか?
パトリックコリンズ

2
@PatrickCollins OOの応答は、実装ではなくインターフェイスに依存しています。インターフェイスの定義の一部としてパフォーマンス保証が含まれていないインターフェイスを使用しないでください(C ++ 11のList.sizeがO(1)を保証している例のように)。インターフェイス定義に実装の詳細を含める必要はありません。あなたのコードがあなたが望むより遅い場合、あなたがそれをより速くするために変更しなければならない他の答えがあります(ボトルネックを決定するためにそれをプロファイリングした後)?
ストーンメタル

2

ルーチン/メソッドの複雑さのコストについてプログラマーに通知できないプログラマー向けのドキュメントには欠陥があります。

  • 私たちは副作用のない方法を生み出そうとしています。

  • メソッドの実行に以外の実行時の複雑さやメモリの複雑さがある場合O(1)、メモリまたは時間に制約のある環境では、副作用があると見なすことができます

  • メソッドがまったく予想外のことを行うと、最小限の驚き原則に違反します。この場合、メモリの占有やCPU時間の浪費です。


1

あなたは彼を正しく理解していたと思いますが、良い点もあると思います。Person.age高価な計算で実装されている場合、ドキュメントでもそれを見たいと思います。繰り返し呼び出す(安価な操作の場合)か、一度呼び出して値をキャッシュする(高価な場合)かで違いが生じる可能性があります。確かにわかりませんが、この場合、マイヤーはドキュメントに警告を含めることに同意するかもしれません

これを処理する別の方法は、名前が長い計算(などPerson.ageCalculatedFromDB)を行うことを意味する新しい属性を導入Person.ageし、クラス内にキャッシュされた値を返すことですが、これは常に適切であるとは限らず、複雑すぎるようです私の意見では、物事。


3
一つは、また、あなたが知る必要がある場合という議論作ることができるageのをPerson、あなたは関係なく、それを得るためにメソッドを呼び出す必要があります。呼び出し元が計算を回避するためにあまりにも賢い半分のことを始めた場合、彼らは誕生日の境界を越えたために実装が正しく動作しないというリスクを冒します。クラス内の高価な実装は、プロファイリングによって根ざすことができるパフォーマンスの問題として現れ、キャッシュなどの改善はクラス内で実行でき、すべての呼び出し元が利点(および正しい結果)を確認できます。
Blrfl

1
@Blrfl:はい、クラスでキャッシュを行う必要がありPersonますが、質問はより一般的なものであり、それPerson.ageは単なる例であると思います。おそらく、呼び出し側が選択する方が理にかなっている場合があります-呼び出し先には、同じ値を計算するための2つの異なるアルゴリズムがあります:1つは高速だが不正確、もう1つはもっと遅いがより正確です(3Dレンダリングは1つの場所として思い浮かびます発生する可能性があります)、ドキュメントでこれについて言及する必要があります。
FrustratedWithFormsDesigner

異なる結果を提供する2つの方法は、毎回同じ答えを期待する場合とは異なるユースケースです。
Blrfl

0

オブジェクト指向クラスのドキュメントには、多くの場合、クラスのメンテナーに設計を変更する柔軟性を与えることと、クラスのコンシューマーがその可能性を最大限に活用することとのトレードオフが含まれます。不変クラスは、特定の持っているであろう多くの特性があります場合は、正確な互いの関係(例えばLeftRightおよびWidth整数座標グリッド整列長方形のプロパティ)、2つのプロパティの任意の組み合わせを格納するクラスを設計して3番目のプロパティを計算するか、3つすべてを格納するように設計することができます。インターフェースについて何も保存されているプロパティが明確にならない場合、クラスのプログラマーは何らかの理由でそうすることが役立つと判明した場合にデザインを変更できる可能性があります。対照的に、たとえば、プロパティの2つがfinalフィールドとして公開され、3 つ目のプロパティが公開されていない場合、クラスの将来のバージョンは常に「基底」と同じ2つのプロパティを使用する必要があります。

プロパティが正確な関係を持っていない場合(たとえば、プロパティがであるfloatかとdoubleいうよりもint)、クラスの値を「定義する」プロパティを文書化する必要がある場合があります。たとえば、LeftプラスWidthが等しいと想定されていてもRight、浮動小数点演算はしばしば不正確です。たとえば、Rectangle型を使用するa がFloat受け入れLeftWidthコンストラクタパラメータLeftとしてas 1234567fおよびWidthas が指定されて構築されるとし1.1fます。float合計の最適な表現は1234568.125 [1234568.13と表示される場合があります]; 次に小さいのfloatは1234568.0です。クラスが実際に保存しLeftWidth、指定された幅の値を報告する場合があります。しかし、コンストラクタが計算した場合Rightには、渡されたに基づくLeftWidth後で、および計算Widthに基づくLeftRight、それはとして幅を報告する1.25fのではなく渡されたとして1.1f

相互に関連する値の1つを変更すると、少なくとも1つの他の値が変更されることになるため、変更可能なクラスではさらに興味深いことがありますが、どちらが常に明確であるとは限りません。いくつかのケースでは、そのようなとしての「セット」単一のプロパティメソッドを避けるために最もよいが、代わりにどちらかの例にメソッドがありSetLeftAndWidth、またはSetLeftAndRightプロパティが指定されており、変更されているものを、または他のmakeがクリアに(例えばMoveRightEdgeToSetWidthChangeWidthToSetLeftEdgeまたはMoveShapeToSetRightEdge) 。

どのプロパティの値が指定され、どのプロパティの値が他から計算されたかを追跡するクラスがあると便利な場合があります。たとえば、「時間の瞬間」クラスには、絶対時間、現地時間、タイムゾーンオフセットが含まれる場合があります。このような多くのタイプと同様に、2つの情報があれば、3番目の情報を計算できます。知っている情報の一部は計算されましたが、重要な場合があります。たとえば、イベントが「17:00 UTC、タイムゾーン-5、現地時間12:00 pm」に発生したと記録され、後でタイムゾーンが-6であることが検出されたとします。UTCがサーバーから記録されたことを知っている場合、記録を「18:00 UTC、タイムゾーン-6、現地時間12:00 pm」に修正する必要があります。誰かが現地時間をオフクロックで入力した場合、「17:00 UTC、タイムゾーン-6、現地時間11:00 am」になります。ただし、グローバル時間とローカル時間のどちらを「より信頼できる」とみなすべきかを知らなければ、どの修正を適用すべきかを知ることはできません。ただし、レコードが指定された時刻を追跡している場合、タイムゾーンを変更すると、一方が変更されずに他方が変更される可能性があります。


0

クラス内の情報を非表示にする方法に関するこれらのすべてのルールは、クラスのユーザーのうち、内部実装への依存関係の作成を間違える人から保護する必要があるという前提で完全に理にかなっています。

クラスにそのような視聴者がいる場合、そのような保護を組み込むことは問題ありません。しかし、ユーザーがクラス内の関数への呼び出しを記述するとき、ユーザーは実行時の銀行口座であなたを信頼しています。

ここに私がよく見る種類のものがあります:

  1. オブジェクトには、何らかの意味で古いかどうかを示す「修正」ビットがあります。十分に単純ですが、従属オブジェクトがあるため、「変更」をすべての従属オブジェクトを合計する関数にするのは簡単です。次に、下位オブジェクトのレイヤーが複数ある場合(同じオブジェクトを複数回共有する場合)、「変更された」プロパティの単純な「Get」が実行時間の健全な部分を占めることになります。

  2. オブジェクトが何らかの方法で変更される場合、ソフトウェアの周りに散らばっている他のオブジェクトは「通知」される必要があると想定されます。これは、さまざまなプログラマーによって記述されたデータ構造、ウィンドウなどの複数のレイヤーで行われ、場合によっては保護が必要な無限の再帰で繰り返されます。それらの通知ハンドラーのすべてのライターが時間を無駄にしないように十分に注意している場合でも、複合相互作用全体が予測時間の非常に大きな実行時間の一部を使用することになり、単純に「必要」であるという仮定は快く行われます。

だから、私は外の世界にきれいで抽象的なインターフェースを提供するクラスを見たいのですが、私はそれらがどのような働きをしているのかを理解するためだけに、それらがどのように機能するかについていくつかの概念を持ちたいです。しかし、それを超えて、私は「少ないほど多い」と感じる傾向があります。人々はデータ構造に夢中になっており、より多くの方が優れていると考えています。パフォーマンスチューニングを行うとき、パフォーマンスの問題の普遍的な大規模な理由は、人々が教えられる方法で構築された肥大化したデータ構造への奴隷的遵守です。

それでは、図に進みます。


0

「計算するかどうか」や「パフォーマンス情報」などの実装の詳細を追加すると、コードとドキュメントの同期を維持するのがより困難になります。

例:

「パフォーマンスが高い」メソッドがある場合、そのメソッドを使用するすべてのクラスにも「高価な」ドキュメントを作成しますか?実装を変更して、もう高価にならないようにしたらどうでしょう。この情報もすべての消費者に更新しますか?

もちろん、コード管理者がコードのドキュメントからすべての重要な情報を取得するのは良いことですが、もう有効でないものを主張するドキュメントは好きではありません(コードと同期していない)


0

受け入れられた答えが結論に達すると:

そのため、パフォーマンスの問題を文書化します。

そして自己文書化されたコードは、ドキュメントよりも優れていると考えられている、メソッド名があること、次の必要があり、異常なパフォーマンスの結果を述べます。

まだPerson.ageですreturn current_year - self.birth_dateが、メソッドが年齢を計算するためにループを使用する場合(はい):Person.calculateAge()

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