私の学校のCSプログラムはオブジェクト指向プログラミングについての言及を避けているので、それを補足するために自分で読んでいます-具体的には、オブジェクト指向ソフトウェア構築Bertrand Meyerによる。
Meyerは、クラスは実装について可能な限り多くの情報を隠す必要があると繰り返し主張しています。これは理にかなっています。特に、属性(つまり、クラスの静的で計算されていないプロパティ)とルーチン(関数/プロシージャの呼び出しに対応するクラスのプロパティ)は互いに区別できないはずであると繰り返し主張しています。
たとえば、クラスPerson
が属性age
を持っている場合、彼は、表記法から、Person.age
がreturn current_year - self.birth_date
または単にreturn self.age
、ここでself.age
は定数属性として定義されています。これは私には理にかなっています。しかし、彼はさらに次のように主張している:
クラスの短い形式と呼ばれるクラスの標準クライアントドキュメントは、特定の機能が属性であるか関数であるかを明らかにしないように考案されます(いずれかの可能性がある場合)。
つまり、クラスのdocumentationでさえ、「ゲッター」が計算を実行するかどうかを指定することは避けるべきであると彼は主張しています。
これ、私はついていません。ドキュメントは、この違いをユーザーに通知することが重要な1つの場所ではありませんか? Person
オブジェクトで満たされたデータベースを設計する場合、Person.age
が高価な呼び出しであるかどうかを知ることは重要ではないので、ある種の実装を行うかどうかを決定できますそれのためのキャッシュ?私は彼の言っていることを誤解しましたか、それとも彼はOOP設計哲学の特に極端な例ですか?
Meyerの要点は、高額な操作がある場合はユーザーに知らせるべきではないということではないと思います。関数がデータベースにアクセスする場合、またはWebサーバーにリクエストを送信し、数時間を費やす場合は、他のコードがそれを知る必要があります。
ただし、クラスを使用するコーダーは、実装済みかどうかを知る必要はありません。
_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)
であると言っているのはこのためです。サイズの計算方法を正確に説明しているわけではありませんが、パフォーマンスの特性を示しています。
So:パフォーマンスの問題を文書化します。実装の詳細を文書化しないでください。 std :: sortが適切かつ効率的にソートする限り、私はstd :: sortがどのようにソートするかは気にしません。また、クラスはそれがどのように計算するかを文書化すべきではありませんが、何かが予期しないパフォーマンスプロファイルを持っている場合は、それを文書化します。
学問的またはCSの純粋主義者の見地から、機能の実装の内部について何かをドキュメントに記述することはもちろん失敗です。これは、クラスのユーザーは、クラスの内部実装について想定しないことが理想的であるためです。実装が変更されても、理想的にはユーザーが気付かないことが理想です-機能は抽象化を作成し、内部は完全に非表示にしておく必要があります。
ただし、ほとんどの現実世界のプログラムは、Joel Spolskyの "Law of Leaky Abstractions" の影響を受けます。
「すべての自明ではない抽象化は、ある程度、漏れやすいです。」
つまり、複雑な機能の完全なブラックボックス抽象化を作成することは事実上不可能です。そして、これの典型的な症状はパフォーマンスの問題です。したがって、実際のプログラムでは、どの呼び出しが高価で、どれが高価でないかが非常に重要になる可能性があり、優れたドキュメントにはその情報を含める必要があります(または、クラスのユーザーがパフォーマンスに関する仮定を行うことができる場所とできない場所を示す必要があります)。
ですから、私のアドバイスは次のとおりです。パフォーマンスに関する考慮事項を維持する必要がある場合、実際のプログラムのドキュメントを作成する場合は高額な通話の可能性に関する情報を含め、CSコースの教育目的でのみ作成しているプログラムの場合は除外します。意図的に範囲外です。
特定の呼び出しが高価であるかどうかを書くことができます。高速アクセスにはgetAge
のような命名規則を使用し、高コストの検索にはloadAge
またはfetchAge
を使用してください。メソッドがIOを実行している場合は、必ずユーザーに通知する必要があります。
ドキュメントで提供するすべての詳細は、クラスが尊重する必要のある契約のようなものです。重要な動作について通知する必要があります。多くの場合、大きなO表記で複雑さを示します。しかし、あなたは通常、短くて要領を得たいと思っています。
Personオブジェクトで満たされたデータベースを設計する場合、Person.ageが高価な呼び出しであるかどうかを知ることは重要ではないでしょうか?
はい。
そのため、Find()
関数を使用して、呼び出しに時間がかかる場合があることを示しています。これは何よりも慣例です。関数または属性が戻るのにかかる時間は、プログラムには影響しません(ユーザーにとってはそうかもしれません)。ただし、プログラマの間ではis属性として宣言されている場合、それを呼び出すためのコストは低くなければなりません。
いずれにせよ、何かが関数であるか属性であるかを推定するのに十分な情報がコード自体にあるはずなので、ドキュメントでそれを言う必要は本当にありません。
この本の初版はOOPの初期の1988年に書かれたことに注意することが重要です。これらの人々は、今日広く使用されている、より純粋にオブジェクト指向の言語を扱っていました。私たちの最も人気のあるOO今日の言語-C++、C#&Java-初期の、より純粋にOOの言語が機能していた方法とはかなり大きな違いがあります。
C++やJavaなどの言語では、属性へのアクセスとメソッド呼び出しを区別する必要があります。 _instance.getter_method
_とinstance.getter_method()
には違いがあります。 1つは実際に値を取得し、もう1つは取得しません。
Smalltalkのより純粋なOO言語、またはRuby説得(この本で使用されているエッフェル言語がそうであるように見える))で作業する場合、完全に有効なアドバイスです。これらの言語は暗黙的にメソッドを呼び出します。_instance.attribute
_と_instance.getter_method
_の違いはありません。
私はこの点に汗をかいたり、独断的にそれをとったりしません。意図は良好です-クラスのユーザーが無関係な実装の詳細について心配する必要はありません-しかし、それは多くの現代言語の構文にきれいに変換されません。
ユーザーとして、何かがどのように実装されているかを知る必要はありません。
パフォーマンスに問題がある場合は、クラス実装ではなく、クラス実装内で何かを行う必要があります。したがって、正しいアクションは、クラスの実装を修正するか、メンテナにバグを報告することです。
プログラマー向けのルーチン/メソッドの複雑さのコストをプログラマーに通知できないドキュメントは、欠陥があります。
私たちは副作用のない方法を生み出そうとしています。
メソッドの実行がO(1)
以外の実行時の複雑さおよび/またはメモリの複雑さを持っている場合、メモリまたは時間に制約のある環境では、副作用があると見なすことができます。
最小の驚きの原則は、メソッドが完全に予期しない何かを行う場合、違反されます。この場合、メモリを消費したり、CPU時間を浪費したりします。
あなたは彼を正しく理解したと思いますが、あなたにも良い点があると思います。 Person.age
が高価な計算で実装されている場合、ドキュメントでもそれを確認したいと思います。繰り返し呼び出す(安価な操作の場合)か、一度呼び出して値をキャッシュする(高価な場合)かの違いが生じる可能性があります。確かではありませんが、この場合、Meyer mightは、ドキュメントに警告を含める必要があることに同意します。
これを処理するもう1つの方法は、名前が長い計算が行われる可能性があることを意味する新しい属性(Person.ageCalculatedFromDB
など)を導入し、Person.age
がクラス内にキャッシュされた値を返すようにすることですが、これは私の意見では、常に適切であるとは限らず、事態が複雑になりすぎるようです。
受け入れられた答えが結論に達すると、
So:ドキュメントのパフォーマンスの問題。
そして 自己文書化されたコードは文書化よりも優れていると見なされます メソッド名は異常なパフォーマンス結果を示す必要があることになります。
したがって、まだ_Person.age
_ for _return current_year - self.birth_date
_ですが、メソッドがループを使用して年齢(yes)を計算する場合:Person.calculateAge()
クラス内の情報を非表示にする方法に関するこれらすべてのルールは、クラスのユーザーの間で、内部実装への依存関係の作成を間違えることから保護する必要があるという前提で完全に理にかなっています。
クラスにそのような聴衆がいる場合、そのような保護を組み込むことは問題ありません。しかし、ユーザーがクラス内の関数への呼び出しを書き込むと、実行時の銀行口座でユーザーを信頼します。
これは私がよく目にするものです:
オブジェクトには、何らかの意味で古いものであるかどうかを示す「変更」ビットがあります。非常に単純ですが、下位オブジェクトがあるので、「modified」をすべての下位オブジェクトを合計する関数にするのは簡単です。次に、下位オブジェクトの複数のレイヤーがある場合(同じオブジェクトを複数回共有する場合があります)、「変更された」プロパティの単純な「取得」は、実行時間のかなりの部分を占めることになります。
オブジェクトが何らかの方法で変更されると、ソフトウェアの周りに散在する他のオブジェクトが「通知」される必要があると想定されます。これは、さまざまなプログラマーによって作成されたデータ構造、ウィンドウなどの複数のレイヤーで発生する可能性があり、時には保護する必要がある無限の再帰で繰り返されます。これらの通知ハンドラーのすべての作成者が時間を無駄にしないように十分に注意していても、複合インタラクション全体は、予測されずに痛々しいほどの実行時間の割合を使用することになり、それが単に「必要」であるという仮定は気楽に行われます。
SO、外の世界にすっきりした抽象的なインターフェースを提供するクラスを見るのが好きですが、それらが私を救っている作業を理解するためだけに、それらがどのように機能するかについての概念を持っています。しかし、それを超えて、私は「少ないほど良い」と感じる傾向があります。人々はデータ構造に夢中になっているので、多くの方が良いと思います。パフォーマンスのチューニングを行うと、パフォーマンスの問題の普遍的で大規模な理由は、人々が教える方法で構築された肥大化したデータ構造への順応です。
だから図に行きます。
「計算するかどうか」や「パフォーマンス情報」などの実装の詳細を追加すると、より詳細になりますコードとドキュメントの同期を維持するのが困難。
例:
「パフォーマンスの高い」メソッドがある場合、そのメソッドを使用するすべてのクラスにも「高価」のドキュメントを作成しますか?実装を変更してコストがかからなくなったらどうなるでしょう。この情報もすべての消費者に更新しますか?
もちろん、コードのメンテナがコードドキュメントからすべての重要な情報を取得するのはいいことですが、もう有効ではない(コードと同期していない)ものを主張するドキュメントは好きではありません
オブジェクト指向クラスのドキュメントには、多くの場合、クラスのメンテナに設計を変更する柔軟性を与えることと、クラスのコンシューマがその可能性を最大限に活用できるようにすることのトレードオフが伴います。不変のクラスに、相互に特定のexact関係を持つ多数のプロパティがある場合(たとえば、整数座標グリッド配置の長方形のLeft
、Right
、およびWidth
プロパティ)、1つ2つのプロパティの任意の組み合わせを格納して3つ目を計算するようにクラスを設計するか、3つすべてを格納するように設計することもできます。インターフェースについて何も保存されているプロパティが明確でない場合、クラスのプログラマーは、何らかの理由でそうすることが役立つことが判明した場合に、デザインを変更できる可能性があります。対照的に、例えば2つのプロパティはfinal
フィールドとして公開され、3つ目は公開されないため、クラスの将来のバージョンでは、常に「基礎」と同じ2つのプロパティを使用する必要があります。
プロパティが正確な関係を持っていない場合(たとえば、それらがfloat
ではなくdouble
またはint
であるため)、クラスの値を「定義」しているプロパティを文書化する必要がある場合があります。たとえば、Left
とWidth
がRight
と等しいと想定されていても、浮動小数点演算はしばしば不正確です。たとえば、タイプRectangle
を使用するFloat
がLeft
とWidth
をコンストラクターパラメーターとして受け入れると、Left
が1234567f
として指定され、Width
が1.1f
として指定されているとします。合計のfloat
の最適な表現は1234568.125です(1234568.13と表示される場合があります)。次に小さいfloat
は1234568.0になります。クラスが実際にLeft
およびWidth
を格納している場合、指定されたとおりに幅の値を報告することがあります。ただし、コンストラクターが渡されたRight
およびLeft
に基づいてWidth
を計算し、後でWidth
およびLeft
に基づいてRight
を計算した場合、幅は1.25f
としてではなく1.1f
として報告されます。
可変クラスでは、相互に関連する値の1つを変更すると少なくとも他の1つが変更されることを意味するため、状況はさらに興味深いものになる可能性がありますが、どれがどれであるかが常に明確であるとは限りません。場合によっては、単一のプロパティをそのように「設定」するメソッドを避けるのが最善ですが、代わりに、 SetLeftAndWidth
またはSetLeftAndRight
、または指定されているプロパティと変更されているプロパティ(たとえば、MoveRightEdgeToSetWidth
、ChangeWidthToSetLeftEdge
、またはMoveShapeToSetRightEdge
)を明確にします。
指定されたプロパティの値と、他のプロパティから計算されたプロパティの値を追跡するクラスがあると便利な場合があります。たとえば、「瞬間」クラスには、絶対時間、現地時間、およびタイムゾーンオフセットが含まれる場合があります。多くのそのようなタイプと同様に、2つの情報があれば、1つは3つ目を計算できます。ただし、whichの情報が計算されたことを知ることは重要な場合があります。たとえば、イベントが「17:00 UTC、タイムゾーン-5、現地時間12:00 pm」に発生したと記録され、後でタイムゾーンが-6であることがわかったとします。 UTCがサーバーから記録されたことがわかっている場合は、レコードを「18:00 UTC、タイムゾーン-6、現地時間12:00 pm」に修正する必要があります。誰かが時計の現地時間を入力した場合、「17:00 UTC、タイムゾーン-6、現地時間11:00 am」になります。ただし、グローバル時刻とローカル時刻のどちらを「より信頼できる」と見なすべきかを知らなければ、どの修正を適用すべきかを知ることはできません。ただし、レコードが指定された時間を追跡している場合、タイムゾーンを変更すると、一方がそのままになり、もう一方が変更される可能性があります。