オブジェクトモデルの変更を伝播するためのパターン..?


22

対処するのが常にイライラする一般的なシナリオを次に示します。

親オブジェクトを持つオブジェクトモデルがあります。親には、いくつかの子オブジェクトが含まれます。このようなもの。

public class Zoo
{
    public List<Animal> Animals { get; set; }
    public bool IsDirty { get; set; }
}

各子オブジェクトにはさまざまなデータとメソッドがあります

public class Animal
{
    public string Name { get; set; }
    public int Age { get; set; }

    public void MakeMess()
    {
        ...
    }
}

子が変更されたとき、この場合はMakeMessメソッドが呼び出されたときに、親の値を更新する必要があります。Animalの特定のしきい値が混乱した場合、ZooのIsDirtyフラグを設定する必要があるとします。

このシナリオを処理する方法はいくつかあります(私が知っていることです)。

1)変更を通知するために、各動物は親動物園の参照を持つことができます。

public class Animal
{
    public Zoo Parent { get; set; }
    ...

    public void MakeMess()
    {
        Parent.OnAnimalMadeMess();
    }
}

これは、Animalをその親オブジェクトに結合するため、最悪のオプションのように感じます。家に住んでいる動物が欲しいならどうしますか?

2)別のオプションは、イベントをサポートする言語(C#など)を使用している場合、変更イベントに親をサブスクライブさせることです。

public class Animal
{
    public event OnMakeMessDelegate OnMakeMess;

    public void MakeMess()
    {
        OnMakeMess();
    }
}

public class Zoo
{
    ...

    public void SubscribeToChanges()
    {
        foreach (var animal in Animals)
        {
            animal.OnMakeMess += new OnMakeMessDelegate(OnMakeMessHandler);
        }
    }

    public void OnMakeMessHandler(object sender, EventArgs e)
    {
        ...
    }
}

これはうまくいくように見えますが、経験から維持するのが難しくなります。動物が動物園を変更する場合は、古い動物園でイベントの登録を解除し、新しい動物園で再登録する必要があります。これは、構成ツリーが深くなるにつれて悪化します。

3)もう1つのオプションは、ロジックを親に移動することです。

public class Zoo
{
    public void AnimalMakesMess(Animal animal)
    {
        ...
    }
}

これは非常に不自然に思え、ロジックの重複を引き起こします。たとえば、Zooと共通の継承親を共有しないHouseオブジェクトがある場合。

public class House
{
    // Now I have to duplicate this logic
    public void AnimalMakesMess(Animal animal)
    {
        ...
    }
}

これらの状況に対処するための良い戦略はまだ見つかりません。他に何がありますか?どうすれば簡単にできますか?


あなたは#1が悪いことについては正しいです、そして私は#2にも熱心ではありません。通常、副作用を避けたいので、代わりに副作用を増やしています。オプション#3について、どうしてAnimalMakeMessをすべてのクラスが呼び出せる静的メソッドに分解できないのですか?
ドーバル14年

4
その特定の親クラスではなく、インターフェース(IAnimalObserver)を介して通信する場合、#1は必ずしも悪くありません。
コアダンプ14年

回答:


11

私はこれに数回対処しなければなりませんでした。初めてオプション2(イベント)を使用したとき、あなたが言ったように本当に複雑になりました。そのルートに行く場合、イベントが正しく行われ、ぶら下がり参照を残していないことを確認するために非常に徹底的なユニットテストが必要になることを強くお勧めします。

2回目は、親プロパティを子の関数として実装したため、Dirty各動物にプロパティを保持してAnimal.IsDirtyreturnを実行しthis.Animals.Any(x => x.IsDirty)ます。それはモデルにありました。モデルの上にコントローラーがあり、コントローラーの仕事は、モデルを変更した後(モデルのすべてのアクションがコントローラーを通過したため、何かが変更されたことを知っていた)、特定のreを呼び出す必要があることを知ることでした-評価関数。たとえば、ZooMaintenance部門Zooが再び汚れたかどうかをチェックするようにトリガーします。またはZooMaintenance、スケジュールされた後の時刻(100ミリ秒、1秒、2分、24時間、必要に応じて)までチェックをオフにすることもできます。

後者の方が保守がずっと簡単で、パフォーマンスの問題に対する私の不安は決して実現しませんでした。

編集

これに対処する別の方法は、メッセージバスパターンです。Controller私の例でlike を使用するのではなく、すべてのオブジェクトにIMessageBusサービスを注入します。Animalこのクラスは、「混乱メイド」のように、メッセージを公開することができ、あなたのZooクラスは、「混乱メイド」のメッセージを購読することができます。メッセージバスサービスはZoo、動物がそれらのメッセージの1つをいつ公開するかを通知し、そのIsDirtyプロパティを再評価できます。

これには、参照が不要になり、すべてのイベントをサブスクライブおよびサブスクライブ解除することを心配する必要がないという利点がAnimalsあります。ペナルティは、そのメッセージにサブスクライブしているすべてのクラスが、その動物の1つではない場合でも、プロパティを再評価する必要があることです。それは大したことでもないかもしれません。インスタンスが1つまたは2つしかない場合は、おそらく問題ありません。ZooZooAnimalZooZoo

編集2

オプション1の単純さを軽視しないでください。コードを再訪する人は、コードを理解するのにそれほど問題はありません。Animalクラスを見る人にMakeMessは、いつ 呼び出されるかによってメッセージが伝播されることZooが明らかZooであり、メッセージをどこから取得するかはクラスに明らかです。オブジェクト指向プログラミングでは、メソッド呼び出しは「メッセージ」と呼ばれていました。実際、オプション1から抜け出すのに意味がZooあるのAnimalは、混乱が生じた場合に通知する必要があるのはそれ以上である場合だけです。通知する必要があるオブジェクトがさらにある場合は、おそらくメッセージバスまたはコントローラーに移動します。


5

ドメインを説明する簡単なクラス図を作成しました。 ここに画像の説明を入力してください

それぞれAnimal Habitat台無しにしています。

Habitat(それは基本的にこのような場合には、あなたはそれがない説明あなたのデザインの一部でない限り)それが持っているものか、どのように多くの動物を気にしません。

しかし、それAnimalは気にしません、なぜならそれは毎回異なる振る舞いをするからHabitatです。

この図は、戦略設計パターンの UML図に似ていますが、異なる方法で使用します。

Javaのコード例をいくつか示します(C#固有のミスをしたくない)。

もちろん、この設計、言語、および要件を独自に調整できます。

これはStrategyインターフェースです:

public interface Habitat {
    public void messUp(float magnitude);

    public float getCleanliness();
}

コンクリートの例Habitat。もちろん、各Habitatサブクラスはこれらのメソッドを別々に実装できます。

public class Zoo implements Habitat {
    public float cleanliness = 1;

    public float getCleanliness() {
        return cleanliness;
    }

    public void messUp(float magnitude) {
        cleanliness -= magnitude;
    }
}

もちろん、複数の動物サブクラスを使用することもできますが、それぞれが異なる方法で混乱させます。

public class Animel {
    private Habitat habitat;

    public void makeMess() {
        habitat.messUp(.05f);
    }

    public Animel addTo(Habitat habitat) {
        this.habitat = habitat;
        return this;
    }
}

これはクライアントクラスです。これは基本的に、この設計の使用方法を説明しています。

public class ZooKeeper {
    public Habitat zoo = new Zoo();

    public ZooKeeper() {
        new Animal()
            .addTo( zoo )
            .makeMess();

        if (zoo.getCleanliness() < 0.5f) {
            System.out.println("The zoo is really messy");
        } else {
            System.out.println("The zoo looks clean");
        }
    }
}

もちろん、実際のアプリケーションでHabitatAnimal、必要に応じて通知して管理できます。


3

過去にオプション2のようなアーキテクチャでかなりの成功を収めてきました。これは最も一般的なオプションであり、最大限の柔軟性が得られます。ただし、リスナーを制御でき、多くのサブスクリプションタイプを管理していない場合は、インターフェイスを作成することで、イベントをより簡単にサブスクライブできます。

interface MessablePlace
{
  void OnMess(object sender, MessEvent e);
}

class MessEvent
{
  String DetailsOrWhatever;
}

インタフェースオプションは、ほとんどあなたのオプション1、など簡単なようであることの利点を持っているだけでなく、中にはかなり楽家の動物にことができますHouseFairlyLand


3
  • オプション1は実際には非常に簡単です。これは単なる後方参照です。ただしDwelling、呼び出されるインターフェイスで一般化し、MakeMessメソッドを提供します。これにより、循環依存関係が解消されます。そして、動物が混乱するとき、それはdwelling.MakeMess()あまりにも呼び出します。

lex parsimoniaeの精神で、私はこの1つを使用しますが、おそらく私を知っている下のチェーンソリューションを使用します。(これは、@ Benjamin Albertが提案するモデルとまったく同じです。)

リレーショナルデータベーステーブルをモデリングしている場合、リレーションは逆の方向に進むことに注意してください。動物は動物園への参照を持ち、動物園の動物のコレクションはクエリ結果になります。

  • その考えをさらに進めると、連鎖アーキテクチャを使用できます。つまり、インターフェイスを作成し、Messable各混乱可能なアイテムにへの参照を含めますnext。混乱を作成した後MakeMess、次のアイテムを呼び出します。

だから、ここのZooは散らかるので、散らかっています。持ってる:

Zoo implements Messable
House implements Messable
Animal implements Messable
   Messable next

   MakeMess()
       messy = true
       next.MakeMess

これで、混乱が作成されたというメッセージを受け取る一連の事柄ができました。

  • オプション2、パブリッシュ/サブスクライブモデルはここで動作しますが、本当に重いと感じます。オブジェクトとコンテナには既知の関係があるため、それよりも一般的なものを使用するのは少し手間がかかりそうです。

  • オプション3:この特定のケースでは、呼び出しZoo.MakeMess(animal)House.MakeMess(animal)家が動物園よりも乱雑を取得するための異なる意味を持っている可能性があるため、実際には悪い選択肢ではありません。

チェーンルートをたどらなくても、ここには2つの問題があるように聞こえます。1)問題はオブジェクトからそのコンテナに変更を伝播すること、2)インターフェースをスピンオフしたい動物が住む場所を抽象化するコンテナ。

...

ファーストクラスの関数がある場合、関数(またはデリゲート)をAnimalに渡して、混乱した後に呼び出すことができます。これは、チェーンのアイデアに少し似ていますが、インターフェースの代わりに関数を使用する点が異なります。

public Animal
    Function afterMess

    public MakeMess()
        messy = true
        afterMess()

動物が動いたら、新しいデリゲートを設定するだけです。

  • 極端に言えば、Aspect Oriented Programming(AOP)をMakeMessに関する「後」のアドバイスとともに使用できます。

2

1にしますが、親子関係と通知ロジックを別々のラッパーにします。これにより、動物園への動物の依存関係がなくなり、親子関係の自動管理が可能になります。ただし、これには、最初に階層内のオブジェクトをインターフェース/抽象クラスに作り直し、各インターフェースに特定のラッパーを作成する必要があります。しかし、それはコード生成を使用して削除できます。

何かのようなもの :

public interface IAnimal
{
    string Name { get; set; }
    int Age { get; set; }

    void MakeMess();
}

public class Animal : IAnimal
{
    public string Name { get; set; }
    public int Age { get; set; }

    public void MakeMess()
    {
        // makes mess
    }
}

public class ZooAnimals
{
    class AnimalInZoo : IAnimal
    {
        public IAnimal _animal;
        public ZooAnimals _zoo;

        public AnimalInZoo(IAnimal animal, ZooAnimals zoo)
        {
            _animal = animal;
            _zoo = zoo;
        }

        public string Name { get { return _animal.Name; } set { _animal.Name = value; } }
        public int Age { get { return _animal.Age; } set { _animal.Age = value; } }

        public void MakeMess()
        {
            _animal.MakeMess();
            _zoo.IsDirty = true;
        }
    }

    private Collection<AnimalInZoo> animals = new Collection<AnimalInZoo>();

    public IAnimal Add(IAnimal animal)
    {
        if (animal is AnimalInZoo)
        {
            var inZoo = (AnimalInZoo)animal;
            if (inZoo._zoo != this)
            {
                // animal is in a different zoo, what to do ?
                // either move animal to this zoo
                // or throw an exception so caller is forced to remove the animal from previous zoo first
            }
        }

        var anim = new AnimalInZoo(animal, this);
        animals.Add(anim);
        return anim;
    }

    public IAnimal Remove(IAnimal animal)
    {
        if (!(animal is AnimalInZoo))
        {
            // animal is not in zoo, throw an exception?
        }
        var inZoo = (AnimalInZoo)animal;
        if (inZoo._zoo != this)
        {
            // animal is in a different zoo, throw an exception?
        }

        animals.Remove(inZoo);
        return inZoo._animal;
    }

    public bool IsDirty { get; set; }
}

これは、実際には、一部のORMがエンティティに対して変更追跡を行う方法です。それらはエンティティの周りにラッパーを作成し、それらを操作させます。これらのラッパーは通常、リフレクションと動的コード生成を使用して作成されます。


1

私がよく使う2つのオプション。2番目のアプローチを使用して、親のコレクション自体にイベントを関連付けるロジックを配置できます。

別のアプローチ(実際には3つのオプションのいずれかで使用できます)は、包含を使用することです。家や動物園などに住むことができるAnimalContainerを作成します(またはコレクションにします)。動物に関連付けられた追跡機能を提供しますが、必要なオブジェクトに含めることができるため、継承の問題を回避します。


0

あなたは基本的な失敗から始めます子オブジェクトは親について知らないはずです。

文字列はリストにあることを知っていますか?いいえ。日付はカレンダーに存在することを知っていますか?いや

最善のオプションは、この種のシナリオが存在しないように設計を変更することです。

その後、制御の反転を検討してください。代わりMakeMessAnimal副作用またはイベントに渡すZoo方法に。Animal常にどこかに住む必要がある不変式を保護する必要がある場合は、オプション1で十分です。それは親ではなく、ピアアソシエーションです。

時々2と3は大丈夫ですが、従うべき重要なアーキテクチャの原則は、子供は親について知らないということです。


これは、リストの文字列というよりも、フォームの送信ボタンに似ていると思います。
svidgen 14年

1
@svidgen-次にコールバックを渡します。イベントよりも簡単で、推論するのが簡単で、知る必要のないものへのいたずらな参照はありません。
テラスティン14年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.