17

オブジェクト指向プログラミングの典型的な問題はダイヤモンド問題です。親クラス A と 2 つのサブクラス B および C があります。A には抽象メソッドがあり、B と C はそれを実装します。これで、BとCを継承するサブクラス D ができました。ひし形の問題は、D が B の実装と C の実装のどちらを使用するかということです。

人々は、Java はダイヤモンドの問題を知らないと主張します。インターフェイスを使用した多重継承のみを行うことができます。それらには実装がないため、ダイヤモンドの問題はありません。これは本当ですか?私はそうは思わない。下記参照:

【取り外し車両例】

ひし形の問題は常に悪いクラス設計の原因であり、プログラマーもコンパイラーも解決する必要のないものですか?


更新: 私の例の選択が不十分だったのかもしれません。

この画像を見る

ダイヤモンド問題
(出典: suffolk.edu )

もちろん、Person を C++ で仮想化することもできるため、メモリ内には person のインスタンスが 1 つしかありませんが、実際の問題は解決しません。GradTeachingFellow の getDepartment() をどのように実装しますか? 考えてみてください、彼はある学部の学生であり、別の学部で教えているかもしれません。したがって、一方または他方の部門を返すことができます。問題の完全な解決策はなく、実装が継承されない可能性がある (たとえば、Student と Teacher の両方がインターフェイスになる可能性がある) という事実は、私には問題を解決していないようです。

4

18 に答える 18

20

あなたが見ているのは、Liskov Substitution Principleの違反により、機能する論理的なオブジェクト指向構造を持つことが非常に困難になっていることです。
基本的に、(パブリック) 継承は、クラスの目的を拡張するのではなく、狭めるべきです。この場合、2 種類の乗り物から継承することで、実際には目的を拡張しています。お気づきのように、それは機能しません。水上乗り物と道路乗り物では、動きが大きく異なるはずです。
代わりに、水陸両用車に水上車両と地上車両オブジェクトを集約し、2 つのうちどちらが現在の状況に適しているかを外部から判断することができます。
別の方法として、「車両」クラスが不必要に汎用的であり、両方に別個のインターフェースを持つことを決定することもできます。ただし、それだけでは水陸両用車の問題は解決しません。両方のインターフェイスで移動メソッド「move」を呼び出すと、まだ問題が発生します。したがって、継承ではなく集約をお勧めします。

于 2009-02-18T16:14:55.297 に答える
6

あなたの例では、はインターフェースにmove()属し、Vehicle「ポイントAからポイントBに行く」コントラクトを定義します。

GroundVehicleWaterVehicle拡張するとVehicle、暗黙的にこのコントラクトを継承します(アナロジー:List.containsコントラクトを継承しCollection.containsます-何か別のものを指定した場合は想像してみてください!)。

したがって、具体的なAmphibianVehicle実装の場合、move()実際に尊重する必要のあるコントラクトはVehicle'sです。ダイアモンドがありますが、ダイアモンドの片側を考慮しても、もう一方を考慮しても契約は変わりません(または、それを設計上の問題と呼びます)。

サーフェスの概念を具体化するために「移動」のコントラクトが必要な場合は、この概念をモデル化しないタイプで定義しないでください。

public interface GroundVehicle extends Vehicle {
    void ride();
}
public interface WaterVehicle extends Vehicle {
    void sail();
}

(アナロジー:get(int)のコントラクトはListインターフェイスによって定義されます。Collectionコレクションは必ずしも順序付けられていないため、によって定義することはできませんでした)

または、ジェネリックインターフェイスをリファクタリングして、次の概念を追加します。

public interface Vehicle {
    void move(Surface s) throws UnsupportedSurfaceException;
}

複数のインターフェースを実装するときに私が目にする唯一の問題は、まったく関係のないインターフェースからの2つのメソッドがたまたま衝突した場合です。

public interface Vehicle {
    void move();
}
public interface GraphicalComponent {
    void move(); // move the graphical component on a screen
}
// Used in a graphical program to manage a fleet of vehicles:
public class Car implements Vehicle, GraphicalComponent {
    void move() {
        // ???
    }
}

しかし、それはダイヤモンドではありません。逆三角形のようなものです。

于 2009-02-18T18:01:24.323 に答える
6

C#これに部分的に対処するための明示的なインターフェース実装があります。少なくとも、中間インターフェースの1つ(そのオブジェクト..)を持っている場合

ただし、AmphibianVehicle オブジェクトは、現在水上にあるか陸上にあるかを認識し、正しいことを行う可能性があります。

于 2009-02-18T16:14:46.670 に答える
5

人々は、Java はダイヤモンドの問題を知らないと主張します。インターフェイスを使用した多重継承のみを行うことができます。それらには実装がないため、ダイヤモンドの問題はありません。これは本当ですか?

はい、D でインターフェイスの実装を制御するためです。メソッドのシグネチャは両方のインターフェイス (B/C) で同じであり、インターフェイスが実装されていないことを確認すると、問題はありません。

于 2009-02-18T16:12:56.330 に答える
4

インターフェイスベースの継承にはダイヤモンド問題はありません。

クラスベースの継承では、複数の拡張クラスがメソッドの異なる実装を持つことができるため、実行時にどのメソッドが実際に使用されるかについてあいまいさが生じます。

インターフェイスベースの継承では、メソッドの実装が 1 つしかないため、あいまいさはありません。

編集:実際には、スーパークラスで抽象として宣言されたメソッドのクラスベースの継承にも同じことが当てはまります。

于 2009-02-18T16:15:55.357 に答える
4

私はJavaを知りませんが、インターフェイスBとCがインターフェイスAから継承され、クラスDがインターフェイスBとCを実装する場合、クラスDはmoveメソッドを1回実装するだけで、実装する必要があるのはA.Moveです。あなたが言うように、コンパイラはこれに問題はありません。

GroundVehicle と WaterVehicle を実装する AmphibianVehicle に関する例から、たとえば環境への参照を格納し、AmphibianVehicle の Move メソッドが検査する Surface プロパティを環境に公開することで、これを簡単に解決できます。これをパラメーターとして渡す必要はありません。

プログラマーが解決するものであるという意味では正しいですが、少なくともコンパイルされ、「問題」になるべきではありません。

于 2009-02-18T16:13:27.140 に答える
3

Student / Teacherの例で見られる問題は、データモデルが間違っているか、少なくとも不十分であるということです。

StudentクラスとTeacherクラスは、それぞれに同じ名前を使用することで、「部門」の2つの異なる概念を混同しています。この種の継承を使用する場合は、代わりに、Teacherで「getTeachingDepartment」、Studentで「getResearchDepartment」のようなものを定義する必要があります。教師と生徒の両方であるGradStudentは、両方を実装します。

もちろん、大学院の現実を考えると、このモデルでさえおそらく不十分です。

于 2010-04-26T23:07:42.497 に答える
3

GroundVehicle と WaterVehicle を継承する AmphibianVehicle インターフェースがあることがわかっている場合、その move() メソッドをどのように実装しますか?

sに適した実装を提供しますAmphibianVehicle

aGroundVehicleが「異なる」動きをする (つまり、 a とは異なるパラメータを取るWaterVehicle) 場合、 はAmphibianVehicle2 つの異なるメソッドを継承します。1 つは水上用、もう 1 つは地上用です。これが不可能な場合は、およびAmphibianVehicleから継承すべきではありません。GroundVehicleWaterVehicle

ひし形の問題は常に悪いクラス設計の原因であり、プログラマーもコンパイラーも解決する必要のないものですか?

原因がクラス設計の悪さである場合、それを解決する必要があるのはプログラマーです。コンパイラーはその方法を知らないからです。

于 2009-02-18T16:21:11.350 に答える
1

move() が Ground または Water (GroundVehicle および WaterVehicle インターフェース自体が move() シグネチャを持つ GeneralVehicle インターフェースを拡張するのではなく) であることに基づいてセマンティックな違いがある場合、地上と水の実装者を混合して一致させることが期待されます。例 1 は、設計が不十分な API の 1 つです。

本当の問題は、名前の衝突が事実上偶発的である場合です。例(非常に合成):

interface Destructible
{
    void Wear();
    void Rip();
}

interface Garment
{
    void Wear();
    void Disrobe();
}

衣服であり破壊可能であるジャケットを持っている場合、(正式に命名された) 着用方法で名前の衝突が発生します。

Java にはこれに対する解決策がありません (他のいくつかの静的型付け言語についても同じことが言えます)。ダイアモンドまたは継承がなくても、動的プログラミング言語には同様の問題があります。これは単なる名前の衝突です (Duck Typing に固有の潜在的な問題)。

.Net には明示的なインターフェイス実装の概念があり、クラスは同じ名前とシグネチャの 2 つのメソッドを定義できますが、両方が 2 つの異なるインターフェイスにマークされている必要があります。呼び出す関連メソッドの決定は、変数のコンパイル時の既知のインターフェイスに基づいています (または、呼び出し先の明示的な選択によるリフレクションによる場合)。

合理的で可能性の高い名前の衝突は非常に困難であり、Java は明示的なインターフェース実装を提供しないために使用できないとしてさらされていないということは、この問題が実際の使用にとって重大なものではないことを示唆しています。

于 2009-02-18T19:33:49.220 に答える
1

具体的な多重継承を防止することが、問題をコンパイラーからプログラマーに移しているとは思いません。あなたが示した例では、プログラマーが使用する実装をコンパイラーに指定する必要があります。コンパイラがどちらが正しいかを推測する方法はありません。

両生類クラスの場合、車両が水上にあるか陸上にあるかを判断するメソッドを追加し、これを使用して使用する移動メソッドを決定できます。これにより、パラメーターのないインターフェイスが保持されます。

move()
{

  if (this.isOnLand())
  {
     this.moveLikeLandVehicle();
  }
  else
  {
    this.moveLikeWaterVehicle();
  }
}
于 2009-02-18T16:20:58.713 に答える
1

この場合、そもそも問題を完全に回避するために、AmphibiousVehicle を Vehicle のサブクラス (WaterVehicle と LandVehicle の兄弟) にすることがおそらく最も有利です。いずれにせよ、水陸両用車は水上車両でも陸上車両でもなく、まったく別のものであるため、おそらくもっと正しいでしょう。

于 2009-02-18T16:30:15.323 に答える
0

これは特定の例であり、一般的な解決策ではないことは理解していますが、状態を判断し、車両が実行する move() の種類を決定する追加のシステムが必要なようです。

水陸両用車の場合、発信者(「スロットル」としましょう)は水/地面の状態について何も知らないようですが、「トラクションコントロール」と組み合わせた「トランスミッション」のような中間決定オブジェクトはおそらくそれを理解してから、適切なパラメーター move(wheels) または move(prop) を指定して move() を呼び出します。

于 2009-02-18T16:14:44.487 に答える
0

問題は実際に存在します。サンプルでは、​​AmphibianVehicle-Class に別の情報、つまりサーフェスが必要です。私が好む解決策は、AmpibianVehicle クラスにゲッター/セッター メソッドを追加して、サーフェス メンバー (列挙型) を変更することです。実装は正しいことを行うことができ、クラスはカプセル化されたままになります。

于 2009-02-18T16:16:30.427 に答える
0

実際、StudentTeacherが両方ともインターフェースである場合、実際には問題を解決します。それらがインターフェイスである場合、クラスgetDepartmentに表示する必要があるのは単なるメソッドです。とインターフェイスGradTeachingFellowの両方がそのインターフェイスを強制するという事実は、まったく競合しません。クラスに実装すると、ダイヤモンドの問題なしに両方のインターフェースが満たされます。StudentTeachergetDepartmentGradTeachingFellow

しかし、コメントで指摘されているように、これは、ある部門でのGradStudent教育/ TAであり、別の部門での学生であるという問題を解決しません。カプセル化は、おそらくここで必要なものです。

public class Student {
  String getDepartment() {
    return "Economics";
  }
}

public class Teacher {
  String getDepartment() {
    return "Computer Engineering";
  }
}

public class GradStudent {
  Student learning;
  Teacher teaching;

  public String getDepartment() {
    return leraning.getDepartment()+" and "+teaching.getDepartment(); // or some such
  }

  public String getLearningDepartment() {
    return leraning.getDepartment();
  }

  public String getTeachingDepartment() {
    return teaching.getDepartment();
  }
}

GradStudentが概念的に教師と生徒を「持っていない」ことは問題ではありません。カプセル化はまだ道のりです。

于 2010-04-26T07:49:08.463 に答える
0

インターフェイス A { void add(); }

インターフェイス B は A を拡張します { void add(); }

インターフェイス C は A { void add(); を拡張します。}

クラス D は B,C を実装します {

}

ダイヤの問題じゃない?

于 2011-02-07T07:35:45.767 に答える
0

C++ (多重継承が可能) ではひし形の問題が発生する可能性がありますが、Java や C# では発生しません。2 つのクラスから継承する方法はありません。具象メソッドの実装はクラスでのみ行うことができるため、同じメソッド宣言で 2 つのインターフェイスを実装しても、この状況では意味がありません。

于 2009-02-18T16:23:12.610 に答える