背景:
Javaプログラマーとして、私はインターフェイスから広範囲に継承(むしろ:実装)し、抽象基底クラスを設計することもあります。しかし、具体的な(非抽象)クラス(私がそれを行った場合、後で delegation などの別のソリューションの方が優れていることがわかりました).
そのため、具象クラスからの継承が適切である状況はほとんどないと感じ始めています。 1つには、 Liskov置換原理 (LSP)は、自明でないクラスを満足させることはほとんど不可能のようです。また、 多数 その他 質問 ここでも同様の意見が反映されているようです。
だから私の質問:
具象クラスから継承することが実際に意味があるのは、どのような状況(ある場合)ですか?別の具象クラスから継承する実際のクラスの具体例を挙げてもらえますか。制約がある場合、これが最良の設計だと思いますか? LSPを満たす例(またはLSPを満たすことが重要ではないと思われる例)に特に興味があります。
私は主にJavaの背景がありますが、どの言語の例にも興味があります。
インターフェースI
のスケルトン実装がよくあります。抽象メソッドなしで(フックなどを介して)拡張性を提供できる場合は、インスタンス化できるため、非抽象スケルトンクラスを使用することをお勧めします。
転送ラッパークラスの例は、具象クラスの別のオブジェクトに転送できるようにするC
実装I
、例えば有効にする 装飾 またはC
から継承する必要のない、C
の単純なコード再利用。そのような例は Effective Java item 16にあり、継承よりも構成を優先します。 (著作権のためにここに投稿したくありませんが、I
のすべてのメソッド呼び出しをラップされた実装に転送するだけです)。
以下は、適切な場合の良い例だと思います。
public class LinkedHashMap<K,V>
extends HashMap<K,V>
別の良い例は、例外の継承です。
public class IllegalFormatPrecisionException extends IllegalFormatException
public class IllegalFormatException extends IllegalArgumentException
public class IllegalArgumentException extends RuntimeException
public class RuntimeException extends Exception
public class Exception extends Throwable
私が考えることができる非常に一般的なケースの1つは、フォーム、テキストボックス、コンボボックスなどの基本的なUIコントロールから派生することです。これらは完全で、具体的で、独自に立つことができます。ただし、それらのほとんどは非常に基本的なものであり、デフォルトの動作が希望どおりにならない場合があります。たとえば、完全に動的なUIレイヤーを作成していない限り、実質的に誰もが純粋なフォームのインスタンスを使用することはありません。
たとえば、最近開発したソフトウェアで、比較的成熟している(つまり、主に開発に集中するのに時間を使い果たしたということです))、ComboBoxに「遅延読み込み」機能を追加する必要があることがわかりました。 t最初のウィンドウがロードされるまでに50年(コンピューター年で)かかります。また、あるComboBoxで使用可能なオプションを別のComboBoxに表示されているものに基づいて自動的にフィルタリングする機能も必要でした。最後に、別の編集可能なコントロールで1つのComboBoxの値を「ミラーリング」して、1つのコントロールで変更を行う方法が必要でした他にも。そこで、基本的なComboBoxを拡張してこれらの追加機能を提供し、LazyComboBoxと、さらにMirroringComboBoxという2つの新しいタイプを作成しました。どちらも完全にサービス可能な具体的なComboBoxコントロールに基づいており、いくつかの動作をオーバーライドして、他の動作をいくつか追加しています。これらはあまり疎結合ではなく、したがってSOLIDでもありませんが、追加された機能は十分に汎用的であり、必要に応じて、これらのクラスのいずれかを最初から書き直して同じ作業を行うことができます。
一般的に言って、私が具象クラスから派生するのは、それらがフレームワーク内にあるときだけです。 Applet
またはJApplet
からの派生は、自明な例です。
これは、私が着手している現在の実装の例です。
OAuth 2環境では、ドキュメントはドラフト段階でstillであるため、仕様は変化し続けます(現時点で)執筆中、バージョン21です。
したがって、さまざまなアクセストークンに対応するために、具体的なAccessToken
クラスを拡張する必要がありました。
以前のドラフトでは、token_type
フィールドが設定されていなかったため、実際のアクセストークンは次のとおりです。
public class AccessToken extends OAuthToken {
/**
*
*/
private static final long serialVersionUID = -4419729971477912556L;
private String accessToken;
private String refreshToken;
private Map<String, String> additionalParameters;
//Getters and setters are here
}
今、token_type
を返すアクセストークンを使用して、
public class TokenTypedAccessToken extends AccessToken {
private String tokenType;
//Getter and setter are here...
}
したがって、両方を返すことができ、エンドユーザーは賢明ではありません。 :-)
要約:具象クラスの構造を変更せずに具象クラスと同じ機能を持つカスタマイズされたクラスが必要な場合は、具象クラスを拡張することをお勧めします。
その他の使用例は、デフォルトの動作を上書きすることです:
解析に標準のJaxbパーサーを使用するクラスがあるとしましょう
public class Util{
public void mainOperaiton(){..}
protected MyDataStructure parse(){
//standard Jaxb code
}
}
ここで、解析操作にいくつかの異なるバインディング(たとえば、XMLBean)を使用したいとします。
public class MyUtil extends Util{
protected MyDataStructure parse(){
//XmlBean code code
}
}
これで、スーパークラスのコード再利用で新しいバインディングを使用できます。
私は主にJavaバックグラウンドを持っていますが、どの言語の例にも興味があります。
多くのフレームワークと同様に、ASP.NETは継承を多用してクラス間で動作を共有します。たとえば、 HtmlInputPassword には次の継承階層があります。
System.Object
System.Web.UI.Control
System.Web.UI.HtmlControls.HtmlControl // abstract
System.Web.UI.HtmlControls.HtmlInputControl // abstract
System.Web.UI.HtmlControls.HtmlInputText
System.Web.UI.HtmlControls.HtmlInputPassword
派生元の具体的なクラスの例を見ることができます。
フレームワークを構築していて、それを実行したいと確信している場合は、ニースの大きな継承階層が必要であることに気付くでしょう。
decorator pattern は、クラスを一般的になりすぎずに追加の動作を追加する便利な方法であり、具象クラスの継承を多用します。これは既にここで言及されていますが、「転送ラッパークラス」という科学的な名前で呼ばれています。
答えはたくさんありますが、自分で$ 0.02を追加します。
まれに、特定の状況下でconcreateクラスをオーバーライドします。フレームワーククラスが拡張されるように設計されている場合、少なくとも1つはすでに言及されています。いくつかの例で2つの追加のものが思い浮かびます:
1)具体的なクラスの動作を微調整したい場合。具象クラスの動作を変更したい場合や、特定のメソッドが呼び出されたときに何かをトリガーできるようにしたい場合があります。多くの場合、具象クラスは、サブクラスがメソッドをオーバーライドするための唯一の使用法であるフックメソッドを定義します。
例:JMX Beanの登録を解除できる必要があるため、MBeanExporter
をオーバーライドします。
public class MBeanRegistrationSupport {
// the concrete class has a hook defined
protected void onRegister(ObjectName objectName) {
}
私たちのクラス:
public class UnregisterableMBeanExporter extends MBeanExporter {
@Override
protected void onUnregister(ObjectName name) {
// always a good idea
super.onRegister(name);
objectMap.remove(name);
}
ここに別の良い例があります。 LinkedHashMap
は removeEldestEntry
メソッドがオーバーライドされるように設計されています。
private static class LimitedLinkedHashMap<K, V> extends LinkedHashMap<K, V> {
@Override
protected boolean removeEldestEntry(Entry<K, V> eldest) {
return size() > 1000;
}
2)クラスが機能の一部の微調整を除いて、具象クラスとかなりの量の重複を共有する場合。
例:My ORMLite プロジェクトは、永続的なLong
オブジェクトフィールドとlong
プリミティブフィールドを処理します。どちらもほぼ同じ定義です。 LongObjectType
は、データベースがlong
フィールドを処理する方法を説明するすべてのメソッドを提供します。
public class LongObjectType {
// a whole bunch of methods
while LongType
は LongObjectType
をオーバーライドし、プリミティブを処理するという単一のメソッドを微調整するだけです。
public class LongType extends LongObjectType {
...
@Override
public boolean isPrimitive() {
return true;
}
}
お役に立てれば。
具象クラスの継承は、サイドライブラリ機能を拡張する場合の唯一のオプションです。
実際の使用例として、FilterInputStreamのDataInputインターフェイスを実装するDataInputStreamの階層を見ることができます。
あなたの懸念は、あなたが述べた理由のために、古典的な原則 " 継承よりも構成を好む "にも反映されています。最後に具象クラスから継承したのを思い出せません。子クラスで再利用する必要のある一般的なコードでは、ほとんどの場合、それらのクラスの抽象インターフェースを宣言する必要があります。この順序で、私は次の戦略を優先するようにしています:
具象クラスから継承することは、決して良い考えではありません。
[編集]あなたがアーキテクチャを制御しているとき、私はこのステートメントの良いユースケースが見当たらないと言って、このステートメントを修飾します。もちろん、それを期待するAPIを使用する場合、whaddayaはどうしますか?しかし、私はそれらのAPIによる設計の選択を理解していません。呼び出し元のクラスは、常に 依存関係の逆転の原則 に従って抽象を宣言して使用できる必要があります。子クラスに使用する追加のインターフェースがある場合、DIPに違反するか、これらのインターフェースを取得するために醜いキャストを行う必要があります。
具象クラスからの継承が適切である状況はほとんどないと感じ始めています。
これは「ほとんど」の1つです。 Applet
またはJApplet
を拡張せずに applet を記述してみてください。
ここに例があります。 アプレット情報ページ から。
/* <!-- Defines the applet element used by the appletviewer. -->
<applet code='HelloWorld' width='200' height='100'></applet> */
import javax.swing.*;
/** An 'Hello World' Swing based applet.
To compile and launch:
Prompt> javac HelloWorld.Java
Prompt> appletviewer HelloWorld.Java */
public class HelloWorld extends JApplet {
public void init() {
// Swing operations need to be performed on the EDT.
// The Runnable/invokeLater() ensures that happens.
Runnable r = new Runnable() {
public void run() {
// the crux of this simple applet
getContentPane().add( new JLabel("Hello World!") );
}
};
SwingUtilities.invokeLater(r);
}
}
別の良い例は、データストレージタイプです。正確な例を示すと、赤黒ツリーはより具体的なバイナリツリーですが、データやサイズなどの他の情報の取得は同じように処理できます。もちろん、優れたライブラリにはすでに実装されているはずですが、問題に特定のデータ型を追加する必要がある場合もあります。
現在、ユーザーの行列を計算するアプリケーションを開発しています。ユーザーは、計算に影響を与える設定を提供できます。計算できる行列にはいくつかのタイプがありますが、特に構成可能性には明らかな類似点があります。行列Aは行列Bのすべての設定を使用できますが、使用できる追加のパラメーターがあります。その場合、自分のConfigObjectAのConfigObjectBから継承したので、かなりうまくいきます。
一般に、具象クラスから継承するよりも抽象クラスから継承する方が適切です。具象クラスはそのデータ表現の定義を提供する必要があり、一部のサブクラスは異なる表現が必要になります。抽象クラスはデータ表現を提供する必要がないため、将来のサブクラスは、継承したものと競合することを恐れずに任意の表現を使用できます。具体的な相続が必要だと感じたことは一度もありませんでした。ただし、ソフトウェアに下位互換性を提供している場合は、具体的な継承が必要になる場合があります。その場合、uはclass A
しかし、古いアプリケーションが使用している可能性があるため、具体的にしたい場合。
gdata project から:
com.google.gdata.client.Service は、特定のタイプのGDataサービス用にカスタマイズできる基本クラスとして機能するように設計されています。
サービスjavadoc:
Serviceクラスは、GDataサービスへのクライアント接続を表します。 GDataサーバーとのすべてのプロトコルレベルの相互作用をカプセル化し、サーバー上の操作を呼び出してその結果を処理する上位レベルのエンティティ(フィード、エントリなど)のヘルパークラスとして機能します。
このクラスは、GDataサービスにアクセスするために必要な基本レベルの共通機能を提供します。また、特定のタイプのGDataサービス用にカスタマイズできる基本クラスとして機能するように設計されています。 サポートされているカスタマイズの例は次のとおりです:
Authentication-認証を必要とし、HTTP基本認証またはダイジェスト認証以外のものを使用するサービスにカスタム認証メカニズムを実装します。
Extensions-サービスに関連付けられたフィード、エントリ、およびその他のタイプの予期される拡張を定義します。
Formats-サービスとクライアント側のパーサーとジェネレーターによって消費または生成され、それらを処理する追加のカスタムリソース表現を定義します。
ただの一般的な考え。抽象クラスに欠けているものがあります。欠落しているものがこれが各派生クラスで異なる場合、それは理にかなっています。しかし、クラスを変更したくないが、何かを追加したい場合があるかもしれません。継承するコードの重複を避けるために。また、両方のクラスが必要な場合は、具象クラスからの継承になります。
だから私の答えは、本当に何かを追加したいだけのすべての場合です。たぶん、これはあまり頻繁には起こりません。
たとえば、GUIライブラリでそれを行います。単なるComponentから継承してPanelに委譲することはあまり意味がありません。パネルから直接継承する方がはるかに簡単です。
Javaコレクションクラスは非常に良い例です。ですから、AbstractList、AbstractSet、AbstractQueueなどの子を持つAbstractCollectionがあります...この階層は適切に設計されていると思います。爆発がないことを確認してください。すべての内部静的クラスを含むCollectionsクラスがあります。