クラスを「オープン」に設計するべきではないのはなぜですか?


44

さまざまなStack Overflowの質問や他の人のコードを読むとき、クラスを設計する方法の一般的なコンセンサスは閉じられています。これは、JavaおよびC#のデフォルトでは、すべてがプライベートであり、フィールドがfinalであり、一部のメソッドがfinalであり、クラスがfinalでさえあることを意味します。

この背後にある考え方は、実装の詳細を隠すことです。これは非常に良い理由です。ただしprotected、ほとんどのOOP言語とポリモーフィズムの存在により、これは機能しません。

クラスに機能を追加したり変更したりするたびに、私は通常、あらゆる場所に配置されたプライベートおよびファイナルによって妨げられています。ここでは、実装の詳細が重要です。実装を取得して拡張し、結果が何であるかを十分に理解しています。ただし、プライベートおよび最終的なフィールドとメソッドにアクセスできないため、3つのオプションがあります。

  • クラスを拡張せず、より複雑なコードにつながる問題を回避してください
  • クラス全体をコピーして貼り付け、コードの再利用性を損なう
  • プロジェクトを分岐する

これらは良い選択肢ではありません。なぜprotectedそれをサポートする言語で書かれたプロジェクトで使用されないのですか?一部のプロジェクトがクラスからの継承を明示的に禁止しているのはなぜですか?


1
私は同意します、私はJava Swingコンポーネントでこの問題を抱えていました、それは本当に悪いです。
ジョナス


3
この問題が発生している場合、クラスの設計がそもそも不十分である可能性が高くなります。または、誤って使用しようとしている可能性があります。クラスに情報を要求するのではなく、何かをするように要求します。そのため、通常はデータである必要はありません。これは常に当てはまるわけではありませんが、クラスのデータにアクセスする必要があることがわかった場合、何かがおかしくなっている可能性が高いでしょう。
ビルK

2
「ほとんどのOOP言語?」クラスを閉じることができない場所をもっと知っています。4番目のオプションは省略しました:言語の変更。
ケビンクライン

@Jonas Swingの大きな問題の1つは、実装が多すぎることです。それは本当に開発を殺します。
トムホーティン-タックライン

回答:


55

特に拡張を行うプログラマがクラスの動作方法を完全に理解していない場合、拡張時にクラスが適切に動作するように設計すると、かなりの労力がかかります。プライベートなものをすべて取り出してパブリック(または保護)にし、それを「オープン」と呼ぶことはできません。他の誰かが変数の値を変更できるようにする場合、すべての可能な値がクラスにどのように影響するかを考慮する必要があります。(変数をnullに設定した場合、空の配列ですか?負の数ですか?)同じことが、他の人にメソッドの呼び出しを許可する場合にも当てはまります。慎重に考えます。

そのため、クラスをオープンにすべきではありませんが、クラスをオープンにする努力に値しない場合があります。

もちろん、図書館の作者が怠け者だった可能性もあります。あなたが話しているライブラリに依存します。:-)


13
はい、これがクラスがデフォルトでシールされる理由です。クライアントがクラスを拡張する多くの方法を予測することはできないため、クラスを拡張する可能性のある無限の方法をサポートすることにコミットするよりも、それを封印する方が安全です。
ロバートハーヴェイ


18
クラスがどのようにオーバーライドされるかを予測する必要はありません。クラスを拡張する人々が自分のしていることを知っていると仮定するだけです。クラスを拡張し、nullであってはならないときに何かをnullに設定すると、あなたのせいではなく、そのせいです。この引数が理にかなっている唯一の場所は、フィールドがnullの場合に何かがひどく間違ってしまうような、非常に重要なアプリケーションです。
TheLQ

5
@TheLQ:それはかなり大きな仮定です。
ロバートハーヴェイ

9
@TheLQの欠点は非常に主観的な質問です。彼らが変数を一見妥当な値に設定し、それが許可されていないという事実を文書化せず、コードがArgumentException(または同等のもの)をスローしなかったが、サーバーがオフラインになるかリードする場合セキュリティの悪用に、私はそれがあなたのせいであると言うでしょう。そして、有効な値を文書化し、引数チェックを実施した場合、あなたは私が話している種類の努力を正確に出し、その努力が本当にあなたの時間の良い使用であったかどうかを自問する必要があります。
アーロン

24

すべてをデフォルトでプライベートにすることはきびしいですが、反対側から見てください。すべてがデフォルトでプライベートである場合、何かを公開する(または保護する、ほぼ同じこと)ことを意識的に選択することになっています。クラスの使用方法については、消費者であるあなたとのクラス作成者の契約です。これはあなたにとっても便利です。作成者は、インターフェイスが変更されない限り、クラスの内部動作を自由に変更できます。また、クラスのどの部分に依存でき、どの部分が変更される可能性があるかを正確に知っています。

基本的な考え方は「疎結合」です(「狭いインターフェース」とも呼ばれます)。その価値は、複雑さを抑えることにあります。コンポーネントが相互作用できる方法の数を減らすことにより、コンポーネント間の相互依存関係の量も減少します。また、相互依存関係は、メンテナンスと変更管理に関しては最悪の複雑さの1つです。

適切に設計されたライブラリでは、継承を通じて拡張する価値のあるクラスは、適切な場所で保護され、パブリックメンバーを持ち、他のすべてを隠します。


それはいくらか理にかなっています。ただし、非常によく似たものが必要な場合でも、実装で1つまたは2つの変更が加えられた場合(クラスの内部動作)に問題があります。また、クラスをオーバーライドする場合、すでにその実装に大きく依存しているのではないことを理解するのに苦労しています。
TheLQ

5
疎結合の利点のために+1。@TheLQ、それは実装ではなく、あなたが強く依存すべきインターフェースです。
カールビーレフェルト

19

プライベートではないものは、多かれ少なかれ、クラスのすべての将来のバージョンで変更さていない動作存在することになっています。実際、ドキュメント化されているかどうかにかかわらず、APIの一部と見なすことができます。したがって、多くの詳細を公開すると、おそらく互換性の問題が後で発生します。

「最終」に関して 「封印された」クラス、1つの典型的なケースは不変クラスです。フレームワークの多くの部分は、不変の文字列に依存しています。Stringクラスが最終的なものでない場合、不変のStringクラスの代わりに使用すると、他の多くのクラスであらゆる種類のバグを引き起こす可変Stringサブクラスを作成するのは簡単です(魅力的ですら)。


4
これが本当の答えです。他の答えは重要な事柄に触れますが、本当の関連ビットはこれです:そうではないfinal、および/またはprivate所定の位置にロックされて おり、変更することはできません
コンラッドルドルフ

1
@Konrad:開発者がすべてを改良し、下位互換性を持たないと決定するまで。
JAB

それは本当ですが、それがpublicメンバーよりもはるかに弱い要件であることに注意する価値があります。protectedメンバーは、それらを公開するクラスとその直接派生クラスの間でのみコントラクトを形成します。対照的に、public現在および将来継承される可能性のあるすべてのクラスに代わって、すべての消費者と契約するメンバー
-supercat

14

OOには、既存のコードに機能を追加する2つの方法があります。

1つ目は継承によるものです。クラスを取得し、そこから派生します。ただし、継承は注意して使用する必要があります。基本クラスと派生クラスの間にisA関係がある場合、主にパブリック継承を使用する必要があります(たとえば、Rectangle Shapeです)。代わりに、既存の実装を再利用するためのパブリック継承を避ける必要があります。基本的に、パブリック継承は、派生クラスが基本クラス(またはそれより大きいクラス)と同じインターフェースを持っていることを確認するために使用されるため、リスコフの置換原則を適用できます。

機能を追加するもう1つの方法は、委任または構成を使用することです。既存のクラスを内部オブジェクトとして使用する独自のクラスを作成し、実装作業の一部をそれに委任します。

あなたが使おうとしているライブラリーのすべてのファイナルの背後にあるアイデアが何であったかはわかりません。おそらくプログラマは、あなたが自分のクラスから新しいクラスを継承しないようにしたかったのです。なぜなら、それはコードを拡張する最良の方法ではないからです。


5
構成と継承の大きなポイント。
ゾルトTörök

+1ですが、継承の「形状は」という例は好きではありませんでした。長方形と円は、2つの非常に異なるものになる可能性があります。私はもっ​​と使うのが好きです、正方形は制約された長方形です。四角形は、図形のように使用できます。
タイラーマック

@tylermac:確かに。Square / Rectangleの場合は、実際にはLSPの反例の1つです。おそらく、私はより効果的な例を考え始める必要があります。
knulp

3
正方形/長方形は、変更を許可する場合の反例にすぎません。
スターブルー

11

ほとんどの答えは正解です。オブジェクトが拡張されるように設計されていない、または意図されていない場合は、クラスを封印する必要があります(最終、NotOverridableなど)。

ただし、適切なコード設計をすべて単純に閉じることは考えません。SOLIDの「O」は「Open-Closed Principle」を表しており、クラスは変更に対して「クローズ」し、拡張に対しては「オープン」にする必要があることを示しています。アイデアは、オブジェクトにコードがあるということです。それはうまく機能します。機能を追加するには、そのコードを開いて、以前は動作していた動作を中断する可能性のある外科的変更を行う必要はありません。代わりに、継承または依存性注入を使用して、クラスを「拡張」して追加の処理を実行できる必要があります。

たとえば、クラス「ConsoleWriter」はテキストを取得し、コンソールに出力します。これはうまくいきます。ただし、特定の場合には、ファイルに書き込まれた同じ出力も必要です。ConsoleWriterのコードを開き、外部インターフェイスを変更してパラメーター「WriteToFile」をメイン関数に追加し、コンソール書き込みコードの隣に追加のファイル書き込みコードを配置することは、一般に悪いことと見なされます。

代わりに、次の2つのいずれかを実行できます。ConsoleWriterから派生してConsoleAndFileWriterを形成し、Write()メソッドを拡張して最初に基本実装を呼び出し、次にファイルに書き込むこともできます。または、ConsoleWriterからインターフェイスIWriterを抽出し、そのインターフェイスを再実装して2つの新しいクラスを作成できます。FileWriter、および他のIWriterを指定できる「MultiWriter」。指定されたすべてのライターへのメソッド呼び出しを「ブロードキャスト」します。どちらを選択するかは、必要なものによって異なります。単純な派生はまあ、より簡単ですが、最終的にネットワーククライアントにメッセージを送信する必要があるというわずかな先見性がある場合は、3つのファイル、2つの名前付きパイプ、およびコンソール、先に進み、インターフェイスを抽出して「Yアダプター」; それ'

さて、Write()関数が仮想宣言されていない(またはシールされている)場合、ソースコードを制御しないと問題が発生します。場合によっては。これは一般的に悪い立場であり、クローズドソースまたはリミテッドソースのAPIのユーザーをいらいらさせます。ただし、Write()、またはクラスConsoleWriter全体が封印される正当な理由があります。保護されたフィールドに機密情報が存在する場合があります(この情報を提供するために基本クラスから順番にオーバーライドされます)。検証が不十分な場合があります。apiを作成するとき、コードを使用するプログラマは、最終的なシステムの平均的な「エンドユーザー」より賢くも慈悲深くもないと想定する必要があります。そうすれば、決して失望することはありません。


いい視点ね。継承は良い場合も悪い場合もあるため、すべてのクラスを封印する必要があると言うのは誤りです。本当に手元の問題に依存します。
knulp

2

おそらく、cプログラミングにoo言語を使用しているすべての人からだと思います。私はあなたを見ています、java。ハンマーがオブジェクトのような形をしているが、手続き型システムが必要な場合、および/または実際にクラスを理解していない場合は、プロジェクトをロックダウンする必要があります。

基本的に、オブジェクト指向言語で書く場合、プロジェクトは拡張機能を含むオブジェクト指向パラダイムに従う必要があります。もちろん、誰かが別のパラダイムでライブラリを書いた場合、とにかくそれを拡張することになっていないかもしれません。


7
BTW、OO、および手続き型は、相互に排他的ではありません。手順なしでオブジェクト指向を作成することはできません。また、構成は拡張と同じくらいオブジェクト指向の概念です。
マイケルK

もちろん、@ Michaelは違います。私は、手続き型システム、つまりオブジェクト指向の概念のないシステムが必要かもしれないと述べました。私は人々が純粋に手続き型のコードを書くためにJavaを使用するのを見てきましたが、オブジェクト指向のやり方でそれとやり取りしようとするとうまくいきません。
スペンサー

1
Javaにファーストクラスの機能があれば、それは解決できます。えー
マイケルK

JavaやDOTNet言語を新入生に教えることは困難です。私は通常、Procedural Pascalまたは「Plain C」でProceduralを教え、後でObject Pascalまたは「C ++」でOOに切り替えます。そして、後でJavaを使用します。手続き型プログラムでプログラミングを教えることは、単一の(シングルトン)オブジェクトがうまく機能するように見えます。
umlcat

@Michael K:OOPでは、ファーストクラス関数は、メソッドが1つだけのオブジェクトです。
ジョルジオ

2

彼らはそれ以上何も知らないからです。

原作者は、混乱し複雑なC ++の世界に由来する固体原理の誤解に固執しているでしょう。

ruby、python、perlの世界には、ここでの回答が封印の理由であると主張する問題がないことにお気づきになることを願っています。動的型付けに直交することに注意してください。アクセス修飾子は、ほとんどの(すべての)言語で簡単に機能します。C ++フィールドは、他の型にキャストすることでいじることができます(C ++はより弱い)。JavaとC#はリフレクションを使用できます。アクセス修飾子を使用すると、本当に必要な場合を除き、実行できないようになります。

クラスを封印してメンバーをプライベートにすることは、単純なことは単純であり、困難なことは可能であるという原則に明示的に違反します。突然、単純であるべきことはそうではありません。

原作者の視点を理解することをお勧めします。その多くは、現実の世界で絶対的な成功を示したことのない、カプセル化の学術的なアイデアに由来しています。どこかで開発者が少し違った動作を望み、それを変更する正当な理由がないフレームワークやライブラリを見たことがありません。メンバーを封印し、プライベートにした元のソフトウェア開発者を悩ませた可能性のある可能性が2つあります。

  1. Ar慢-彼らは本当に彼らが拡張のために開いていて、修正のために閉じられていると信じていました
  2. 自己満足-他のユースケースが存在する可能性があることを知っていましたが、それらのユースケースについては記述しないことにしました

私は、企業の枠組みの中で、#2がおそらくそうだと思います。これらのC ++、Java、および.NETフレームワークは「完了」する必要があり、特定のガイドラインに従う必要があります。通常、これらのガイドラインは、型が型階層の一部として明示的に設計され、他のユーザーが使用できる多くのことのためにプライベートメンバーとして設計されていない限り、封印された型を意味します。 。新しいタイプの抽出は、サポート、文書化などに費用がかかりすぎます...

アクセス修飾子の背後にある全体的な考えは、プログラマーが自分自身から保護されるべきだということです。「Cプログラミングは、自分で足を踏み入れることができるので、悪いです。」プログラマーとして私が同意するのは哲学ではありません。

私はpythonの名前のマングリングアプローチを非常に好みます。必要に応じて、プライベートを簡単に(リフレクションよりもはるかに簡単に)置き換えることができます。これに関するすばらしい記事はこちらから入手できます:http ://bytebaker.com/2009/03/31/python-properties-vs-java-access-modifiers/

Rubyのprivate修飾子は、実際にはC#で保護されているようなもので、C#修飾子としてのprivateはありません。保護は少し異なります。ここに素晴らしい説明があります:http ://www.ruby-lang.org/en/documentation/ruby-from-other-languages/

静的言語は、その言語で書かれたコードの時代遅れのスタイルに準拠する必要はありません。


5
「カプセル化は絶対的な成功を示したことはありません」。カプセル化の欠如が多くの問題を引き起こしたことに誰もが同意すると思っていただろう。

5
-1。愚か者たちは、天使たちが踏むのを恐れるところに突入します。プライベート変数を変更する機能を祝うことは、コーディングスタイルに重大な欠陥があることを示しています。
リウォーク

2
議論の余地があり、おそらく間違っていることは別として、この投稿はまた非常にand慢でin辱的です。よくできました。
コンラッドルドルフ

1
支持者がうまくいかないように思われる思考の2つの学校があります。静的型付けの人はソフトウェアについて簡単に推論したいのに対して、動的型の人は機能を簡単に追加したいのです。基本的にどちらが良いかは、プロジェクトの予想寿命によって異なります。
ジョー

1

編集:クラスはオープンになるように設計する必要があると思います。いくつかの制限はありますが、継承のために閉じてはいけません。

理由:「最終クラス」または「密閉クラス」は奇妙に思えます。自分のクラスの1つを「最終」(「封印済み」)としてマークする必要はありません。後でそのクラスを継承する必要があるかもしれないからです。

クラス(ほとんどのビジュアルコントロール)を含むサードパーティのライブラリを購入/ダウンロードしましたが、それらのライブラリのどれも「最終」を使用していませんでした。ソースコードなしでライブラリをコンパイルしたとしても。

時々、私はいくつかの「封印された」クラスを扱う必要があり、同様のメンバーを持つ特定のクラスを含む新しいクラス「ラッパー」の作成を終了しました。

class MyBaseWrapper {
  protected FinalClass _FinalObject;

  public virtual DoSomething()
  {
    // these method cannot be overriden,
    // its from a "final" class
    // the wrapper method can  be open and virtual:
    FinalObject.DoSomething();
  }
} // class MyBaseWrapper


class MyBaseWrapper: MyBaseWrapper {

  public override DoSomething()
  {
    // the wrapper method allows "overriding",
    // a method from a "final" class:
    DoSomethingNewBefore();
    FinalObject.DoSomething();
    DoSomethingNewAfter();
  }
} // class MyBaseWrapper

スコープ分類子を使用すると、継承を制限せずに一部の機能を「封印」できます。

構造化プログラムから切り替えたとき。OOPでは、プライベートクラスメンバーの使用を開始しましたが、多くのプロジェクトの後、protectedの使用を終了し、最終的にこれらのプロパティまたはメソッドを必然的にpublicに昇格させました。

PS私は、C#のキーワードとして「最終」が好きではありません。むしろ、Javaのような「封印された」、または継承のメタファーに従って「無菌」を使用します。また、「最終」はいくつかの文脈で使用される曖昧な言葉です。


-1:あなたはオープンなデザインが好きだと言いましたが、これは質問に答えません。だからクラスがオープンになるようにデザインされるべきではないのです。

@Joh申し訳ありませんが、私は自分自身をうまく説明しませんでした、反対意見を表明しようとしました、封印されるべきではありません。同意しない理由を説明していただきありがとうございます。
umlcat
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.