サブクラスのAPIを汚染する場合でも、自分自身をキャストするオブジェクトを使用しても大丈夫ですか?


33

基本クラスがありBaseます。2つのサブクラスとがSub1ありSub2ます。各サブクラスにはいくつかの追加メソッドがあります。たとえば、Sub1has Sandwich makeASandwich(Ingredients... ingredients)、およびSub2has boolean contactAliens(Frequency onFrequency)

これらのメソッドは異なるパラメーターを取り、まったく異なることを行うため、完全に互換性がなく、この問題を解決するためにポリモーフィズムを使用することはできません。

Baseほとんどの機能を提供し、Baseオブジェクトの大規模なコレクションを持っています。ただし、すべてのBaseオブジェクトはa Sub1またはaのいずれかSub2であり、時にはそれらがどれであるかを知る必要があります。

次のことを行うのは悪い考えのようです。

for (Base base : bases) {
    if (base instanceof Sub1) {
        ((Sub1) base).makeASandwich(getRandomIngredients());
        // ... etc.
    } else { // must be Sub2
        ((Sub2) base).contactAliens(getFrequency());
        // ... etc.
    }
}

そこで、キャストせずにこれを回避する戦略を思いつきました。 Base現在、これらのメソッドがあります:

boolean isSub1();
Sub1 asSub1();
Sub2 asSub2();

そしてもちろん、Sub1これらのメソッドを次のように実装します

boolean isSub1() { return true; }
Sub1 asSub1();   { return this; }
Sub2 asSub2();   { throw new IllegalStateException(); }

そして、Sub2それらを逆の方法で実装します。

残念ながら、現在Sub1Sub2これらのメソッドは独自のAPIにあります。そのため、たとえばでこれを行うことができますSub1

/** no need to use this if object is known to be Sub1 */
@Deprecated
boolean isSub1() { return true; }

/** no need to use this if object is known to be Sub1 */
@Deprecated
Sub1 asSub1();   { return this; }

/** no need to use this if object is known to be Sub1 */
@Deprecated
Sub2 asSub2();   { throw new IllegalStateException(); }

このように、オブジェクトがaのみであることがわかっている場合Base、これらのメソッドは非推奨ではなく、サブクラスのメソッドを呼び出すために別の型に「キャスト」するために使用できます。これはある意味ではエレガントに思えますが、一方で、クラスからメソッドを「削除」する方法としてDeprecatedアノテーションを悪用しています。

以来Sub1インスタンスが本当にあるベース、それは使用の継承ではなく、カプセル化にメイクセンスを行います。私がやっていることは良いですか?この問題を解決するより良い方法はありますか?


12
3つのクラスはすべて、お互いについて知る必要があります。Sub3の追加には多くのコード変更が必要であり、Sub10の追加は実に痛みを伴います。
Dan Pichelman

15
実際のコードを提供していただければ大いに役立ちます。何かの特定のクラスに基づいて決定を下すことが適切な場合もありますが、そのような不自然な例で何をしているのかを正当化できるかどうかを判断することは不可能です。価値があるものは何でも、あなたが望むのは訪問者またはタグ付きユニオンです。
ドーバル

1
申し訳ありませんが、簡単な例を挙げると物事が簡単になると思いました。たぶん、私がやりたいことのより広い範囲で新しい質問を投稿するでしょう。
codebreaker

12
キャストを再実装しているだけでありinstanceof、多くの入力が必要で、エラーが発生しやすく、サブクラスを追加するのが難しくなります。
user253751

5
交換可能に使用できない場合Sub1Sub2なぜそれらをそのように扱うのですか?「サンドイッチメーカー」と「エイリアンコンタクター」を別々に追跡してみませんか?
ピーター・ウィトヴォエト

回答:


27

他のいくつかの回答で提案されているように、基本クラスに関数を追加することは必ずしも意味がありません。追加する特殊なケース関数が多すぎると、それ以外の場合は無関係なコンポーネントが結合される可能性があります。

たとえばAnimalCatDogコンポーネントを持つクラスがあります。それらを印刷したり、GUIで表示したい場合は、基本クラスに追加しrenderToGUI(...)たり追加したりsendToPrinter(...)するのはやり過ぎかもしれません。

型チェックとキャストを使用しているアプローチは脆弱ですが、少なくとも懸念事項は分離されています。

ただし、これらのタイプのチェック/キャストを頻繁に実行している場合は、ビジター/ダブルディスパッチパターンを実装することが1つのオプションです。次のように見えます:

public abstract class Base {
  ...
  abstract void visit( BaseVisitor visitor );
}

public class Sub1 extends Base {
  ...
  void visit(BaseVisitor visitor) { visitor.onSub1(this); }
}

public class Sub2 extends Base {
  ...
  void visit(BaseVisitor visitor) { visitor.onSub2(this); }
}

public interface BaseVisitor {
   void onSub1(Sub1 that);
   void onSub2(Sub2 that);
}

これであなたのコードは

public class ActOnBase implements BaseVisitor {
    void onSub1(Sub1 that) {
       that.makeASandwich(getRandomIngredients())
    }

    void onSub2(Sub2 that) {
       that.contactAliens(getFrequency());
    }
}

BaseVisitor visitor = new ActOnBase();
for (Base base : bases) {
    base.visit(visitor);
}

主な利点は、サブクラスを追加すると、静かに欠落しているケースではなく、コンパイルエラーが発生することです。新しいビジタークラスは、関数をプルするための素晴らしいターゲットにもなります。例えば、それは移動しても意味がありますgetRandomIngredients()ActOnBase

ループロジックを抽出することもできます。たとえば、上記のフラグメントは次のようになります。

BaseVisitor.applyToArray(bases, new ActOnBase() );

さらに少しマッサージして、Java 8のラムダとストリーミングを使用すると、

bases.stream()
     .forEach( BaseVisitor.forEach(
       Sub1 that -> that.makeASandwich(getRandomIngredients()),
       Sub2 that -> that.contactAliens(getFrequency())
     ));

どのIMOが、見た目も簡潔で、あなたが手に入れることができるのと同じくらい簡潔です。

次に、より完全なJava 8の例を示します。

public static abstract class Base {
    abstract void visit( BaseVisitor visitor );
}

public static class Sub1 extends Base {
    void visit(BaseVisitor visitor) { visitor.onSub1(this); }

    void makeASandwich() {
        System.out.println("making a sandwich");
    }
}

public static class Sub2 extends Base {
    void visit(BaseVisitor visitor) { visitor.onSub2(this); }

    void contactAliens() {
        System.out.println("contacting aliens");
    }
}

public interface BaseVisitor {
    void onSub1(Sub1 that);
    void onSub2(Sub2 that);

    static Consumer<Base> forEach(Consumer<Sub1> sub1, Consumer<Sub2> sub2) {

        return base -> {
            BaseVisitor baseVisitor = new BaseVisitor() {

                @Override
                public void onSub1(Sub1 that) {
                    sub1.accept(that);
                }

                @Override
                public void onSub2(Sub2 that) {
                    sub2.accept(that);
                }
            };
            base.visit(baseVisitor);
        };
    }
}

Collection<Base> bases = Arrays.asList(new Sub1(), new Sub2());

bases.stream()
     .forEach(BaseVisitor.forEach(
             Sub1::makeASandwich,
             Sub2::contactAliens));

+1:この種のことは、一般的にsum-typesとパターンマッチングを第一級でサポートする言語で使用され、構文的に非常にいにもかかわらず、Javaで有効な設計です。
マンカルス

@Mankarse少しの構文糖はAいものから非常に遠く離れたものになる可能性があります-例で更新しました。
マイケルアンダーソン

仮にOPが心を変えて、を追加することにしたとしましょうSub3。訪問者はまったく同じ問題を抱えていinstanceofませんか?ここでonSub3、ビジターに追加する必要があり、忘れると中断します。
Radiodef

1
@Radiodefタイプを直接切り替えるよりもビジターパターンを使用する利点はonSub3、ビジターインターフェイスに追加すると、まだ更新されていない新しいビジターを作成するすべての場所でコンパイルエラーが発生することです。対照的に、タイプを切り替えると、せいぜい実行時エラーが生成されます。これは、トリガーするのが難しい場合があります。
マイケルアンダーソン

1
@FiveNine、他の人を助けるかもしれない答えの最後にその完全な例を追加して満足している場合。
マイケルアンダーソン

83

私の観点から:あなたのデザインが間違っています。

自然言語に翻訳すると、あなたは次のことを言っています:

我々が持っていることを考えるとanimals、がcatsありfishます。 およびにanimals共通のプロパティがcatsありfishます。しかし、それだけでは十分ではありません。いくつかのプロパティがあり、これらはcatから区別されるfishため、サブクラス化する必要があります。

これで、運動のモデル化を忘れたという問題があります。はい。それは比較的簡単です:

for(Animal a : animals){
   if (a instanceof Fish) swim();
   if (a instanceof Cat) walk();
}

しかし、それは間違った設計です。正しい方法は次のとおりです。

for(Animal a : animals){
    animal.move()
}

move共有行動は、各動物によって異なる場所で実装されます。

これらのメソッドは異なるパラメーターを取り、まったく異なることを行うため、完全に互換性がなく、この問題を解決するためにポリモーフィズムを使用することはできません。

つまり、デザインが壊れています。

私の推奨事項:リファクタリングBaseSub1およびSub2


8
実際のコードを提供する場合は、推奨事項を作成できます。
トーマスジャンク

9
@codebreakerあなたの例が良くないことに同意するなら、私はこの答えを受け入れてから新しい質問を書くことをお勧めします。質問を別の例に修正したり、既存の回答が意味をなさないようにしたりしないでください。
Mobyディスク

16
@codebreakerこれは本当の質問に答えることで問題を「解決」すると思います。本当の問題は、どうすれば問題を解決できますか?コードを適切にリファクタリングします。リファクタリングあなたのように、.move()または.attack()、適切に抽象that部分-の代わりになる.swim().fly().slide().hop().jump().squirm().phone_home()、などがどのように適切にリファクタリングしていますか?より良い例なしで不明...しかし、一般的に正しい答えです-例のコードと詳細がそうでないことを示唆しない限り。
WernerCD

11
@codebreakerクラス階層がこのような状況につながる場合、定義上は意味がありません
パトリックコリンズ

7
@codebreaker Crisfoleの最後のコメントに関連して、役立つかもしれない別の見方があります。たとえば、movevs fly/ run/ etcを使用すると、1つの文で次のように説明できます。アクセサに関しては、ここでのコメントで述べているように、実際のケースに似ているため、オブジェクトの状態を調べるのではなく、オブジェクトに質問する必要があります。
イズカタ

9

あなたが物事のグループを持っている状況を想像するのは少し難しいです。このようなキャストを見つけるほとんどの場合、1つのタイプで操作します。たとえば、clang では、リスト内のノードごとに異なる処理を行うのではなく、getAsFunctionがnull以外を返す宣言のノードセットをフィルタリングします。

アクションのシーケンスを実行する必要があるかもしれませんが、アクションを実行するオブジェクトが関連していることは実際には関係ありません。

のリストの代わりに、Baseアクションのリストを処理します

for (RandomAction action : actions)
   action.act(context);

どこで

interface RandomAction {
    void act(Context context);
} 

interface Context {
    Ingredients getRandomIngredients();
    double getFrequency();
}

必要に応じて、Baseにアクションを返すメソッドを実装させるか、ベースリスト内のインスタンスからアクションを選択する必要がある他の手段を実行できます(ポリモーフィズムを使用できないと言うので、おそらく実行するアクションはクラスの関数ではなく、基底の他のプロパティです;それ以外の場合は、Baseにact(Context)メソッドを渡します)


2
私の種類の不条理なメソッドは、サブクラスがわかればクラスが実行できることの違いを示すように設計されています(そして、それらは互いに関係がない)が、Contextオブジェクトがあると事態が悪化するだけだと思います。Objectメソッドにsを渡してキャストし、それらが正しい型であることを願っています。
codebreaker

1
@codebreakerあなたは本当にあなたの質問を明確にする必要があります-ほとんどのシステムでは、それらが何をするかを知らない関数の束を呼び出す正当な理由があるでしょう。たとえば、イベントのルールを処理したり、シミュレーションのステップを実行したりします。通常、このようなシステムには、アクションが発生するコンテキストがあります。「getRandomIngredients」と「getFrequency」が関連していない場合、同じオブジェクト内にあるべきではないため、成分や頻度のソースをキャプチャーするアクションなど、別のアプローチが必要です。
ピートカーカム

1
あなたは正しい、そして私はこの質問を救うことができるとは思わない。私は悪い例を選んでしまったので、たぶん別の例を投稿します。getFrequency()およびgetIngredients()は単なるプレースホルダーでした。私はおそらく、「...」を引数として入れて、それらが何であるかわからないことをより明らかにする必要がありました。
codebreaker

これがIMOの正しい答えです。がContext新しいクラスやインターフェースである必要はないことを理解してください。通常、コンテキストは呼び出し元であるため、呼び出しはの形式になりaction.act(this)ます。場合getFrequency()getRandomIngredients()静的メソッドで、あなたもコンテキストを必要としない場合があります。これが、ジョブキューという実装方法です。これは、問題のように思えます。
質問

また、これは訪問者パターンソリューションとほとんど同じです。違いは、あなたがSub1をとしてビジター:: OnSub1()/ビジター:: OnSub2()メソッドを実装しているされて::行為()/ SUB2 ::行為()
QuestionC

4

サブクラスで実行できることを定義する1つ以上のインターフェイスを実装している場合はどうでしょうか。このようなもの:

interface SandwichCook
{
    public void makeASandwich(String[] ingredients);
}

interface AlienRadioSignalAwarable
{
    public void contactAliens(int frequency);

}

クラスは次のようになります。

class Sub1 extends Base implements SandwichCook
{
    public void makeASandwich(String[] ingredients)
    {
        //some code here
    }
}

class Sub2 extends Base implements AlienRadioSignalAwarable
{
    public void contactAliens(int frequency)
    {
        //some code here
    }
}

そして、forループは次のようになります。

for (Base base : bases) {
    if (base instanceof SandwichCook) {
        base.makeASandwich(getRandomIngredients());
    } else if (base instanceof AlienRadioSignalAwarable) {
        base.contactAliens(getFrequency());
    }
}

このアプローチの2つの主な利点:

  • キャストなし
  • 各サブクラスに必要な数のインターフェイスを実装させることができます。これにより、将来の変更に対してある程度の柔軟性が提供されます。

PS:インターフェースの名前についてすみません、その特定の瞬間にもっとクールなものは考えられませんでした:D。


3
これは実際には問題を解決しません。まだforループでキャストが必要です。(そうしない場合は、OPの例でもキャストは必要ありません)。
-Taemyr

2

アプローチは、ファミリー内のほぼすべてのタイプであろう場合には良いものとすることができるいずれかのいくつかの基準を満たすいくつかのインターフェースの実装として直接使用すること、またはそのインターフェースの実装を作成するために使用することができます。組み込みのコレクションタイプはこのパターンの恩恵を受けましたが、例の目的ではないため、コレクションインターフェイスを発明しますBunchOfThings<T>

の一部の実装BunchOfThingsは変更可能です。いくつかはそうではありません。多くの場合、オブジェクトFredはaとして使用できるものを保持しBunchOfThings、Fred以外は何も変更できないことを知りたい場合があります。この要件は、次の2つの方法で満たすことができます。

  1. フレッドは、それへの唯一の参照を保持し、その内部BunchOfThingsへの外部参照BunchOfThingsは宇宙のどこにも存在しないことを知っています。他の誰もBunchOfThingsまたはその内部への参照を持たない場合、他の誰もそれを変更できないため、制約は満たされます。

  2. BunchOfThings、および外部参照が存在する内部のいずれも、いかなる手段でも変更できません。絶対に誰もaを変更できない場合BunchOfThings、制約は満たされます。

制約を満たす1つの方法は、受け取ったオブジェクトを無条件にコピーすることです(ネストされたコンポーネントを再帰的に処理します)。もう1つは、受け取ったオブジェクトが不変性を保証するかどうかをテストし、そうでない場合はそのコピーを作成し、ネストされたコンポーネントで同様に行うことです。2番目よりもクリーンで、1番目よりも高速になりがちな代替AsImmutable方法は、オブジェクトにそれ自体の不変コピーを作成するように要求するメソッドを提供することです(それAsImmutableをサポートするネストされたコンポーネントを使用)。

関連するメソッドも提供される場合がありますasDetached(コードがオブジェクトを受け取り、それを変更するかどうかわからない場合に使用します。その場合、変更可能なオブジェクトを新しい変更可能なオブジェクトに置き換える必要がありますが、不変のオブジェクトは保持できます)そのまま)、asMutable(オブジェクトが以前に返されたオブジェクトを保持することを知っている場合asDetached、つまり、可変オブジェクトへの非共有参照または可変オブジェクトへの共有可能な参照)、およびasNewMutable(コードが外部を受信する場合)参照し、その中のデータのコピーを変更したいことを知っています。着信データが変更可能な場合、変更可能なコピーを作成するためにすぐに使用されてから破棄される不変のコピーを作成することから始める理由はありません)

一方でことに注意してくださいasXX方法は若干異なる種類を返すことがあり、彼らの本当の役割は、返されるオブジェクトは、プログラムのニーズを満たすことを確認することです。


0

あなたが良いデザインを持っているかどうかの問題を無視し、それが良いか少なくとも受け入れられると仮定すると、タイプではなくサブクラスの機能を検討したいと思います。

したがって、次のいずれかです。


基本クラスの一部のインスタンスではできないことを知っていても、サンドイッチとエイリアンの存在に関する知識を基本クラスに移動します。基本クラスに実装して例外をスローし、コードを次のように変更します。

if (base.canMakeASandwich()) {
    base.makeASandwich(getRandomIngredients());
    // ... etc.
} else { // can't make sandwiches, must be able to contact aliens
    base.contactAliens(getFrequency());
    // ... etc.
}

次に、1つまたは両方のサブクラスがオーバーライドしcanMakeASandwich()、1つだけがmakeASandwich()とのそれぞれを実装しcontactAliens()ます。


型が持つ機能を検出するには、具体的なサブクラスではなくインターフェイスを使用します。基本クラスをそのままにして、コードを次のように変更します。

if (base instanceof SandwichMaker) {
    ((SandwichMaker)base).makeASandwich(getRandomIngredients());
    // ... etc.
} else { // can't make sandwiches, must be able to contact aliens
    ((AlienContacter)base).contactAliens(getFrequency());
    // ... etc.
}

または場合によっては(そして、このオプションがあなたのスタイルに合わない場合、あるいはあなたが賢明だと思うJavaスタイルについては、このオプションを無視しても構いません):

try {
    ((SandwichMaker)base).makeASandwich(getRandomIngredients());
} catch (ClassCastException e) {
    ((AlienContacter)base).contactAliens(getFrequency());
}

個人的に私はそうではない通常ので不適切引くのリスクを、半予想例外をキャッチする後者のスタイルのようにClassCastException出てくるのgetRandomIngredientsmakeASandwichが、メーリングリストへ。


2
ClassCastExceptionをキャッチするのは簡単ではありません。これがinstanceofの全体的な目的です。
Radiodef

@Radiodef:true、しかし、1つの目的<は、キャッチを回避することArrayIndexOutOfBoundsExceptionであり、一部の人々もそれを行うことを決定します。基本的にここでの私の「問題」はPythonで働いていることです。そこでは、事前テストがどれほど単純であっても、例外が発生するかどうかを事前にテストするよりも、例外をキャッチする方が望ましいという通常のスタイルです。たぶん、Javaでその可能性に言及する価値はありません。
スティーブジェソップ

@SteveJessop»許可よりも許しを求める方が簡単です«はスローガンです;)
トーマスジャンク

0

ここに、派生クラスにダウンキャストする基本クラスの興味深いケースがあります。これは通常良くないことを十分に知っていますが、正当な理由が見つかったと言いたい場合は、これに対する制約が何であるかを見てみましょう。

  1. 基本クラスのすべてのケースをカバーできます。
  2. 外部派生クラスは、新しいケースを追加する必要がありません。
  3. 基本クラスの制御下にある呼び出しを行うポリシーを使用できます。
  4. しかし、私たちの科学から、基本クラスにないメソッドの派生クラスで何をすべきかというポリシーは、派生クラスのポリシーであり、基本クラスではないことがわかっています。

4の場合、次のようになります。5.派生クラスのポリシーは、基本クラスのポリシーと常に同じ政治的管理下にあります。

2と5は両方とも、すべての派生クラスを列挙できることを直接意味します。つまり、外部派生クラスは存在しないはずです。

しかし、ここにあります。それらがすべてあなたのものであれば、ifを仮想メソッド呼び出しである抽象化に置き換えて(それがナンセンスな呼び出しであっても)、ifとセルフキャストを取り除くことができます。したがって、それをしないでください。より良いデザインが利用可能です。


-1

Sub1で1つのこと、Sub2で別のことを行う抽象メソッドを基本クラスに配置し、混合ループでその抽象メソッドを呼び出します。

class Sub1 : Base {
    @Override void doAThing() {
        this.makeASandwich(getRandomIngredients());
    }
}

class Sub2 : Base {
    @Override void doAThing() {
        this.contactAliens(getFrequency());
    }
}

for (Base base : bases) {
    base.doAThing();
}

getRandomIngredientsとgetFrequencyの定義方法によっては、これに他の変更が必要になる場合があることに注意してください。しかし、正直なところ、とにかくgetFrequencyを定義するには、おそらく、エイリアンに接触するクラス内がより良い場所です。

ちなみに、asSub1メソッドとasSub2メソッドは間違いなく悪い習慣です。これを行う場合は、キャストしてください。これらのメソッドが、キャストでは得られないものは何もありません。


2
あなたの答えは、あなたが質問を完全に読んでいないことを示唆しています:多型はここでそれをカットしないでしょう。Sub1とSub2にはそれぞれいくつかのメソッドがあり、これらは単なる例であり、「doAThing」は実際には何も意味しません。それは私がやろうとしていることに対応していません。また、最後の文については、型の安全性は保証されていますか?
コードブレーカー

@codebreaker-例のループで行っていることに正確に対応しています。意味がない場合は、ループを記述しないでください。メソッドとして意味をなさない場合、ループ本体として意味をなしません。
Random832

1
そして、メソッドはキャストと同じように型の安全性を「保証」します。オブジェクトが間違った型である場合に例外をスローします。
Random832

悪い例を選んだことに気づきました。問題は、サブクラスのメソッドのほとんどが、変更可能なメソッドというよりはゲッターに似ていることです。これらのクラスはそれ自体ではなく、ほとんどの場合データを提供するだけです。ポリモーフィズムはそこに私を助けません。私は消費者ではなく生産者を扱っています。また、例外のスローは実行時の保証であり、コンパイル時ほど適切ではありません。
codebreaker

1
私のポイントは、あなたのコードも同様にランタイム保証を提供することです。asSub2を呼び出す前に誰かがisSub2のチェックに失敗するのを止めるものは何もありません。
Random832

-2

両方makeASandwichcontactAliens基本クラスにプッシュしてから、それらをダミー実装で実装できます。戻り値の型/引数は共通の基本クラスに解決できないため。

class Sub1 extends Base{
    Sandwich makeASandwich(Ingredients i){
        //Normal implementation
    }
    boolean contactAliens(Frequency onFrequency){
        return false;
    }
 }
class Sub2 extends Base{
    Sandwich makeASandwich(Ingredients i){
        return null;
    }
    boolean contactAliens(Frequency onFrequency){
       // normal implementation
    }
 }

この種のことには明らかな欠点があります。それはメソッドのコントラクトにとって何を意味するのか、結果のコンテキストについて何を推論できるのか、できないのかなどです。サンドイッチを作れなかったので、試食などで材料が使い果たされたとは思えません。


1
私はこれを行うことを考えましたが、それはリスコフ置換の原則に違反しており、たとえば「sub1」を入力するときなど、一緒に作業するのは良くありません。オートコンプリートが表示されると、makeASandwichが表示されますが、contactAliensは表示されません。
codebreaker

-5

あなたがしていることは完全に合法です。彼らはいくつかの本でそれを読んだので、単に教義を繰り返している否定論者に注意を払ってはいけません。ドグマにはエンジニアリングの場がありません。

私は同じメカニズムを数回使用しましたが、Javaランタイムも、考えられる少なくとも1つの場所で同じことを実行できたため、パフォーマンス、使いやすさ、コードの可読性が向上したと確信できます。それを使用します。

例えば取るjava.lang.reflect.Memberのベースである、java.lang.reflect.Fieldjava.lang.reflect.Method。(実際の階層はそれよりも少し複雑ですが、それは無関係です。)フィールドとメソッドは非常に異なる動物です。一方には取得または設定できる値があり、他方にはそのようなものはありませんが、多数のパラメータを使用し、値を返す場合があります。したがって、フィールドとメソッドはどちらもメンバーですが、それらを使ってできることは、サンドイッチを作ることとエイリアンに連絡することと同じくらいお互いに異なっています。

さて、リフレクションを使用するコードを書くときMember、私たちはしばしばa を手にし、それがa Methodまたはa Field(またはめったに何か他のもの)であることを知っていますが、instanceof正確に把握するためにすべての退屈な作業をしなければなりませんそれが何であるか、それをキャストして適切な参照を取得する必要があります。(また、これは退屈なだけでなく、パフォーマンスもあまりよくありません。)Methodクラスは、説明しているパターンを非常に簡単に実装できたため、何千人ものプログラマーの生活が楽になりました。

もちろん、この手法は、密接に結合されたクラスの小さく明確に定義された階層でのみ実行可能です。クラス階層が以下の場合、ソースレベルで制御します(常に保持します)。基本クラスをリファクタリングする自由がない人々によって拡張される可能性があります。

ここに私がしたこととあなたがしたこととの違いがあります:

  • 基本クラスは、asDerivedClass()メソッドのファミリー全体にデフォルトの実装を提供し、それぞれがメソッドを返すようにしnullます。

  • 各派生クラスは、asDerivedClass()メソッドの1つだけをオーバーライドし、thisではなくを返しますnull。残りの部分をオーバーライドすることも、それらについて何も知りたくないこともありません。したがって、IllegalStateExceptionsはスローされません。

  • 基本クラスは、次のようにコード化されたメソッドファミリfinal全体の実装も提供isDerivedClass()します。return asDerivedClass() != null; この方法により、派生クラスによってオーバーライドされる必要があるメソッドの数が最小限に抑えられます。

  • 私は@Deprecatedそれを考えなかったので、このメカニズムで使用していません。あなたが私にアイデアを与えたので、私はそれを使用することにします、ありがとう!

C#には、asキーワードを使用して組み込まれた関連メカニズムがあります。C#で言うDerivedClass derivedInstance = baseInstance as DerivedClassことができ、DerivedClassあなたbaseInstanceがそのクラスに属していた場合、そうでない場合に参照を取得しますnull。これは(理論的に)isキャストが続くよりも優れたパフォーマンスを発揮します(is確かにinstanceof、C#キーワードの名前の方が優れていinstanceofます)。asC#の演算子は、カスタムアプローチの単一の仮想メソッド呼び出しほど高速ではないためです。

私はここで、この手法はパターンであると宣言されるべきであり、それについていい名前が見つかるべきであるという命題を提示しました。

ジー、ダウンボートに感謝します!

コメントを読む手間を省くための論争の要約:

人々の反対は、元のデザインが間違っていたということであるようです。つまり、共通の基本クラスから派生するクラスが大きく異なることはありません。基本参照であり、派生クラスを把握する必要があります。したがって、彼らは、この質問と私の答えによって提案された、元のデザインの使用を改善する自己鋳造メカニズムは、そもそも必要ではなかったと言っています。(彼らは実際にセルフキャスティングのメカニズム自体については何も言わず、メカニズムが適用されることを意図している設計の性質についてのみ不平を言っています。)

しかし、この例では私がしている上に、すでに Javaランタイムの作成者が実際にのために正確なデザインを選んだことを示しjava.lang.reflect.MemberFieldMethod階層、およびI下のコメント欄では、また、 C#のランタイムのクリエイターが独立してANに到着したことを示しています同等の設計System.Reflection.MemberInfoFieldInfoMethodInfo階層。したがって、これらは2つの異なる現実世界のシナリオであり、すべての人の鼻の真下にあり、まさにそのような設計を使用した実証可能な実行可能なソリューションを備えています。

それが、以下のすべてのコメントの要約です。自己キャストのメカニズムについてはほとんど言及されていません。


13
あなたがボックスに自分自身を設計した場合、それは当然です。これは、これらの種類の質問の多くの問題です。実際の解決策は、そもそもなぜこのような状況に陥ったのかを理解することである場合、人々はこの種のハック的な解決策を強制する設計の他の領域で決定を下します。きちんとしたデザインではここには行きません。現在、200KのSLOCアプリケーションを見ていますが、これを行うために必要なものがどこにもありません。したがって、それは人々が本を読むだけではありません。こうしたタイプの状況に陥らないことは、単に優れた実践に従うことの最終的な結果です。
ダンク

3
@Dunkこのようなメカニズムの必要性は、たとえばjava.lang.reflection.Member階層に存在することを示しました。Javaランタイムの作成者は、このような状況に陥りました。ボックスに自分自身を設計したと言ったり、何らかのベストプラクティスに従わなかったと非難したりするとは思わないでしょうか。ですから、ここで私が指摘しているのは、いったんこのような状況に陥ると、このメカニズムは物事を改善する良い方法であるということです。
マイクナキス

6
@MikeNakis Javaの作成者は、多くの悪い決定を下しているので、それだけではあまり語りません。とにかく、訪問者を使用することは、アドホックなテスト後キャストを行うよりも優れています。
ドーバル

3
さらに、この例には欠陥があります。Memberインターフェースはありますが、アクセス修飾子を確認するだけです。通常、メソッドまたはプロパティのリストを取得し、それらを直接(それらを混合せずにList<Member>表示するため)使用するため、キャストする必要はありません。メソッドをClass提供しないことに注意してくださいgetMembers()。もちろん、無知なプログラマーがを作成することもありますがList<Member>、それを作成しList<Object>て一部Integerまたは追加するのと同じくらいほとんど意味がありませんString
SJuan76

5
@MikeNakis「多くの人がやる」と「有名人がやる」は本当の議論ではありません。アンチパターンは悪い考えであり、定義によって広く使用されていますが、それらの言い訳はそれらの使用を正当化します。何らかのデザインを使用する本当の理由を示してください。アドホックキャストに関する私の問題は、APIのすべてのユーザーが、キャストするたびに正しくキャストする必要があることです。訪問者は一度だけ正しい必要があります。いくつかのメソッドの背後にキャストを隠し、null例外の代わりに戻ることは、それをそれほど良くしません。他のすべてが等しい場合、訪問者は同じ仕事をしている間はより安全です。
ドーバル
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.