[B..I]と言うすべてのサブクラスでサポートされる必要のある動作を含む抽象基本クラス(A)を持つ多数のクラスがあります。
私のコードでは、外部システムからの入力に基づいて作成されたオブジェクトのコレクションが作成されます。これらのオブジェクトは上記のサブクラスに属しており、私のコードでさまざまな操作を実行します。
これらのすべての操作にはコアと共通の動作が必要なので、抽象基本クラスを使用し、基本タイプ(A.DoTheThing())のみを使用してオブジェクトのコレクションのメソッドを呼び出すことができます。
問題は、これらのタイプの小さなサブセットのみが、共通の機能セットをサポートする必要があることです。何もしないかnullを返すデフォルト実装を使用してメソッドを基本クラスに追加すると、Godクラスに移動します。いくつかのサブタイプだけがこれらのメソッドをオーバーライドしますが、ベースタイプを使用してコレクションを処理し続けることができます。サブクラスの大きなグループは、基本クラスのデフォルトの空の実装に基づくメソッド呼び出しに応答して何もしません。デフォルトの動作をオーバーライドするものは、必要なことを実行します。
ほとんどが属さない動作を行いたくない場合は、インターフェイス(X)を定義して、コレクションに含まれるサブタイプの小さなサブセットに実装する必要があります。ただし、今はタイプAに基づくコレクションがあり、Aからのメソッドを使用した後のある時点で、Xを実装するオブジェクトのサブセットで操作を実行する必要があります。 instanceof(X)で、関連するメソッドを呼び出します。
どちらがより小さな悪であり、ここで別の選択肢がありますか?
悪と見なされているからといって回避しないでください。まずなぜ悪と見なされているのかを理解してから、その悪を回避する方法を決定します。
これらの邪悪なことは通常、問題を解決するための簡単で簡単な方法であるため、警告が表示されます。それ以外の場合、人々は常にそうしようとはせず、警告が冗長になります。問題は、方法Aが悪である理由を理解していない場合、方法Aとまったく同じ理由で悪である方法Bを使用することになるかもしれませんが、方法BはAより扱いにくいため、あまり一般的ではなかったためです。誰もあなたにそれを警告する必要を感じませんでした。
私はそれを見つけることができませんが、クラスのすべてのフィールドをstatic
にして、そのクラスの各新しいオブジェクトが同じ状態を使用するようにしてシングルトンを回避したことを覚えています(ただし、単一のinstance
がないため、シングルトンではありません!)
とにかく、これはそのマスター基本クラスでやっていることです。あなたの場合、ダウンキャストは「悪」ではありませんが、そのマスター基本クラスは、ダウンキャストが通常受けるのと同じ問題に悩まされています!
このことを考慮:
public abstract class Base {
// bla bla bla
}
public class ImplA extends Base {
// bla bla bla
}
public class ImplB extends Base {
// bla bla bla
}
// somewhere else
void doit(Base base) {
if (base instanceof ImplA) {
ImplA implA = (ImplA)base;
// ImplA specific code
} else if (base instanceof ImplB) {
ImplB implB = (ImplB)base;
// ImplB specific code
}
}
なぜこれが悪いのですか?ImplC
を処理しないためです。 しかしImplC
!!!はありません
まあ、ImplC
nowはありませんが、私または他の誰かが来年それを書いてextend Base
にするのを止めるものは何もありません。そして、それらはImplC
のインスタンスを作成し、そのインスタンスはdoit
に渡されます-これはおそらくそれを間違って処理します。なぜなら、ImplC
の意味とdoit
がそれをどのように処理する必要があるかはわかりませんが、doit
にImplA
の特別なコードとImplB
の特別なコードが必要な場合は、ImplC
の特別なコードが必要であると想定する必要があります。ただし、そのコードを追加できない場合があります(doit
がサードパーティのライブラリの一部である可能性があるため)。または、単純にできない場合もあります 、doit
がImplC
を処理するための特別なコードを持たないというコンパイル警告がないため。しかし、数時間/数日のデバッグの後で、プログラムが機能しない理由を理解しようとすると、やがて気付くでしょう...
これがダウンキャストが嫌われる理由であり、ポリモーフィズムとメソッドのオーバーライドを優先することをお勧めします。
public abstract class Base {
// bla bla bla
public abstract void doit();
}
public class ImplA extends Base {
// bla bla bla
@Override
public void doit() {
// ImplC specific code
}
}
public class ImplB extends Base {
// bla bla bla
@Override
public void doit() {
// ImplB specific code
}
}
この設計では、ImplC
は、doit
を独自のコードでオーバーライドすることができ、それを記述し忘れると、コンパイルエラーが発生します。
私はあなたの基本クラスが同様の問題に苦しんでいると主張します-新しい機能を追加するために基本クラスを修正する必要があります。コードにアクセスでき、基本メソッドを使用する必要があるため、基本メソッドを追加するのを忘れないので、これは最悪のケースではありません。しかし、それでも-ダウンキャストの一般的な問題に苦しむ構造を作成することで、ダウンキャストを回避しています...
doit
の問題は、Base
を処理するためのものでしたが、実際には、既知のタイプのBase
-ImplA
およびImplB
のみを処理していました。
あなたのケースは異なります。 A
のコレクションをループして、たとえばB
のインスタンスのみを探して、それらをB
にダウンキャストし、それらをB
として使用しています。このループは、A
のすべてのインスタンスを処理するためのものではありません。コレクション内のB
sを処理するためだけのものです。 C
、D
、... I
には、それらを処理する独自のループ(おそらく他の場所)があります。また、新しいサブクラスJ
を追加する場合は、新しいループも必要になります。ただし、これらのループの作成は、A
をサブクラス化する基本的な部分であり、修正が必要なランダムなメソッドではありません。
あなたが自分がこのようなものを書いているのを見つけたら:
for (A a : theBigCollection) {
if (a instanceof B) {
B b = (B)a;
// B specific code
} else if (a instanceof G) {
G g = (G)a;
// G specific code
}
// Other subclasses are not handled here
}
doit
の問題を繰り返しています-このループがA
とG
の両方を処理する必要がある場合、J
を処理する必要がないことをどのようにして知ることができますか?
この場合、この動作をmidサブクラスまたはインターフェースにバンドルする必要があります。これをX
と呼びましょう。
public interface X {
// bla bla bla
}
public class B extends A implements X {
}
public class G extends A implements X {
}
// the loop from before
for (A a : theBigCollection) {
if (a instanceof X) {
X x = (X)a;
// X specific code
}
}
これで、J
が必要な場合、X
を実装して、このループで処理できます。同じメソッドを実装するサブクラスの小さなセットについて言及したので、これはおそらく必要なものです。必要に応じて、そのようなインターフェースを複数持つことができます。
ダウンキャストの問題は、同じコードパスを通過するが、それらを処理する特別なコードがない新しいサブクラスを追加する可能性です。したがって、ダウンキャストを使用する必要があるかどうかを検討するときは、誰かが新しいサブクラスを追加するとどうなるかを常に考えてください。
これは ゼロ-ゼロ-無限大ルール のより広い解釈を使用するのに良い場所だと思います。コード「ユニット」では、A
のゼロ、1、または無限(==すべて可能)のサブクラスを処理する必要があります。
A
を使用しません。面白くないA
のすべての可能なサブクラスを処理します。つまり、A
自体をダウンキャストせずに使用します。複数のレベルがある可能性があることに注意してください-私の最後の例では、最初にX
(one)にダウンキャストし、次にX
のすべての可能なサブクラスで機能するX
のコードを記述しましたinfinity)。
いくつかのポリモーフィックな動作を使用できることをすでに認識しているようです。オブジェクトのコレクションがあり、「ことを行う」の詳細が異なる場合でも、各オブジェクトを「DoTheThing()」に順序付けるだけで便利です。
これらのオブジェクトのほとんどが、その動作に対して操作なしの実装を持っている可能性があることは気になるようです。それは全く問題ではないと思います。何もしないデフォルトのメソッドがあると、メソッドを呼び出す前にinstanceof
またはカスタムメソッドを使用してオブジェクトのタイプをチェックする手間が省けます。
神の階級に向かうことは別の問題です。おそらく、ベースクラスがSやIなどの原則 [〜#〜] solid [〜#〜] にどれだけ順守しているかを評価する必要があります。
現在の基本型Aのすべてのオブジェクトを単一のコレクションでオブジェクトをDoTheThing()
に順序付けるメソッドに渡すことは本当に重要ですか?そうでない場合は、おそらく実際にDoTheThing()
を実行できるオブジェクトを別のコレクションで渡すことができます。次に、インターフェイスを実装するなどして別の型にすることができ、DoTheThing()
を呼び出す前に型を確認する必要はありません。
私のアドバイスは、意味のあるドメインモデルを作成し、それに従うことです。
クラス(A)のインスタンスがDoTheThingを実行できることが理にかなっている場合は、関数をクラス(A)に配置しますが、これを便宜のために行っている場合、これはおそらく間違った決定です。クラス/インターフェース間の関係は、意味があり、理解しやすいものでなければなりません。
フィルターの論理スコープはクラスの動作の範囲外であるため、フィルタリングでinstanceof
テストを使用することは妥当です。
議論の余地のない例、つまり宛先アドレスに基づいてメッセージをディスパッチする必要があるメッセージキューのエンドポイントであると私が期待していることから始めましょう。メッセージの内容はフィルターに対して不透明です。アドレスフィールドの文字列比較を行っているだけです。
ここで、問題により近いものを示します。XMLDOMは、Node
、Element
、Comment
にサブクラス化されるProcessingInstruction
の基本クラスで実装されることがよくあります。 、&c。通常は、可能なノードタイプのサブセットのみを処理する必要があるため、ノードのリストをそのタイプでフィルタリングします。 DOMは実際には defines フィルタリングに使用できるnodeType
値ですが、実際にはinstanceof
は人々が使用するものです(少なくとも一部はnullセーフであるため) 。
instanceof
を避けたい場所は、それをメソッドのロジックに含めることです(たとえば、任意のオブジェクトを受け取り、instanceof
を使用してさまざまな操作を実行するメソッド)。この場合、ポリモーフィックメソッドを作成する方が、ほとんどの場合、振る舞いがより透明になるため、より優れています。
他のオプションもあります:
InstanceOfを直接使用する代わりに、ブールプロパティまたはメソッドを使用します。これにより、クライアントコードを使用して決定を行うことができますが、instanceOfクラステストを直接使用するよりもさらに抽象化されています。
abstract class BaseA { virtual bool hasFoo () { return false; } }
Booleanメソッドの代わりに、インターフェイスまたはnullを返すメソッドを使用します。インターフェースが返された場合、クライアントはそれを使用し、そうでない場合は使用しません。
abstract class BaseA { virtual IFoo getFoo () { return null; } }
上記と組み合わせて、クラス階層の複数のレベルを使用するため、ルートでは、インターフェイスを返すメソッドはnullを返しますが、階層内のいくつかのレベルの下では、インターフェイスを返すメソッドはそれ自体を返します。
abstract class SubC extends BaseA, IFoo { virtual IFoo getFoo () { return this; } abstract void DoFooThing (); // provide methods of IFoo } class SubD extends SubC { override void DoFooThing () { ... } }
これを行う理由は、IFooに多数のメソッド/操作がある場合です。 1つしかない場合は、基本クラスでDoThing ()
を提供することもできます。
ここでの通常の推奨代替案は、 emを分離しておく です。言い換えると、instanceof
を使用して、拡張機能のみを実装するオブジェクトを除外する代わりに、それらのオブジェクトを常に元のコレクションとの両方に完全に格納します。インターフェースX
オブジェクトの個別のコレクション。これは、オブジェクトの作成時に行うのが非常に簡単で、通常、多くの二重責任呼び出しコードを分割することもできます。
ここで機能する設計原則は インターフェース分離原則 と呼ばれます。これは、SOLIDの原則の「私」です。他の答えが言及しているnullオブジェクトパターンは、そのようなケースの1つです。
ただし、そのようなメソッドのグループ全体がある場合は、そのようなケースはありません。使用しないメソッドに多数のクラスを依存させると、コードが過度に結合され、抽象クラスが過度に大きくなるため、将来的にメンテナンスの問題が発生します。
クラスのシグネチャの具体的または具体的な例がなければ、有用な答えを出すことは困難です。
しかし、私はあなたと一緒にエクササイズを共有します
これが私の逸話です。
一部の流体は温度とともに膨張するため、以前はセンサーから温度値を受け取って内部の製品体積を計算する基本のタンククラスを使用していました。
後で、高さが異なる温度センサーを備えた特殊なタイプのタンクが到着しました。そのようなタンクは、一連の温度を受け取り、体積を計算する前に平均を計算する必要があります。
私の最初の考えは次のとおりです。「良い!、タンクのリストを処理するには、instanceof
を使用して適切なタイプにキャストする必要があります(プロセスでハードカップリングを作成する)。
しかし、私はそれについてさらに考えました。元の基本的なタンクも温度の配列を受け取ることができることに気づきました。そのデータフィードが常に単一のアイテムの配列を供給することだけです。単一のtempと思っていたのは、実際には単一のアイテムを持つtempの配列です。ベースタンクには単一のセンサーがありましたが、他のタイプのタンクは、いくつかのセンサーと異なる高さを必要とする冷蔵タンクでした。
だから私の提案は、抽象クラスに最も適合するクラスとそうでないクラスの違いが、私が与えた例のクラスと似ているかどうかをさらに分析することです。
それが役に立てば幸い。
これは非常に興味深い質問だと思います。この問題のバリエーションに何度か遭遇しました。私はこの種の問題のいくつかの解決策を、短所と長所ですべて使用しました:
タイプフィルタリング
ロジックを適用することには本質的に問題はありませんifオブジェクトが特定のインターフェイスを実装することは、あなたが提案するとおりです。開発者として、あなたはしばしばifステートメントに「アレルギー」ですが、それがあなたの目的に適合し、うまく構成されていれば、それで問題ありません。
ビジターパターン
このパターンは、この種の問題に対処するために特別に設計されています。パターンは少し不透明で、パターン自体をサポートするためにクラスを拡張する必要があるという事実から、個人的には嫌いですが、場合によっては非常に役立つことがあります。
戦略パターン
コレクション内のオブジェクトを反復処理し、戦略パターンを使用して、各オブジェクトに適切な戦略を適用できます。これはタイプフィルタリングに似ていますが、透過性が高くなる可能性があり、コードはよりクリーンになります。