web-dev-qa-db-ja.com

サブクラスのAPIを汚染する場合でも、それ自体をキャストするオブジェクトがあっても大丈夫ですか?

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

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

Baseはほとんどの機能を提供し、Baseオブジェクトの大規模なコレクションを持っています。ただし、すべてのBaseオブジェクトは_Sub1_または_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_は逆の方法でそれらを実装します。

残念ながら、現在_Sub1_および_Sub2_は、独自の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(); }
_

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

_Sub1_インスタンスは実際にはis a Baseなので、カプセル化ではなく継承を使用することは理にかなっています。私は何をしてるの?この問題を解決するより良い方法はありますか?

33
codebreaker

他のいくつかの回答で示唆されているように、基本クラスに関数を追加することが常に意味があるとは限りません。あまりにも多くの特殊なケース関数を追加すると、それ以外の場合は無関係なコンポーネントが一緒にバインドされる可能性があります。

たとえば、AnimalおよびCatコンポーネントを含むDogクラスがあるとします。それらを印刷したり、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));
_
27

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

自然言語に翻訳すると、次のようになります。

animalsがあるとすると、catsfishがあります。 animalsには、catsfishに共通のプロパティがあります。しかし、それだけでは不十分です。catfishを区別するいくつかのプロパティがあるため、サブクラス化する必要があります。

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

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

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

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

ここでmoveは、動物ごとに異なる方法で実装された共有行動です。

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

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

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

82
Thomas Junk

物事のグループがあり、サンドイッチを作ったりエイリアンに連絡したりする状況を想像するのは少し難しいです。このようなキャストが見つかるほとんどの場合、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に実装させることができます(多態性を使用できないため、実行するアクションはおそらくクラスの関数ではなく、ベースの他のいくつかのプロパティです。それ以外の場合は、ベースにact(Context)メソッドを与えるだけです)

9
Pete Kirkham

サブクラスに、何ができるかを定義する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。

4
Radu Murzea

アプローチは、ファミリー内のほとんどすべてのタイプがどちらかが何らかの基準を満たすインターフェイスの実装として直接使用できる場合に適していますorを使用してそのインターフェースの実装を作成します。組み込みのコレクション型はこのパターンの恩恵を受けますが、例ではないため、コレクションインターフェイスを作成しますBunchOfThings<T>

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

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

  2. BunchOfThingsも、外部参照が存在するその内部も、いかなる方法でも変更できません。誰もBunchOfThingsを変更できない場合、制約は満たされます。

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

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

asXXメソッドは少し異なる型を返す場合がありますが、それらの実際の役割は、返されたオブジェクトがプログラムのニーズを満たすことを保証することです。

2
supercat

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

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


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

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

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


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

_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またはgetRandomIngredientsからのmakeASandwichを不適切にキャッチするリスクがあるため、個人的には通常は半分予期された例外をキャッチする後者のスタイルとは異なりますが、YMMVです。

0
Steve Jessop

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

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

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

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

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

0
Joshua