c# - 如何强制解耦对象之间的约束?

标签 c# events decoupling intercept

注意-我将原始帖子移至底部,因为我认为它对于该主题的新手仍然有价值。下面直接进行的是尝试根据反馈来重写问题。

完全编辑过的帖子

好的,我将尝试详细说明我的特定问题。我意识到我将域逻辑与接口/表示逻辑混为一谈,但老实说,我不确定该在何处将其分开。请多多包涵 :)

我正在编写一个应用程序,该应用程序(除其他事项外)执行物流模拟以将物料移动。基本思想是用户看到一个类似于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,它是一组指标,这些指标基本上描述了此费用的多少以及计划的执行情况(以Project Triangle为例)

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, 


因此,基本上,当用户尝试通过world.RemoveItem(item)调用从WorldState(甚至整个Project)中删除项目时,我想确保在使用该WorldState的任何Plan对象中均未引用该项目。如果是这样,我想告诉用户“嘿!以下Plan X正在使用该物品!在尝试删除它之前,请先处理它!”。我不希望通过world.RemoveItem(item)调用进行的行为是:


删除该项目,但仍然有计划引用它。
删除项目但让计划静默删除其列表中引用该项目的所有元素。 (实际上,这可能是可取的,但仅作为第二选择)。


因此,我的问题基本上是如何以一种清晰分离的方式实现这种期望的行为。我曾考虑过将其作为用户界面的权限(因此,当用户在项目上按“ del”键时,它将触发对Plan对象的扫描并在调用world.RemoveItem(item)之前执行检查)-但是(a)我还允许用户编写和执行自定义脚本,以便他们可以自己调用world.RemoveItem(item),并且(b)我不认为此行为是纯粹的“用户界面”问题。

ew好吧,我希望有人还在读...

原始帖子

假设我有以下课程:

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可以说是合适的)和复杂的代码,而这些代码从等式的所有者方面基本上无关紧要。

那么这种事情通常如何处理?我考虑过的事情:


我考虑过使用类似于ComponentModel INotifyPropertyChanging / PropertyChanging模式的事件,除了EventArgs具有某种Veto()或Error()功能(就像winforms允许您使用键或取消表单退出一样)。但是我不确定这是否构成事件滥用。
另外,您也可以通过明确定义的界面自行管理事件,例如


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);
        }
    }
}


但是我不确定这是否更好。我也不确定以这种方式滚动自己的事件是否会对性能产生某些影响,或者是否有其他原因说明这可能是个好主意。


第三种选择可能是使用PostSharp或IoC /依赖注入容器的一些非常古怪的aop。我还没有准备好走那条路。
上帝的对象负责所有检查,依此类推-只是在stackoverflow中搜索god object给我的印象是很不好


我主要担心的是,这似乎是一个相当明显的问题,我认为这是一个相当普遍的问题,但是我还没有看到对此的任何讨论(例如,System.ComponentModel没有提供否决否决PropertyChanging事件的功能-是吗?);这使我担心,我(再一次)未能掌握总体的耦合或(更糟糕的)面向对象设计中的一些基本概念。

注释?
}

最佳答案

根据修改后的问题:

我在想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不能告诉它有多少引用,也不能控制它自己的用法。

最好的办法是可以理解的合同... WorldState同意在Plan使用某物品的同时不删除该物品,并且Plan同意告诉WorldState它在使用某物品。如果Plan没有延期合同的终止,则它可能最终处于无效状态。不幸的是,Plan,这就是您不遵守规则所得到的。

您不使用事件的原因是因为您需要返回值。另一种选择是让WorldState公开一种添加IPlan类型的“侦听器”的方法,其中IPlan定义了CheckItemForUse(Item item)。但是您仍然必须依靠Plan通知WorldState在删除项目之前询问。

我看到的一个巨大缺口:在您的示例中,您创建的PlanWorldState stuffAtMyHouse无关。例如,您可以创建一个Plan将您的狗带到海滩,并且Plan会很高兴(当然,您必须创建一条狗Item)。编辑:您的意思是将stuffAtMyHouse传递给Plan构造函数,而不是myHouse

因为它们没有绑在一起,所以您目前不在乎是否从stuffAtMyHouse卸下自行车...因为您当前在说的是“我不在乎自行车在哪里开始,我不在乎它在哪里? ,只需将它带到海滩即可。”但是,您的意思(我相信)是“从家中拿出我的自行车去海滩”。 Plan需要具有起始WorldState上下文。

TLDR:最好的去耦方法是让Plan选择WorldState在删除项目之前应查询的方法。

HTH,
詹姆士



原始答案
我还不是100%清楚您的目标是什么,也许这只是强迫性的例子。一些可能性:


I.在SpaceBase.Dock(myShip)之类的方法上强制最大船只尺寸

非常简单直接...当调用时,SpaceBase会跟踪大小,并在试图停靠的船上抛出TooBigToDockException,如果船太大。在这种情况下,实际上没有任何耦合……您不会通知新的最大船型,因为管理最大船型不是船的责任。

如果最大飞船尺寸减小,您将迫使飞船再次停靠...,飞船不需要知道新的最大船体尺寸(尽管可以通过事件或界面告知它现在漂浮在太空中) 。舰船对此决定没有发言权或否决权。基地已经决定它太大了,已经下水了。

您的猜想是正确的。明确界定的责任使他们从设计中消失了。


二。 SpaceBase的可查询属性

如果您想让一艘船问您是否太大而无法停靠,则可以公开此属性。再一次,您并没有真正地耦合在一起……您只是让飞船根据此属性决定是否停靠。但是如果基地太大,基地就不相信这艘船不停靠……基地仍然会检查对Dock()的调用并抛出异常。

检查与码头相关的约束的责任完全在于基地。


三,作为真正的结合,当双方都需要信息时

为了停靠,基地可能需要控制飞船。这里的接口ISpaceShip是适当的,它可能具有Rotate()MoveLeft()MoveRight()之类的方法。

在这里,您可以避免通过接口本身的优点进行耦合...每艘船都将以不同的方式实现Rotate() ...根本不在乎,只要它可以调用Rotate()并使船到位即可。如果NoSuchManeuverException不知道如何旋转,它可能会把它扔掉,在这种情况下,基地决定做出尝试不同的尝试或拒绝船坞的决定。对象进行通信,但是它们没有超出接口(契约)的耦合,并且基地仍然负责对接。


IV。在MaxShipSize设置器上进行验证

您谈论的是,如果调用方试图将MaxShipSize设置为小于停靠的船舶,则会向调用方引发异常。但是,我必须问,谁在尝试设置MaxShipSize,为什么? MaxShipSize应该已经在构造函数中设置并且是不可变的,或者设置大小应该遵循自然规则,例如您不能将飞船的尺寸设置为小于其当前尺寸,因为在现实世界中,您会扩展一个SpaceBase,但永远不会缩小它。

通过防止不合逻辑的更改,可以解决强制取消对接以及随之发生的通信的问题。


我要说的是,当您觉得代码变得不必要的复杂时,您几乎总是对的,而首先要考虑的是底层设计。而在代码中,更少总是更多。当您谈论编写Veto()和Error()以及“收集过大的飞船”的其他方法时,我开始担心代码将变成Rube Goldberg机器。而且我认为,责任与封装的分离会减少您正在经历的许多不必要的复杂性。

这就像一个有水管问题的水槽...您可以放入各种弯头和管道,但是正确的解决方案通常是简单,直截了当且优雅的。

HTH,
詹姆士

关于c# - 如何强制解耦对象之间的约束?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/4126849/

相关文章:

c# - 如何根据所选数据自定义同步,只有同步框架/技巧才能做到

javascript - 关闭网络套接字不会在服务器上引起事件

java - 模态窗口中的事件处理(Java swing)

javascript - 脱钩客户端是什么意思?

c# - 在 C# 中读取和写入 XML 文件的值

c# - 还有其他方法可以访问 C# 中的打开表单吗?

c# - 在多个文件中搜索多个字符串的 Grep 工具

c# - 从 Sender 获取 GridView 对象

decoupling - 数据库好的系统解耦点?

python - Django 可插拔动态导航应用程序