10年以上のJava/c#プログラミングの後、私は次のいずれかを作成しています。
単純な「クラス」(つまり、抽象でも最終でも封印でもない)が「賢いプログラミング」になる状況は考えられません。
クラスが「抽象」または「最終/封印」以外のものである必要があるのはなぜですか?
[〜#〜]編集[〜#〜]
この素晴らしい記事 は私の懸念を私が説明できるよりもはるかによく説明しています。
皮肉なことに、私は反対のことを見つけます。抽象クラスの使用は規則ではなく例外であり、私は最終クラス/封印されたクラスを嫌う傾向があります。
インターフェースは、内部を指定しないため、契約による設計のより一般的なメカニズムです。インターフェースについて心配する必要はありません。これにより、その契約のすべての実装を独立させることができます。これは多くのドメインで重要です。たとえば、ORMを構築している場合、データベースにクエリを統一した方法で渡すことが非常に重要ですが、実装はまったく異なる場合があります。この目的で抽象クラスを使用すると、すべての実装に適用される場合と適用されない場合があるコンポーネントのハードワイヤリングが発生します。
Final/sealedクラスの場合、これらを使用するための唯一の言い訳は、オーバーライドを許可することが実際に危険な場合(暗号化アルゴリズムなど)です。それ以外の場合は、ローカルな理由で機能を拡張する必要がある場合はわかりません。クラスをシールすると、ほとんどのシナリオでは存在しないゲインのオプションが制限されます。クラスを後で拡張できるようにクラスを記述する方がはるかに柔軟です。
この後者の見方は、クラスをシールするサードパーティのコンポーネントを使用することで私にとって固くなりました。
それはisエリックリッパートによるすばらしい記事ですが、あなたの見解を裏付けるものではないと思います。
彼は、-他の人が使用するために公開されたすべてのクラスは封印するか、そうでなければ拡張できないようにする必要があると主張しています。
あなたの前提はすべてのクラスは抽象的または封印されるべきであるということです。
大きな違い。
ELの記事は、あなたと私が何も知らない、彼のチームが作成した(おそらく多くの)クラスについては何も述べていません。一般に、フレームワーク内の公開クラスは、そのフレームワークの実装に関与するすべてのクラスのサブセットにすぎません。
Javaの観点から、最終クラスは見た目ほどスマートではないと思います。
多くのツール(特にAOP、JPAなど)はロードタイムウェイビングで機能するため、クラスを拡張する必要があります。もう1つの方法は、デリゲート(.NETではない)を作成し、すべてを元のクラスに委任することです。これは、ユーザークラスを拡張するよりもはるかに面倒です。
あなたが必要とする2つの一般的なケースバニラ、シールされていないクラス:
技術:2レベルより深い階層の階層があり、途中で何かをインスタンス化できます。
原則:安全に拡張できるように設計されたexplicityであるクラスを記述することが望ましい場合があります。 (これは、Eric LippertのようなAPIを作成しているとき、または大規模なプロジェクトのチームで作業しているときによく起こります)。単独で正常に機能するクラスを記述したい場合がありますが、拡張性を考慮して設計されています。
Eric Lippertのシーリングに関する考え方は理にかなっていますが、クラスを「オープン」のままにすることで拡張性のために設計しているdoことも認めています。
はい、多くのクラスはBCLで封印されていますが、非常に多くのクラスは封印されておらず、あらゆる種類の素晴らしい方法で拡張できます。頭に浮かぶ1つの例はWindowsフォームで、継承によってほとんどすべてのControl
にデータまたは動作を追加できます。もちろん、これは他の方法(デコレータパターン、さまざまな種類の構成など)で行うこともできますが、継承も非常にうまく機能します。
2つの.NET固有の注意事項:
virtual
機能を混乱させることができないため、クラスのシーリングは安全にとって重要ではないことがよくあります。internal
にすることで、コードベースの内部では継承できるが、外部では継承できないようにすることもできます。多くの人がクラスをfinal
/sealed
と感じる本当の理由は、ほとんどの非抽象拡張可能クラスがでない正しく文書化。
詳しく説明しましょう。一部のプログラマーの間では、OOPのツールとしての継承は、過度に使用され、悪用されているとの見方があります。これは私たちを止めることはありませんでしたが、私たちはすべてLiskov置換の原則を読みました。何百回(おそらくは数千回)の違反から。
プログラマーはコードを再利用するのが大好きです。それがそれほど良い考えではないときでも。そして継承は、このコードの「再利用」における重要なツールです。最後の密封された質問に戻ります。
Final/sealedクラスの適切なドキュメントは比較的小さく、各メソッドの機能、引数、戻り値などを記述します。通常のすべてのもの。
ただし、適切に拡張可能なクラスをドキュメント化している場合少なくとも以下を含める必要があります:
super
実装を呼び出しますか?メソッドの最初と最後のどちらで呼び出しますか?コンストラクターvsデストラクタだと思います)これらは私の頭の上にあります。そして、これらのそれぞれが重要であり、それをスキップすると拡張クラスが台無しになる理由の例を提供できます。
次に、これらの各事項を適切に文書化するためにどれだけの文書化作業を行うべきかを検討します。 7-8メソッド(理想化された世界では多すぎるかもしれませんが、実際には少なすぎる)のクラスにも、テキストのみの5ページのドキュメントがあると思います。したがって、代わりに、中途半端にダックアウトしてクラスを封印しないため、他の人がそれを使用できるようにしますが、適切に文書化しないでください。非常に時間がかかるためです(そして、とにかく延長されることはないので、なぜわざわざ?).
クラスを設計している場合、クラスを封印して、人々が予期しない(および準備されていない)方法でクラスを使用できないようにする誘惑を感じるかもしれません。一方、他の誰かのコードを使用している場合、場合によってはパブリックAPIから、クラスが最終であるという明白な理由がないため、「くそー、これは回避策を探すのに30分かかる」と考えるかもしれません。
ソリューションの要素のいくつかは次のとおりです。
また、次の「哲学的な」質問にも言及する価値があります。クラスがsealed
/final
であるかどうかは、クラスのコントラクトの一部ですか、それとも実装の詳細ですか?今、私はそこを踏みたくありませんが、これに対する答えは、クラスを封印するかどうかの決定にも影響を与えるはずです。
次の場合、クラスはfinal/sealedでもabstractでもありません。
たとえば、 ObservableCollection<T>
C#のクラス を取ります。 Collection<T>
の通常の操作にイベントの発生を追加するだけでよいので、Collection<T>
をサブクラス化します。 Collection<T>
はそれ自体で実行可能なクラスなので、ObservableCollection<T>
になります。
Final/sealedクラスの問題は、まだ発生していない問題を解決しようとしていることです。問題が存在する場合にのみ役立ちますが、サードパーティが制限を課しているため、イライラします。シーリングクラスが現在の問題を解決することはめったになく、そのため、その有用性を主張することは困難です。
クラスを封印する必要がある場合があります。例として;クラスは、割り当てられたリソース/メモリを、将来の変更によってその管理がどのように変わるかを予測できない方法で管理します。
何年にもわたって、カプセル化、コールバック、およびイベントが、抽象化されたクラスよりもはるかに柔軟で便利であることがわかりました。カプセル化とイベントによって開発者の生活がよりシンプルになったであろう、クラスの大きな階層を持つはるかに多くのコードを見ています。
完全なディスパッチグラフがわかっているため、オブジェクトシステムの「シーリング」または「ファイナライズ」により、特定の最適化が可能になります。
つまり、パフォーマンスのトレードオフとして、システムを別のものに変更することを困難にします。 (それがほとんどの最適化の本質です。)
他のすべての点で、それは損失です。システムは、デフォルトでオープンで拡張可能である必要があります。すべてのクラスに新しいメソッドを追加し、任意に拡張するのは簡単なはずです。
今日、将来の拡張を防ぐための措置を講じても、新しい機能は得られません。
したがって、予防のためにそれを行う場合、私たちがしていることは、将来のメンテナの生活を制御しようとすることです。自我についてです。 「私がここで働かなくても、このコードは私の方法で維持されます、いまいましいです!」
テストクラスが思い浮かぶようです。これらは、プログラマー/テスターが達成しようとしていることに基づいて、自動化された方法で、または「意のままに」呼び出されるクラスです。私がプライベートなテストの完成したクラスを見たことがあるか、聞いたことがあるかわかりません。
拡張する予定のクラスを検討する必要があるのは、将来の計画を実際に立てているときです。私の仕事の実際の例を挙げましょう。
主な製品と外部システム間のインターフェースツールの作成に多くの時間を費やしています。新しい販売を行う場合、1つの大きなコンポーネントは、その日に発生したイベントの詳細を示すデータファイルを生成する定期的な間隔で実行されるように設計された一連のエクスポーターです。これらのデータファイルは、お客様のシステムで使用されます。
これはクラスを拡張する絶好の機会です。
すべてのエクスポーターの基本クラスであるエクスポートクラスがあります。それはデータベースに接続する方法を知っており、それが最後に実行されたときにどこに到達したかを見つけ、それが生成するデータファイルのアーカイブを作成します。また、プロパティファイルの管理、ロギング、およびいくつかの単純な例外処理も提供します。
これに加えて、私は各タイプのデータを操作するための異なるエクスポーターを持っています。おそらく、ユーザーアクティビティ、トランザクションデータ、キャッシュ管理データなどがあります。
このスタックの上に、顧客が必要とするデータファイル構造を実装する顧客固有のレイヤーを配置します。
このように、基本エクスポーターが変更されることはほとんどありません。中核となるデータ型エクスポーターは変更されることもありますが、まれにしか行われず、通常、データベーススキーマの変更を処理するだけで、すべての顧客に伝達する必要があります。私が各顧客に対してこれまでしなければならない唯一の作業は、その顧客に固有のコードの部分です。完璧な世界!
したがって、構造は次のようになります。
Base
Function1
Customer1
Customer2
Function2
...
私の主なポイントは、このようにコードを設計することにより、主にコードの再利用のために継承を利用できることです。
3つのレイヤーを通過する理由は何も考えられません。
私は2つのレイヤーを何度も使用しました。たとえば、データベーステーブルクエリを実装する共通のTable
クラスがあり、Table
のサブクラスはenum
フィールドを定義します。 enum
にTable
クラスで定義されたインターフェースを実装させることは、あらゆる意味を成します。
抽象クラスは便利ですが、常に必要というわけではないことがわかりました。ユニットテストを適用する場合、シールクラスが問題になります。 Teleriksのジャストモックのようなものを使用しない限り、封印されたクラスをモックまたはスタブすることはできません。
私はあなたの意見に同意します。 Javaでは、デフォルトで、クラスを「最終」として宣言する必要があると思います。最終的にしない場合は、具体的に準備し、拡張のために文書化します。
これの主な理由は、クラスのインスタンスが、最初に設計および文書化されたインターフェイスに確実に準拠するようにするためです。そうしないと、クラスを使用する開発者が脆弱で一貫性のないコードを作成する可能性があり、その結果、そのコードを他の開発者/プロジェクトに渡し、クラスのオブジェクトを信頼できないものにします。
より実用的な観点からすると、ライブラリのインターフェイスのクライアントは、当初考えていたより柔軟な方法でクラスを微調整したり使用したりできないため、実際にはこれにはマイナス面があります。
個人的には、存在するすべての品質の悪いコードについて(そして、これをより実用的なレベルで議論しているので、私は議論しますJava開発はこれになりやすい)この剛性は小さいと思いますコードの保守を容易にするために私たちの探求で支払う価格。