web-dev-qa-db-ja.com

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

これは、私が対処するのがいつもイライラする一般的なシナリオです。

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

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メソッドが呼び出されたときに、親の一部の値を更新する必要があります。動物の特定のしきい値が混乱した場合、動物園のIsDirtyフラグを設定する必要があるとしましょう。

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

1)変更を伝達するために、各動物は親Zooリファレンスを持つことができます。

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)
    {
        ...
    }
}

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

もう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)
    {
        ...
    }
}

私はこれらの状況に対処するための良い戦略をまだ見つけていません。他に何が利用できますか?これをどのように単純化できますか?

22
ConditionRacer

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

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

後者の方が保守がはるかに簡単で、パフォーマンスの問題に対する私の恐れが実現することはありませんでした。

編集

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

これには、AnimalsZoo参照を必要とせず、ZooがすべてのAnimal。そのメッセージにサブスクライブするすべてのZooクラスは、たとえそれがその動物の1つではなかったとしても、そのプロパティを再評価する必要があるというペナルティがあります。それは大したことではないかもしれません。 Zooインスタンスが1つまたは2つしかない場合は、おそらく問題ありません。

編集2

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

11
Scott Whitlock

ドメインを表す簡単なクラス図を作成しました。 enter image description here

AnimalはaHabitatを持っています。

Habitatは、動物の数や数を気にしません(ただし、基本的にデザインの一部であり、この場合はそうではないと説明している場合を除きます)。

ただし、AnimalHabitatごとに異なる動作をするため、注意が必要です。

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

Javaのコード例をいくつか示します(C#固有の間違いを犯したくありません)。

もちろん、このデザイン、言語、要件に合わせて独自のTweakを作成できます。

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

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を知らせて管理することができます。

5
Benjamin Albert
  • オプション1は実際にはかなり単純です。これは単なる後方参照ですが、Dwellingと呼ばれるインターフェイスで一般化し、その上にMakeMessメソッドを提供します。これにより、循環依存関係が解消されます。次に、動物が混乱すると、dwelling.MakeMess()も呼び出します。

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

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

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

だから、ここの動物園は乱雑になるので、乱雑になっています。持ってる:

_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()
_

動物が動いたら、新しいデリゲートを設定します。

  • 極端に言えば、MakeMessの「アフター」アドバイスでアスペクト指向プログラミング(AOP)を使用できます。
3
Rob

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

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

class MessEvent
{
  String DetailsOrWhatever;
}

インターフェースオプションには、オプション1と同じくらい単純であるという利点がありますが、HouseまたはFairlyLandで動物を簡単に収容することもできます。

3
svidgen

私は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がエンティティに対して変更追跡を行う方法です。それらはエンティティの周りにラッパーを作成し、それらを使用して作業させます。これらのラッパーは通常、リフレクションと動的コード生成を使用して作成されます。

2
Euphoric

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

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

1
AJ Henderson

あなたは基本的な失敗から始めます:子オブジェクトはその親について知っているべきではありません。

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

この種のシナリオが存在しないように設計を変更するのが最善の方法です。

その後、制御の反転を検討してください。副作用またはイベントを伴うMakeMessAnimalの代わりに、Zooをメソッドに渡します。 Animalが常にどこかに存在する必要がある不変条件を保護する必要がある場合は、オプション1で問題ありません。それは親ではなく、ピアアソシエーションです。

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

0
Telastyn