web-dev-qa-db-ja.com

ポリモーフィズムのケーススタディ-モーフィングのデザインパターン?

2つのクラスを想像してください。

class Person {
    MarriedPerson marry(Person other) {...}
}
class MarriedPerson extends Person {
}

これは、メソッドPersonが呼び出されたときに、MarriedPersonが自動的にmarryと入力するように「モーフィング」されるという考え方です。このため、MarriedPersonのインスタンスを作成し、最初のPersonオブジェクトからデータを転送する必要があります。 Personオブジェクトはまだ存在しますが、これは悪いことです。別の方法は、デザインパターンの適用です...

  • 問題に合うのはどのデザインパターンですか?
  • この関係をモデル化するより良い方法はありますか?
  • (オフトピック)Javacustomダウンキャストを定義する機能はありますか?

これは宿題ではありません。コメントしてから気になった here

29
akuzminykh

これは、実際にアイデアを実演するためのかなり不自然な例であることを知っています。しかし、経験則として、サブタイプのポリモーフィズムに依存する前に、可能なすべてのオプションを使い果たしたいと思います。ここでは、ドメインに基づいた構成を使用する方が優れたソリューションです。

結婚は人種、富、利害などを超えて人の「タイプ」を定義するものではありません。したがって、このケースをモデル化するためにサブタイプ多態性を使用することは適切ではありません。より技術的なレベルでは、ほとんどの言語は単一継承のみをサポートしています。したがって、MarriedPersonPacificIslanderPerson、およびWealthyPersonのクラスがある場合、これらを一緒に「構成」して、裕福な既婚の太平洋諸島人を記述する方法はありません。

代わりに、Personクラス内で単純な構成を使用します。

public class Person {
  public MarriageStatus marriageStatus;
  public Race race;
  public Wealth wealth;
}

ここで、MarriageStatusRace、およびWealthはすべて単一の責任を持つことができ、おそらくかなり単純です。 MarriageStatusの例は次のとおりです。

public class MarriageStatus {
  public Datetime anniversary;
  public Person husband;
  public Person wife;

  // TODO: In the future the stakeholder would like to support polyamory
//  public List<Person> spouses;
}

HaskellやRust with traits(Haskell用語の型クラス))などのプログラミング言語を使用している場合は、Personを関数の観点から自動的にMarriedPersonのように動作させることができます。 OOP言語の場合、ビジネスロジックはMarriageStatusRace、およびWealthオブジェクトでのみ機能します。これらはPersonこれら3つの合成プロパティ間の相互作用が必要な場合。

このようにして、再帰的な関係とそのすべての落とし穴から自分を設計しました。


質問のポイントを完全に逃してしまったことをお詫び申し上げます。具体的に言うと

Personオブジェクトはまだ存在しますが、これは悪いことです。

それは必ずしも本当だとは思いません。 MarriedPersonオブジェクトを返し、元の "Person"への参照がない場合、ガベージコレクターは古い "Person"オブジェクトを削除します。私はあなたを誤解しているかもしれません。

90

personは、結婚しているかどうかにかかわらず、同じ人物です。コピー置換アプローチでそれを別の種類のpersonにモーフィングしても、この基本的なアイデンティティは維持されません。

他のいくつかの代替案を検討することができます:

  • candied_orange ですでに提案されている状態パターンは、同じ人物を維持することを許可しますが、状態に応じてその人物の振る舞いを変更するには、 継承に対する構成 を使用します。
  • デコレータパターン は、関連するもう1つの代替手段です。継承よりも構成を使用して、オブジェクトに責任を追加します。それはあなたのソリューションに非常に似ていますが、モーフィングの代わりに、最初の人を参照するデコレーターを作成します。しかし、それはあなたの解決策の欠点を共有します:人の単一のアイデンティティがどういうわけか失われます。
  • 別のアプローチは、単一の人を維持し、結婚した人を関係に関連付けられている通常の人の役割と見なすことです。役割は、一種の 戦略パターン として機能します。このアプローチはデコレータほど柔軟ではなく、状態のように動作を変更するのは簡単ではありません。しかし、それは探索する価値があります。
  • 配偶者についての問い合わせを追加したいだけなら、最終的な代替案は過度に設計されたように見えるかもしれませんが、究極の柔軟性を与えることができます: エンティティコンポーネントシステム :人はコアな人間の行動のみを実装します。人はコンポーネントのコンテナになります。各コンポーネントは、プロパティと動作のクラスターを担当します。私は同意します。これはもっと複雑ですが、興味深いことに、離婚して再婚したが、最初の配偶者に対する義務をまだ負っている人に対処できるのは、4つの選択肢のうちの1つだけです。
22
Christophe

enter image description here

状態パターン をここで適用できます。しかし、関係をモデル化するより良い方法があるかどうかは、使用するコードのニーズに完全に依存します。使用するコードを提供していないので、divorce()fileTaxes()などの関数を想像するしかありません。しかし、それらがあなたのニーズに合っているかどうかはわかりません。

使用という観点からは、自分でデザインをするのがとても好きです。これら2つのクラスから始めて、それらのすべての使用法を想像しようとすると、多くのオーバーデザインを招きます。

これらのクラスもあるかもしれないさまざまなニーズに適用される他のパターンを想像することができますが、それに対する妥当な終わりが見当たらないので、ここで停止すると思います。

9
candied_orange

MarriedPersonは、実際にはPersonが実装できない可能性のある異なる動作を示しますか?いいえの場合、クラスは必要ありません。

その上、私は人の内に人の結婚ステータスを置くことを避けます。 2つのPersonオブジェクトが関係するMarriageオブジェクトを用意し、そのMarriageを適切な台帳に入れます。そうすれば、一貫性のあるデータを確保するのが簡単になります。

7
Kafein

サブタイプのポリモーフィズムはこの特定の問題に対する誤った解決策であり、さらに一般的には、それが通常は最善の解決策ではないという他の回答にも同意します。

とはいえ、isサブタイプ多態性を使用したこの問題の標準的な解決策があり、他の状況で役立つ場合があります。このソリューションは、最初はJames Coplienによって正式に説明され、さまざまな名前で呼ばれていますが、最も一般的に知られているのは envelope/letter idiom /です。 (取り出して、同じ人宛の異なる内容の手紙と交換できる手紙が入った封筒に似ているため)。

両方のクラス(例ではPersonMarriedPerson)は同じ抽象基本クラスの具体的な実装(「文字」)であり、両方にでアクセスします一般に同じインターフェイスも実装し、内部的にレタークラスのインスタンスに委ねるエンベロープクラス。

関係をより明確にするために、以下ではPersonUnmarriedPersonに名前変更し、Personをエンベロープクラスの名前として使用しました。

interface AbstractPerson {
    String name();
    boolean married();
}

class UnmarriedPerson implements AbstractPerson {
    private final String name;

    UnmarriedPerson(final String name) { this.name = name; }

    public String name() { return name; }
    public boolean married() { return false; }
}

class MarriedPerson implements AbstractPerson {
    private final String name;

    MarriedPerson(final String name) { this.name = name; }

    public String name() { return name; }
    public boolean married() { return true; }
}

class Person implements AbstractPerson {
    private AbstractPerson handle;

    public Person(final String name) { handle = new UnmarriedPerson(name); }

    public String name() { return handle.name(); }
    public boolean married() { return handle.married(); }

    public void marry(Person other) {
        if (married() || other.married()) {
            throw new IllegalStateException("already married");
        }

        setMarried();
        other.setMarried();
    }

    private void setMarried() { handle = new MarriedPerson(name()); }
}

class Main {
    static void print(final Person p) {
        System.out.printf("%s is %s\n", p.name(), p.married() ? "married" : "single");
    }

    public static void main(String[] args) {
        final Person sue = new Person("Sue");
        final Person andy = new Person("Andy");

        print(sue);        // “Sue is single”
        print(andy);       // “Andy is single”
        sue.marry(andy);
        print(sue);        // “Sue is married”
        print(andy);       // “Andy is married”
    }
}

ただし、他の回答で述べられているすべての理由により、この特定のケースではこのソリューションを使用しないでください/すべきではありません繰り返します。封筒/手紙のイディオムが良い解決策である具体的な例を考えるのはかなり難しいです:それで「実際の状況」をモデル化しようと試みることはおそらくお勧めしません。

代わりに、本当の用途は、抽象データ型に動的にスワップアウトする必要がある複数の実装がある場合です。データの特性に応じてMatrixまたはSparseMatrixとして実装できるDenseMatrixクラスなど、最適な実装戦略が実行後に変更される可能性がある場所行列の操作。

1
Konrad Rudolph

人が結婚すると、本来の人物の正確なコピーである結婚した人物が作成され、元の人物が破棄されると主張することができます。

幸いなことに、オブジェクト指向の世界は、現実の世界よりもはるかに論理的であることがよくあります。この場合、MarriedPersonクラスが存在する理由はまったくわかりません。このクラスがPersonクラスと異なるのは正確には何ですか?

元のクラスの状態を区別するためだけにサブタイピングすることは、一般に非常に悪い考えです。変更する必要のある属性を元のクラスに追加するだけでよいのです。

1
Vladimir Stokic

結婚したらその人に行動を変えてもらいたい場合は、訪問者パターンを使用してその行動を実装します。次に、結婚行為は、どの訪問者が適切な行動を実行したかを切り替えます。

確かに、オブジェクトが動的に動作を変更したい場合があります。しかし、これは戦略またはビジターパターンのいずれかを介して行うことができます。

0
Kirk