クラスが「抽象」または「最終/封印」以外のものである必要があるのはなぜですか?


29

10年以上のjava / c#プログラミングの後、次のいずれかを作成しています。

  • 抽象クラス:そのままインスタンス化されることを意図していないコントラクト。
  • final / sealedクラス:実装は、他の何かの基本クラスとして機能することを意図していません。

単純な「クラス」(つまり、抽象的でもファイナル/シールドでもない)が「賢明なプログラミング」になる状況は考えられません。

クラスが「抽象」または「最終/封印」以外のものである必要があるのはなぜですか?

編集

このすばらしい記事は、私の懸念を、私ができる以上によく説明しています。


21
Open/Closed principleではなく、と呼ばれているためClosed Principle.
StuperUser

2
あなたはプロとしてどのようなことを書いていますか?それは問題に関するあなたの意見に非常によく影響するかもしれません。

継承インターフェイスを最小限に抑えたいため、すべてを封印するプラットフォーム開発者を知っています。
K ...

4
@StuperUser:それは理由ではありません、それは礼儀です。OPは、プラティチュードが何であるかを尋ねているのではなく、その理由を尋ねています。原則の背後に理由がなければ、それに注意を払う理由はありません。
マイケルショー

1
Windowクラスをシールドに変更すると、いくつのUIフレームワークが壊れるのでしょうか。
Reactgular

回答:


46

皮肉なことに、私は反対を見つけます。抽象クラスの使用は規則ではなく例外であり、私は最終クラス/密閉クラスに眉をひそめる傾向があります。

インターフェイスは、内部構造を指定しないため、より典型的な契約ごとの設計メカニズムです。心配する必要はありません。これにより、その契約のすべての実装を独立させることができます。これは多くのドメインで重要です。たとえば、ORMを構築する場合、データベースにクエリを一定の方法で渡すことができることが非常に重要ですが、実装はまったく異なる場合があります。この目的で抽象クラスを使用すると、すべての実装に適用される場合と適用されない場合があるコンポーネントでハードワイヤリングが行われます。

final / sealedクラスの場合、それらを使用することでわかる唯一の言い訳は、オーバーライドを許可することが実際に危険な場合です(暗号化アルゴリズムなど)。それ以外は、ローカルの理由で機能をいつ拡張したいかはわかりません。クラスを封印すると、ほとんどのシナリオに存在しないゲインのオプションが制限されます。後で拡張できるようにクラスを記述する方がはるかに柔軟です。

この後者の見方は、クラスを封印するサードパーティのコンポーネントと連携することにより、私にとっては強固なものになりました。


17
最後の段落の+1。サードパーティのライブラリ(または標準ライブラリクラス)でカプセル化が多すぎると、カプセル化が少なすぎるよりも痛みの大きな原因になることがよくあります。
メイソンウィーラー

5
クラスをシールするための通常の引数は、クライアントがクラスをオーバーライドする可能性のあるさまざまな方法を予測できないため、その動作について保証することはできないということです。参照してくださいblogs.msdn.com/b/ericlippert/archive/2004/01/22/...
ロバート・ハーヴェイ

12
@RobertHarvey私はこの議論に精通しており、理論的には素晴らしいと思います。オブジェクトデザイナーとしてあなたの人々があなたのクラスを拡張することができる方法をforseeすることはできません-彼らはすべき理由を正確であることではない密閉すること。すべてをサポートすることはできません-罰金。しないでください。ただし、選択肢を奪わないでください。
マイケル

6
@RobertHarveyは、LSPを破る人々だけが彼らにふさわしいものを手に入れているということではありませんか?
StuperUser

2
私も封印されたクラスの大ファンではありませんが、Microsoftのような一部の企業(頻繁に自分自身を壊さないものをサポートしなければならない)が魅力的である理由を理解できました。フレームワークのユーザーは、必ずしもクラスの内部の知識を持っているとは限りません。
ロバートハーヴェイ

11

これ Eric Lippertによる素晴らしい記事ですが、あなたの視点をサポートするとは思いません。

彼は、他の人使用するために公開されているすべてのクラスを封印するか、そうでなければ拡張不可能にする必要があると主張します

あなたの前提は、すべてのクラスが抽象または封印されている必要があるということです。

大きな違い。

ELの記事には、あなたと私が何も知らない彼のチームによって作成された(おそらく多くの)クラスについて何も書かれていません。一般に、フレームワーク内で公開されているクラスは、そのフレームワークの実装に関係するすべてのクラスのサブセットにすぎません。


ただし、パブリックインターフェイスの一部ではないクラスに同じ引数を適用できます。クラスをfinalにする主な理由の1つは、それがずさんなコードを防ぐための安全対策として機能することです。その引数は、時間の経過とともに異なる開発者によって共有されるコードベース、またはあなたが唯一の開発者であっても同様に有効です。コードのセマンティクスを保護するだけです。
DPM

クラスは抽象的または封印されるべきだという考えは、より広範な原則の一部であり、インスタンス化可能なクラス型の変数の使用を避けるべきだという考えです。このような回避により、特定のタイプのすべてのプライベートメンバーを継承する必要なく、特定のタイプのコンシューマーが使用できるタイプを作成できます。残念ながら、そのような設計は実際にはパブリックコンストラクター構文では機能しません。代わりに、コードはのnew List<Foo>()ようなものに置き換える必要がありList<Foo>.CreateMutable()ます。
supercat

5

Javaの観点からは、最終クラスは見た目ほどスマートではないと思います。

多くのツール(特にAOP、JPAなど)は読み込み時間の変動に対応しているため、クラスを拡張する必要があります。もう1つの方法は、デリゲート(.NETのものではない)を作成し、すべてを元のクラスに委任します。これは、ユーザークラスを拡張するよりも面倒です。


5

バニラ、非シールクラスが必要になる2つの一般的なケース:

  1. 技術:2レベル以上の階層がある場合、中間でインスタンス化できるようにしたい場合。

  2. 原則:安全に拡張されるように明示的に設計されたクラスを記述することが望ましい場合があります。(これは、Eric LippertのようなAPIを書いているとき、または大規模なプロジェクトでチームで作業しているときによく起こります)。時にはそれ自体でうまく機能するクラスを書きたいが、拡張性を念頭に置いて設計されています。

Eric Lippertのシーリングに関する考え方は理にかなっていますが、クラスを「オープン」のままにして拡張性を考慮した設計を行っていることも認めています

はい、多くのクラスはBCLで​​封印されていますが、膨大な数のクラスは封印されておらず、あらゆる素晴らしい方法で拡張できます。頭に浮かぶ1つの例は、Control継承を介してほとんどすべてにデータまたは動作を追加できるWindowsフォームです。もちろん、これは他の方法(装飾パターン、さまざまなタイプの構成など)で行うこともできますが、継承も非常にうまく機能します。

2つの.NET固有の注意:

  1. virtual明示的なインターフェイスの実装を除き、継承者が非機能を混乱させることはできないため、ほとんどの場合、クラスの安全性は安全性にとって重要ではありません。
  2. internalクラスをシールするのではなく、コンストラクターを作成することで、コードベースの外部ではなく継承できるようにすることが適切な場合があります。

4

私は多くの人がクラスのあるべき感じる本当の理由は信じてfinal/ sealedその最も非抽象拡張クラスがされているではない、適切に文書化

詳しく説明させてください。遠くから始めて、OOPのツールとしての継承が広く使われ、乱用されているという意見が一部のプログラマーにあります。私たちは皆、リスコフ置換の原則を読みましたが、これは何百回(おそらくは数千回)違反することを止めるものではありませんでした。

問題は、プログラマーがコードを再利用することです。それがそんなに良い考えではない時でさえ。そして、継承はこのコードの「改ざん」における重要なツールです。最終/封印された質問に戻ります。

final / sealedクラスの適切なドキュメントは比較的小さく、各メソッドの動作、引数、戻り値などを記述します。すべての通常のもの。

ただし、拡張可能クラス適切に文書化する場合は、少なくとも次のものを含める必要があります

  • メソッド間の依存関係(どのメソッドがどのメソッドを呼び出すかなど)
  • ローカル変数への依存関係
  • 拡張クラスが尊重すべき内部契約
  • 各メソッドの呼び出し規則(たとえば、オーバーライドするとき、super実装を呼び出しますか?メソッドの最初に呼び出しますか、それとも最後に呼び出しますか?コンストラクターとデストラクターを考えます)
  • ...

これらは私の頭上にあります。そして、これらのそれぞれが重要であり、それをスキップすると拡張クラスが台無しになる理由の例を提供できます。

次に、これらの各事項を適切に文書化するためにどれだけの文書化努力が必要かを検討します。7〜8個のメソッド(理想化された世界では多すぎるかもしれませんが、実際には少なすぎる)を持つクラスには、テキストのみの5ページに関するドキュメントも含まれていると思います。したがって、代わりに、他の人が使用できるようにクラスを途中で閉じて封印しませんが、巨大な時間がかかるため、適切に文書化しないでくださいとにかく延長されることはありませんので、なぜわざわざ?

クラスを設計している場合、あなたが予見していない(そして準備していない)方法で人々がそれを使用できないように、それを封印する誘惑を感じるかもしれません。一方、他の人のコードを使用している場合、パブリックAPIからは、クラスが最終的なものであるという明らかな理由がない場合があります。

ソリューションの要素には次のものがあると思います。

  • ことを確認する最初の拡張は良いアイデアですあなたは、コードのクライアントにいるとき、およびするために実際に相続以上の組成を好みます。
  • 次に、マニュアルを完全に(クライアントとして)読んで、言及されていることを見落とさないようにします。
  • 第三に、クライアントが使用するコードの一部を作成するときは、コードの適切なドキュメントを作成します(そう、長い道のりです)。良い例として、AppleのiOSドキュメントを提供できます。常に適切に自分のクラスを拡張するために彼らは、ユーザーのためには十分ではないですが、彼らは少なくとも含まれ、いくつかの継承についての情報を。これは、ほとんどのAPIで言える以上のことです。
  • 第4に、実際にクラスを拡張して、機能することを確認してください。私はAPIに多くのサンプルとテストを含めることを大いに支持しています。テストを行うとき、継承チェーンもテストするかもしれません。それらは結局あなたの契約の一部です!
  • 第5に、疑わしい状況では、クラスが拡張されることを意図しておらず、それを行うのは悪い考え(tm)であることを示します。そのような意図しない使用については説明責任を負わせてはいけませんが、それでもクラスを封印しないでください。明らかに、これはクラスを100%封印する必要がある場合をカバーしません。
  • 最後に、クラスを封印するとき、中間フックとしてインターフェースを提供します。これにより、クライアントはクラスの独自の修正バージョンを「書き換え」、「封印された」クラスを回避できます。この方法で、彼は封印されたクラスを実装に置き換えることができます。これは、最も単純な形式では疎結合であるため、これは明らかなはずですが、それでも言及する価値があります。

また、次の「哲学的」問題に言及する価値がある:クラスがあるかどうかであるsealed/ finalクラスの契約、または実装の詳細の一部?今、私はそこを踏み込みたくありませんが、これに対する答えは、クラスを封印するかどうかのあなたの決定にも影響を与えるはずです。


3

次の場合、クラスはfinal / sealedまたはabstractであってはなりません。

  • それはそれ自体で有用です。つまり、そのクラスのインスタンスを持つことは有益です。
  • そのクラスが他のクラスのサブクラス/ベースクラスであると便利です。

たとえばObservableCollection<T>、C#クラスを使用します。aの通常の操作にイベントの発生を追加するだけでよいCollection<T>ため、サブクラス化されCollection<T>ます。Collection<T>自分自身で実行可能なクラスがあり、そしてそれそうObservableCollection<T>


私がこれを担当していた場合、おそらくCollectionBase(抽象)、Collection : CollectionBase(封印)、ObservableCollection : CollectionBase(封印)を行います。あなたが見ればコレクション<T>密接に、あなたはそれが中途半端抽象クラスです表示されます。
ニコラスレピケ

2
クラスを2つだけではなく3つにすることで、どのような利点が得られますか?また、Collection<T>「中途半端な抽象クラス」はどうですか?
-FishBasketGordo

Collection<T>保護されたメソッドとプロパティを介して多くの内部を公開し、明らかに特殊なコレクションの基本クラスであることを意図しています。しかし、実装する抽象メソッドがないため、コードを配置する場所が明確ではありません。ObservableCollectionから継承されCollection、封印されていないため、再び継承できます。また、Items保護されたプロパティにアクセスして、イベントを発生させることなくコレクションにアイテムを追加できます。
ニコラスレピケ

@NicolasRepiquet:多くの言語とフレームワークでは、オブジェクトを作成する通常の慣用的な手段では、オブジェクトを保持する変数の型が、作成されたインスタンスの型と同じである必要があります。多くの場合、理想的な使用法は抽象型への参照を渡すことですが、それにより多くのコードがコンストラクターを呼び出すときに変数とパラメーターに1つの型を使用し、異なる具体的な型を使用することになります。ほとんど不可能ではありませんが、少し厄介です。
supercat

3

final / sealedクラスの問題は、まだ発生していない問題を解決しようとしていることです。問題が存在する場合にのみ役立ちますが、サードパーティが制限を課しているため、イライラします。シーリングクラスが現在の問題を解決することはめったにないため、その有用性を主張することは困難です。

クラスを封印する必要がある場合があります。例として; クラスは、将来の変更がその管理をどのように変更するかを予測できない方法で、割り当てられたリソース/メモリを管理します。

長年にわたって、カプセル化、コールバック、およびイベントは、抽象化されたクラスよりもはるかに柔軟/便利であることがわかりました。カプセル化とイベントによって開発者の生活が簡素化されたクラスの大きな階層を持つはるかに多くのコードを見ています。


1
ソフトウェアで最終的に封印されたものは、「このコードの将来のメンテナーに自分の意思を課す必要性を感じている」という問題を解決します。
カズ

OOPに追加された最終的なものではありませんでした。それは、ある種の機能のように思えます。
Reactgular

OOPがC ++やJavaのような言語でだまされる前に、OOPシステムではツールチェーンプロシージャとしてファイナライズが存在していました。プログラマは、最大限の柔軟性を備えたLispのSmalltalkで作業します。何でも拡張でき、常に新しいメソッドが追加されます。次に、システムのコンパイルされたイメージは最適化の対象となります。システムはエンドユーザーによって拡張されないという仮定が行われます。したがって、これは、どのメソッドと現在クラスが存在します
カズ

これは最適化機能にすぎないため、同じことだとは思いません。Javaのすべてのバージョンに封印されたことを覚えていませんが、あまり使用しないので間違っている可能性があります。
Reactgular

コードに入れなければならない宣言として現れるからといって、同じものではないというわけではありません。
カズ

2

完全なディスパッチグラフが既知であるため、オブジェクトシステムの「シーリング」または「ファイナライズ」により、特定の最適化が可能になります。

つまり、パフォーマンスのトレードオフとして、システムを他の何かに変更することを困難にします。(それがほとんどの最適化の本質です。)

他のすべての点で、それは損失です。システムはデフォルトでオープンで拡張可能である必要があります。すべてのクラスに新しいメソッドを簡単に追加し、任意に拡張する必要があります。

将来の拡張を防ぐための措置を講じることにより、現在、新しい機能を取得することはありません。

したがって、予防自体のためにそれを行う場合、私たちがしていることは、将来のメンテナーの生活を制御しようとすることです。それは自我についてです。「ここで仕事をしなくなっても、このコードはそのまま維持されます。


1

テストクラスが思い浮かぶようです。これらは、プログラマー/テスターが達成しようとしていることに基づいて、自動化された方法で、または「任意に」呼び出されるクラスです。プライベートなテストの完成したクラスを見たことも聞いたこともありません。


何を言ってるの?どのようにテストクラスを最終/封印/抽象化してはいけませんか?

1

拡張することを意図したクラスを検討する必要があるのは、将来のために実際の計画を立てているときです。私の仕事から実際の例を挙げましょう。

私は、メイン製品と外部システムの間のインターフェースツールを書くことに多くの時間を費やしています。新規販売を行う場合、1つの大きなコンポーネントは、その日に発生したイベントの詳細を示すデータファイルを生成する定期的な間隔で実行されるように設計されたエクスポーターのセットです。これらのデータファイルは、顧客のシステムによって消費されます。

これは、クラスを拡張する絶好の機会です。

すべてのエクスポーターの基本クラスであるエクスポートクラスがあります。データベースへの接続方法、前回実行した場所を確認し、生成したデータファイルのアーカイブを作成する方法を知っています。また、プロパティファイルの管理、ロギング、およびいくつかの簡単な例外処理も提供します。

さらに、各タイプのデータを扱う異なるエクスポーターがあります。おそらく、ユーザーアクティビティ、トランザクションデータ、キャッシュ管理データなどがあります。

このスタックの上に、顧客が必要とするデータファイル構造を実装する顧客固有のレイヤーを配置します。

このようにして、ベースエクスポーターが変更されることはほとんどありません。コアデータ型エクスポーターは時々変更されますが、めったに変更されず、通常はデータベーススキーマの変更を処理するだけであり、すべての顧客に伝達される必要があります。各顧客に対して私がしなければならない唯一の仕事は、その顧客に固有のコードのその部分です。完璧な世界!

したがって、構造は次のようになります。

Base
 Function1
  Customer1
  Customer2
 Function2
 ...

私の主なポイントは、このようにコードを設計することにより、主にコードの再利用に継承を利用できることです。

3層を超える理由を考えることはできないと言わざるを得ません。

たとえば、Tableデータベーステーブルクエリを実装する共通クラスを持ち、サブクラスがフィールドを定義するためにTableを使用しenumて各テーブルの特定の詳細を実装するために、2つのレイヤーを何度も使用しました。まかせenumで定義されたインタフェースを実装しTableたクラスは、感覚のすべての種類になります。


1

抽象クラスは有用ですが、必ずしも必要というわけではないことがわかりました。また、単体テストを適用する場合、封印クラスが問題になります。Teleriks justmockのようなものを使用しない限り、シールクラスをモックまたはスタブすることはできません。


1
これは良いコメントですが、質問に直接答えません。ここで考えを広げることを検討してください。

1

あなたの意見に同意します。Javaでは、デフォルトでクラスは「最終」と宣言されるべきだと思います。最終決定しない場合は、具体的に準備し、拡張用に文書化します。

これの主な理由は、クラスのインスタンスが、最初に設計および文書化されたインターフェースを順守するようにするためです。そうしないと、クラスを使用する開発者が脆弱で一貫性のないコードを作成し、そのコードを他の開発者/プロジェクトに渡して、クラスのオブジェクトを信頼できないものにする可能性があります。

ライブラリのインターフェイスのクライアントは、元々考えていたより柔軟な方法でクラスを調整したり使用したりすることができないため、より実用的な観点からは実際にこれにはマイナス面があります。

個人的には、存在するすべての品質の悪いコードについて(そして、より実用的なレベルでこれを議論しているので、Java開発はこれになりやすいと主張します)、この厳格さは、コードを維持します。

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