基本クラス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なので、カプセル化ではなく継承を使用することは理にかなっています。私は何をしてるの?この問題を解決するより良い方法はありますか?
他のいくつかの回答で示唆されているように、基本クラスに関数を追加することが常に意味があるとは限りません。あまりにも多くの特殊なケース関数を追加すると、それ以外の場合は無関係なコンポーネントが一緒にバインドされる可能性があります。
たとえば、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));
_
私の観点から:あなたのデザインは間違っています。
自然言語に翻訳すると、次のようになります。
animals
があるとすると、cats
とfish
があります。 animals
には、cats
とfish
に共通のプロパティがあります。しかし、それだけでは不十分です。cat
とfish
を区別するいくつかのプロパティがあるため、サブクラス化する必要があります。
movementをモデル化するのを忘れたという問題があります。はい。それは比較的簡単です:
for(Animal a : animals){
if (a instanceof Fish) swim();
if (a instanceof Cat) walk();
}
しかし、それは間違った設計です。正しい方法は次のとおりです。
for(Animal a : animals){
animal.move()
}
ここでmove
は、動物ごとに異なる方法で実装された共有行動です。
これらのメソッドは異なるパラメーターを取り、まったく異なる処理を行うため、完全に互換性がなく、この問題を解決するために多態性を使用することはできません。
つまり、デザインが壊れています。
私の推奨事項:リファクタリングBase
、Sub1
およびSub2
。
物事のグループがあり、サンドイッチを作ったりエイリアンに連絡したりする状況を想像するのは少し難しいです。このようなキャストが見つかるほとんどの場合、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)メソッドを与えるだけです)
サブクラスに、何ができるかを定義する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。
アプローチは、ファミリー内のほとんどすべてのタイプがどちらかが何らかの基準を満たすインターフェイスの実装として直接使用できる場合に適していますorを使用してそのインターフェースの実装を作成します。組み込みのコレクション型はこのパターンの恩恵を受けますが、例ではないため、コレクションインターフェイスを作成しますBunchOfThings<T>
。
BunchOfThings
の一部の実装は変更可能です。一部ではありません。多くの場合、FredはBunchOfThings
として使用できるものを保持し、Fred以外はそれを変更できないことを知っている場合があります。この要件は、次の2つの方法で満たすことができます。
フレッドは、そのBunchOfThings
への唯一の参照を保持し、そのBunchOfThings
への外部参照は宇宙のどこかに存在しないことを知っています。他に誰もBunchOfThings
またはその内部への参照を持たない場合、他の誰もそれを変更できないため、制約が満たされます。
BunchOfThings
も、外部参照が存在するその内部も、いかなる方法でも変更できません。誰もBunchOfThings
を変更できない場合、制約は満たされます。
制約を満たす1つの方法は、受け取ったオブジェクトを無条件にコピーすることです(ネストされたコンポーネントを再帰的に処理します)。もう1つは、受け取ったオブジェクトが不変性を約束するかどうかをテストし、そうでない場合はそのコピーを作成し、ネストされたコンポーネントについても同様に行います。もう1つは、2番目よりもクリーンで最初よりも高速になる傾向がある、オブジェクトに不変のコピーを作成するようにオブジェクトに要求するAsImmutable
メソッドを提供することです(任意のオブジェクトでAsImmutable
を使用)それをサポートするネストされたコンポーネント)。
関連するメソッドをasDetached
に提供することもできます(コードがオブジェクトを受け取り、それを変更したいかどうかわからない場合に使用します。この場合、可変オブジェクトは新しい可変オブジェクトに置き換える必要がありますが、不変オブジェクトはそのまま保持できます)、asMutable
(オブジェクトがasDetached
から以前に返されたオブジェクトを保持することがわかっている場合、つまり、可変オブジェクトへの非共有参照または変更可能な参照への共有可能な参照)、およびasNewMutable
(コードが外部参照を受信し、そこにあるデータのコピーを変更したいことがわかっている場合-受信データが変更可能な場合、理由はありません)変更可能なコピーを作成するためにすぐに使用され、その後破棄される不変のコピーを作成することから始めます)。
asXX
メソッドは少し異なる型を返す場合がありますが、それらの実際の役割は、返されたオブジェクトがプログラムのニーズを満たすことを保証することです。
あなたが良いデザインを持っているかどうかの問題を無視して、それが良いか少なくとも受け入れられると仮定すると、タイプではなくサブクラスの機能を検討したいと思います。
したがって、次のいずれかです。
基本クラスのインスタンスが実行できないことがわかっている場合でも、サンドイッチとエイリアンの存在に関する知識を基本クラスに移動します。これを基本クラスに実装して例外をスローし、コードを次のように変更します。
_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です。
ここに、それ自体を派生クラスにダウンキャストする基本クラスの興味深いケースがあります。これは通常悪いことですが、十分な理由があることを確認したい場合は、これに対する制約が何であるかを見てみましょう。
4の場合、次のようになります。5.派生クラスのポリシーは、基本クラスのポリシーと常に同じ政治的支配下にあります。
2と5は両方とも、すべての派生クラスを列挙できることを直接的に意味します。つまり、外部の派生クラスはないはずです。
しかし、これが問題です。それらがすべてあなたのものである場合、ifを仮想メソッド呼び出しである抽象化に置き換え(たとえそれがナンセンスなものであっても)、ifとセルフキャストを取り除くことができます。したがって、それを行わないでください。より良いデザインが利用可能です。