4

- 元の投稿を一番下に移動しました。これは、このスレッドの新規参入者にとってまだ価値があると思うからです。すぐ下に続くのは、フィードバックに基づいて質問を書き直す試みです。

完全に編集された投稿

わかりました、私は特定の問題についてもう少し詳しく説明しようとします。ドメインロジックとインターフェイス/プレゼンテーションロジックを少しブレンドしていることに気づきましたが、正直なところ、どこで分離するかわかりません。我慢してください:)

私は、(とりわけ)物を移動するための物流シミュレーションを実行するアプリケーションを作成しています。基本的な考え方は、ユーザーが Visual Studio に似たプロジェクトを表示し、ここで概説しようとしているさまざまなオブジェクトを追加、削除、名前付け、整理、注釈付けなどを行うことができるということです。

  • 項目場所は、基本的な動作のないデータ項目です。

    class Item { ... }
    
    class Location { ... }
    
  • WorldStateは、アイテムと場所のペアのコレクションです。WorldState は変更可能です。ユーザーはアイテムを追加および削除したり、場所を変更したりできます。

    class WorldState : ICollection<Tuple<Item,Location>> { }
    
  • プランは、希望する時間に別の場所にアイテムを移動することを表します。これらは、プロジェクトにインポートするか、プログラム内で生成できます。WorldState を参照して、さまざまなオブジェクトの初期位置を取得します。プランも変更可能です。

    class Plan : IList<Tuple<Item,Location,DateTime>>
    {
       WorldState StartState { get; }
    }
    
  • 次に、シミュレーションがプランを実行します。それは多くのかなり複雑な動作やその他のオブジェクトをカプセル化しますが、最終的な結果はSimulationResult です。これは基本的に、このコストと計画がどれだけうまく達成されたかを説明するメトリックのセットです (プロジェクト トライアングルを考えてください)。

    class Simulation 
    {
       public SimulationResult Execute(Plan plan);
    }
    
    class SimulationResult
    {
       public Plan Plan { get; }
    }
    

基本的な考え方は、ユーザーがこれらのオブジェクトを作成し、それらを結び付け、再利用できる可能性があるということです。WorldState は、複数の Plan オブジェクトで使用できます。その後、シミュレーションは複数のプランで実行できます。

恐ろしく冗長になるリスクを冒して、例

var bicycle = new Item();
var surfboard = new Item();
var football = new Item();
var hat = new Item();

var myHouse = new Location();
var theBeach = new Location();
var thePark = new Location();

var stuffAtMyHouse = new WorldState( new Dictionary<Item, Location>() {
    { hat, myHouse },
    { bicycle, myHouse },
    { surfboard, myHouse },
    { football, myHouse },
};

var gotoTheBeach = new Plan(StartState: stuffAtMyHouse , Plan : new [] { 
    new [] { surfboard, theBeach, 1/1/2010 10AM }, // go surfing
    new [] { surfboard, myHouse, 1/1/2010 5PM }, // come home
});

var gotoThePark = new Plan(StartState: stuffAtMyHouse , Plan : new [] { 
    new [] { football, thePark, 1/1/2010 10AM }, // play footy in the park
    new [] { football, myHouse, 1/1/2010 5PM }, // come home
});

var bigDayOut = new Plan(StartState: stuffAtMyHouse , Plan : new [] { 
    new [] { bicycle, theBeach, 1/1/2010 10AM },  // cycle to the beach to go surfing
    new [] { surfboard, theBeach, 1/1/2010 10AM },  
    new [] { bicycle, thePark, 1/1/2010 1PM },  // stop by park on way home
    new [] { surfboard, thePark, 1/1/2010 1PM },
    new [] { bicycle, myHouse, 1/1/2010 1PM },  // head home
    new [] { surfboard, myHouse, 1/1/2010 1PM },

});

var s1 = new Simulation(...);
var s2 = new Simulation(...);
var s3 = new Simulation(...);

IEnumerable<SimulationResult> results = 
    from simulation in new[] {s1, s2}
    from plan in new[] {gotoTheBeach, gotoThePark, bigDayOut}
    select simulation.Execute(plan);

問題は、次のようなものが実行されるときです。

stuffAtMyHouse.RemoveItem(hat); // this is fine
stuffAtMyHouse.RemoveItem(bicycle); // BAD! bicycle is used in bigDayOut, 

したがって、基本的に、ユーザーが呼び出しを介して WorldState (およびおそらくプロジェクト全体) から項目を削除しようとするworld.RemoveItem(item)と、その項目がその WorldState を使用する Plan オブジェクトで参照されないようにする必要があります。もしそうなら、私はユーザーに「おい!次のプランXがこのアイテムを使用している!それを削除しようとする前にそれを処理してください!」と伝えたい. 通話で望ましくない種類の動作は次のとおりです。world.RemoveItem(item)

  • アイテムを削除しますが、プランはそれを参照します。
  • アイテムを削除するが、Plan にアイテムを参照するリスト内のすべての要素をサイレント モードで削除させる。(実際には、これはおそらく望ましいことですが、二次的なオプションとしてのみ使用できます)。

したがって、私の質問は基本的に、そのような望ましい動作をきれいに分離した方法で実装するにはどうすればよいかということです。これをユーザーインターフェイスの範囲にすることを検討しました(したがって、ユーザーがアイテムで「del」を押すと、プランオブジェクトのスキャンがトリガーされ、world.RemoveItem(item)を呼び出す前にチェックが実行されます)-しかし(a)私はまた、ユーザーがworld.RemoveItem(item)自分自身を呼び出すことができるように、カスタム スクリプトを作成および実行できるようにしています。(b) この動作が純粋に「ユーザー インターフェイス」の問題であるとは確信していません。

ふぅ。まあ、誰かがまだ読んでいるといいのですが...

元の投稿

次のクラスがあるとします。

public class Starport
{
    public string Name { get; set; }
    public double MaximumShipSize { get; set; }
}

public class Spaceship
{
    public readonly double Size;
    public Starport Home;
}

したがって、宇宙船のサイズがそのホームの MaximumShipSize 以下でなければならないという制約が存在するとします。

では、これにどのように対処すればよいでしょうか。

伝統的に、私は次のように結合された何かをしました:

partial class Starport
{
    public HashSet<Spaceship> ShipsCallingMeHome; // assume this gets maintained properly
    private double _maximumShipSize;
    public double MaximumShipSize
    {
        get { return _maximumShipSize; } 
        set
        {
            if (value == _maximumShipSize) return;
            foreach (var ship in ShipsCallingMeHome)
                if (value > ship)
                    throw new ArgumentException();
            _maximumShipSize = value
        }
    }
}

これは、このような単純な例では扱いやすいものですが (おそらく悪い例です)、制約が大きくなり、より複雑になるにつれて、より関連する機能が必要になることがわかりました (たとえば、メソッドを実装するbool CanChangeMaximumShipSizeTo(double)か、データを収集する追加のメソッドを実装する不要な双方向の関係 (この場合、SpaceBase-Spaceship がほぼ間違いなく適切です) と複雑なコードを書くことになり、所有者側とはほとんど関係がありません。

では、この種のことは通常どのように処理されますか? 私が検討したこと:

  1. ComponentModel INotifyPropertyChanging/PropertyChanging パターンに似たイベントを使用することを検討しましたが、EventArgs にはある種の Veto() または Error() 機能があります (winforms でキーを消費したり、フォームの終了を抑制したりできるように)。しかし、これがイベンティングの悪用に該当するかどうかはわかりません。

  2. または、明示的に定義されたインターフェースを介して自分でイベントを管理します。

asdf ここにこの行が必要です。そうしないと、フォーマットが機能しません

interface IStarportInterceptor
{
    bool RequestChangeMaximumShipSize(double newValue);
    void NotifyChangeMaximumShipSize(double newValue);
}

partial class Starport
{
    public HashSet<ISpacebaseInterceptor> interceptors; // assume this gets maintained properly
    private double _maximumShipSize;
    public double MaximumShipSize
    {
        get { return _maximumShipSize; } 
        set
        {
            if (value == _maximumShipSize) return;
            foreach (var interceptor in interceptors)
                if (!RequestChangeMaximumShipSize(value))
                    throw new ArgumentException();
            _maximumShipSize = value;
            foreach (var interceptor in interceptors)
                NotifyChangeMaximumShipSize(value);
        }
    }
}

しかし、これがより良いかどうかはわかりません。また、この方法で自分のイベントをローリングするとパフォーマンスに影響があるかどうか、またはこれが良い/悪い考えである理由が他にあるのかどうかもわかりません。

  1. 3 番目の選択肢は、PostSharp または IoC/依存性注入コンテナーを使用する非常に風変わりな aop です。私はまだその道を進む準備ができていません

  2. すべてのチェックなどを管理する神オブジェクト -神オブジェクトのスタックオーバーフローを検索するだけで、これは悪くて間違っているという印象を受けます

私の主な懸念は、これはかなり明白な問題のように思え、かなり一般的な問題だと思っていましたが、それについての議論を見たことがありません (たとえば、System.ComponentModel は PropertyChanging イベントを拒否する機能を提供していません。そうですか?); これは、私が (もう一度) カップリングまたは (さらに悪いことに) 一般的なオブジェクト指向設計のいくつかの基本的な概念を把握できなかったのではないかと心配しています。

コメント? }

4

5 に答える 5

1

インターフェイスはデータ バインディング用に設計されているため、INotifyPropertyChanging探している機能がない理由が説明されています。私はこのようなことを試すかもしれません:

interface ISpacebaseInterceptor<T>
{ 
    bool RequestChange(T newValue); 
    void NotifyChange(T newValue); 
} 
于 2010-11-08T18:58:54.377 に答える
1

改訂された質問に基づいて:

WorldStateクラスにはデリゲートが必要だと思います...そしてPlan、アイテムが使用されているかどうかをテストするために呼び出されるメソッドを設定します。のような並べ替え:

delegate bool IsUsedDelegate(Item Item);

public class WorldState {

    public IsUsedDelegate CheckIsUsed;

    public bool RemoveItem(Item item) {

        if (CheckIsUsed != null) {
            foreach (IsUsedDelegate checkDelegate in CheckIsUsed.GetInvocationList()) {
                if (checkDelegate(item)) {
                    return false;  // or throw exception
                }
            }
        }

        //  Remove the item

        return true;
    }

}

次に、プランのコンストラクターで、呼び出されるデリゲートを設定します

public class plan {

    public plan(WorldState state) {
        state.IsUsedDelegate += CheckForItemUse;
    }

    public bool CheckForItemUse(Item item) {
         // Am I using it?
    }

}

もちろん、これは非常に大雑把です。もちろん、昼食後にさらに追加してみます : ) しかし、一般的な考え方はわかります。

Plan(昼食後:) 欠点は、デリゲートを設定するために に頼らなければならないことです...しかし、それを避ける方法はまったくありません。Itemそれへの参照がいくつあるかを知る方法や、それ自体の使用を制御する 方法はありません。

あなたが持つことができる最善のことは、理解された契約です... aがアイテムを使用しているWorldState場合はアイテムを削除しないことに同意し、アイテムを使用していることを伝えることに同意します. aが契約の終了を保持しない場合、無効な状態になる可能性があります。運が悪い、それはルールに従わないために得られるものです。PlanPlanWorldStatePlanPlan

イベントを使用しない理由は、戻り値が必要だからです。別WorldStateの方法は、IPlan が定義するタイプ IPlan の「リスナー」を追加するメソッドを公開することCheckItemForUse(Item item)です。ただし、アイテムを削除する前に、aPlanが通知を要求することに依拠する必要があります。WorldState

私が見ている 1 つの大きなギャップ: あなたの例では、Planあなたが作成したものは、WorldStatestuffAtMyHouse に関連付けられていません。Planたとえば、犬をビーチに連れて行くために を作成することができ、Plan完全に満足するでしょう (Itemもちろん、 犬 を作成する必要があります)。 編集:代わりstuffAtMyHouseに、Planコンストラクターに渡すことを意味しますmyHouseか?

それらは結び付けられていないため、stuffAtMyHouse から自転車を削除しても、現在は気にしません...なぜなら、現在あなたが言っているのは、「自転車がどこから始まっても、どこに属してもかまわない」ということだからです。 、それをビーチに持っていってください。」でも、あなたが言いたいのは(私が思うに)「私の家から自転車を持って海に行ってください」ということです。Plan開始WorldStateコンテキストが必要です。

TLDR: 期待できる最善のデカップリングは、アイテムを削除する前に、PlanどのメソッドWorldStateがクエリを実行するかを選択できるようにすることです。

HTH、
ジェームズ



元の回答
あなたの目標が何であるかは私には100%明確ではありません.おそらくそれは強制的な例です. いくつかの可能性:


I.次のような方法で最大船サイズを強制するSpaceBase.Dock(myShip)

かなり簡単です... SpaceBase は呼び出されたときにサイズを追跡し、大きTooBigToDockExceptionすぎる場合はドッキングしようとしている船に を投げます。この場合、実際には結合はありません...最大船サイズの管理は船の責任ではないため、船に新しい最大船サイズを通知しません。

船の最大サイズが減少した場合、船を強制的にドッキング解除することになります... 繰り返しますが、船は新しい最大サイズを知る必要はありません (ただし、現在宇宙に浮いていることを知らせるイベントまたはインターフェイスが適切な場合があります)。 . 船はその決定について発言権も拒否権も持たないだろう...基地はそれが大きすぎると判断し、それを起動した.

あなたの疑いは正しいです...神のオブジェクトは通常悪いです。明確に線引きされた責任は、煙のパフで設計から消えます.


Ⅱ.SpaceBase のクエリ可能なプロパティ

船が大きすぎてドッキングできないかどうかを船に尋ねさせたい場合は、このプロパティを公開できます。繰り返しますが、あなたは実際には結合していません...このプロパティに基づいて、船にドッキングするかどうかを決定させているだけです。しかし、船が大きすぎる場合、基地は船がドッキングしないことを信頼していません...基地は依然として呼び出しをチェックしDock()、例外をスローします。

ドック関連の制約をチェックする責任は、しっかりとベースにあります。


III. 真のカップリングとして、情報が双方にとって必要な場合

ドッキングするために、基地は船を制御する必要があるかもしれません。ここでは、 、、ISpaceShipなどのメソッドを持つインターフェイスが適切です。 Rotate()MoveLeft()MoveRight()

ここでは、インターフェイス自体のおかげで結合を回避できます...すべての船は異なる方法で実装されます...ベースは、呼び出して船を所定の位置に向けるRotate()ことができる限り、気にしません。船が回転する方法がわからない場合、船によってRotate()Aが投げられる可能性があります。その場合、基地は別の方法を試すか、ドックを拒否するかを決定します。NoSuchManeuverExceptionオブジェクトは通信しますが、インターフェイス (コントラクト) を超えて結合されることはなく、ベースは依然としてドッキングの責任を負います。


IV. MaxShipSize セッターの検証

MaxShipSize をドッキングされた船よりも小さく設定しようとすると、呼び出し元に例外をスローすることについて話します。ただし、MaxShipSize を設定しようとしているのは誰で、その理由は? MaxShipSize がコンストラクターで設定されて不変である必要があるか、サイズの設定が自然なルールに従う必要があります。たとえば、船のサイズを現在のサイズよりも小さく設定することはできません。決して縮めないでください。

非論理的な変更を防ぐことで、強制的なドッキング解除とそれに伴うコミュニケーションを意味のないものにします。


私が言おうとしているのは、コードが不必要に複雑になっていると感じたときは、ほとんどの場合正しいということです。最初に考慮すべきことは、基礎となる設計です。そして、コードでは、少ないほど常に多くなります。Veto() と Error() の作成、および「大きすぎる船を収集する」ための追加のメソッドについて話すと、コードが Rube Goldberg マシンになってしまうのではないかと心配になります。そして、分離された責任とカプセル化により、あなたが経験している不必要な複雑さの多くが削ぎ落とされると思います.

それは、配管の問題を抱えたシンクのようなものです...あらゆる種類の曲がりやパイプを入れることができますが、正しい解決策は通常、単純で、率直で、エレガントです。

HTH、
ジェームズ

于 2010-11-09T00:38:12.060 に答える
1

宇宙船にはサイズが必要です。Size を基本クラスに配置し、そこのアクセサーに検証チェックを実装します。

これは特定の実装に過度に焦点を合わせているように見えますが、ここでのポイントは、期待が期待するほど分離されていないということです。派生クラスの何かの基本クラスに強い期待がある場合、基本クラスは、その実装を提供する派生クラスの基本的な期待をしています。その期待を、制約をより適切に管理できる基本クラスに直接移行することもできます。

于 2010-11-08T18:51:08.513 に答える
1

C++ STL 特性クラスのようなことを行うことができます - SpaceBase<Ship, Traits>2 つのパラメーター化 s を持つジェネリックTypeを実装しSpaceShipます。含むことができます。SpaceBaseSpaceShipSpaceBaseTraits

于 2010-11-08T18:51:35.070 に答える
1

アクションに制約を適用したいが、それらをデータに適用したい。

まず、なぜ変更Starport.MaximumShipSizeが許可されているのですか?「サイズを変更」すると、Starportすべての船が離陸するべきではありませんか?

これらは、何をする必要があるかをよりよく理解するための一種の質問です (そして、「正しいか間違っているか」という答えはなく、「私のものとあなたのもの」があります)。

別の角度から問題を見てください。

public class Starport
{
    public string Name { get; protected set; }
    public double MaximumShipSize { get; protected set; }

    public AircarfDispatcher GetDispatcherOnDuty() {
        return new AircarfDispatcher(this); // It can be decoupled further, just example
    }
}

public class Spaceship
{
    public double Size { get; private set; };
    public Starport Home {get; protected set;};
}

public class AircarfDispatcher
{
    Startport readonly airBase;
    public AircarfDispatcher(Starport airBase) { this.airBase = airBase; }

    public bool CanLand(Spaceship ship) {
        if (ship.Size > airBase.MaximumShipSize)
            return false;
        return true;
    }

    public bool CanTakeOff(Spaceship ship) {
        return true;
    }

    public bool Land(Spaceship ship) {
        var canLand = CanLand(ship);
        if (!canLand)
            throw new ShipLandingException(airBase, this, ship, "Not allowed to land");
        // Do something with the capacity of Starport
    }

}


// Try to land my ship to the first available port
var ports = GetPorts();
var onDuty = ports.Select(p => p.GetDispatcherOnDuty())
    .Where(d => d.CanLand(myShip)).First();
onDuty.Land(myShip);

// try to resize! But NO we cannot do that (setter is protected)
// because it is not the responsibility of the Port, but a building company :)
ports.First().MaximumShipSize = ports.First().MaximumShipSize / 2.0
于 2010-11-09T00:40:17.770 に答える