【问题标题】:How to enforce constraints between decoupled objects?如何在解耦对象之间强制执行约束?
【发布时间】:2010-11-08 18:36:24
【问题描述】:

注意 - 我将原来的帖子移到了底部,因为我认为它对这个帖子的新手仍然很有价值。下面直接是根据反馈重写问题的尝试。

完全编辑的帖子

好的,我将尝试详细说明我的具体问题。我意识到我正在将域逻辑与接口/表示逻辑混合一点,但老实说我不确定在哪里分开它。请多多包涵:)

我正在编写一个应用程序,该应用程序(除其他外)执行物流模拟以移动物品。基本思想是用户看到一个类似于 Visual Studio 的项目,她可以在其中添加、删除、命名、组织、注释等等我将要概述的各种对象:

  • ItemsLocations 是基本的无行为数据项。

    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, 

因此,基本上,当用户尝试通过world.RemoveItem(item) 调用从 WorldState(可能是整个项目)中删除项目时,我想确保在使用该 WorldState 的任何 Plan 对象中不引用该项目。如果是,我想告诉用户“嘿!下面的 X 计划正在使用这个项目!在尝试删除它之前去处理它!”。我在world.RemoveItem(item) 通话中想要的那种行为是:

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

所以我的问题基本上是如何以一种完全解耦的方式实现这种期望的行为。我曾考虑将其作为用户界面的权限(因此,当用户在某个项目上按下“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;
}

因此假设存在一个约束,即 Spaceship 的大小必须小于或等于其 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. 第三种选择可能是使用 PostSharp 或 IoC/依赖注入容器的一些非常古怪的 aop。我还没有完全准备好走这条路。

  2. 管理所有检查等的上帝对象 - 仅在 stackoverflow 中搜索 god object 给我的印象是不好和错误

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

评论? }

【问题讨论】:

  • 在改版后的帖子中,Plan 定义了两次,但没有 Simulation 类定义...是否应该为 Simulation 定义定义?
  • 感谢现场 - 我已经修正了这个错误

标签: c# events decoupling intercept


【解决方案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 没有办法告诉它有多少引用,或者控制它自己的使用。

您可以拥有的最好的合同是理解合同...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% 清楚你的目标是什么,也许这只是强制的例子。一些可能性:


我。在 SpaceBase.Dock(myShip)

等方法上强制执行最大船舶尺寸

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

如果最大船舶尺寸减小,您将强制船舶脱离停靠...再次,船舶不需要知道新的最大尺寸(尽管告诉它它现在漂浮在太空中的事件或界面可能适当)。这艘船对这个决定没有发言权或否决权……基地认为它太大了,已经启动了它。

你的怀疑是正确的......上帝的对象通常是坏的;明确界定的职责使它们从设计中烟消云散。


二。 SpaceBase 的可查询属性

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

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


三。作为真正的耦合,当信息对双方都是必要的时候

为了停靠,基地可能需要控制飞船。这里有一个合适的接口ISpaceShip,它可能有Rotate()MoveLeft()MoveRight()等方法。

在这里,您可以通过接口本身避免耦合...每艘船都会以不同的方式实现Rotate()...基地不在乎,只要它可以调用Rotate()并让船转入地方。如果 NoSuchManeuverException 不知道如何旋转,它可能会被船抛出,在这种情况下,基地会决定尝试不同的东西或拒绝停靠。对象之间进行通信,但它们并没有超出接口(契约)耦合,并且基础仍然负责对接。


四。 MaxShipSize 设置器上的验证

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

通过防止不合逻辑的更改,您可以使强制脱离以及随之而来的通信变得毫无意义。


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

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

HTH,
詹姆斯

【讨论】:

  • 主要问题是IV。我已经更新了我的帖子,试图提供一个更具体(并且希望是合法的)问题,欢迎您的 cmets。我确实同意我的代码变得不必要地复杂 - 问题是我如何在不丢失我想要的功能的情况下简化我的设计。
  • 修改后评论:你说的很对,我的意思是把 stuffAtMyHouse 注入到计划中,并且已经这样修改了!我还没有时间完全消化您的新 cmets,但是一旦我这样做了,我会回复 - 感谢您的意见!
【解决方案2】:

你知道宇宙飞船必须有一个尺寸;将 Size 放在基类中,并在访问器中实现验证检查。

我知道这似乎过于关注您的具体实现,但这里的重点是您的期望并不像您期望的那样解耦;如果您对派生类中某些东西的基类有强烈的期望,那么您的基类对派生类的基本期望是提供了它的实现;不妨将这种期望直接迁移到基类,在那里您可以更好地管理约束。

【讨论】:

  • 看来我给了错误的印象。我已将 SpaceBase 重命名为 Starport,以更清楚地表明 Spaceship 并非源自 Starport。
  • 你甚至可以把 Size 放在一个接口而不是一个基类中
【解决方案3】:

您可以执行类似 C++ STL 特征类的操作 - 实现一个通用的 SpaceBase&lt;Ship, Traits&gt;,它有两个参数化 Types - 一个定义 SpaceShip 成员,另一个限制 SpaceBase 及其 @987654326 @s 使用SpaceBaseTraits 类来封装基础的特性,例如它可以包含的船只的限制。

【讨论】:

【解决方案4】:

INotifyPropertyChanging 接口专为数据绑定而设计,这解释了为什么它没有您正在寻找的功能。我可能会尝试这样的事情:

interface ISpacebaseInterceptor<T>
{ 
    bool RequestChange(T newValue); 
    void NotifyChange(T newValue); 
} 

【讨论】:

  • 谢谢 - 这基本上是我在 2 中的建议。我想知道这是否通常被认为是可接受的做法。
  • fostandy:是的,我只是复制了您的界面并使其通用。
【解决方案5】:

您想对操作应用约束,但将它们应用于数据。

首先,为什么允许更改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

【讨论】:

  • 我已经大幅更新了问题/问题,如果它更有意义(或者不是,无论我猜如何)您的意见将不胜感激!
猜你喜欢
  • 1970-01-01
  • 2012-12-05
  • 2011-09-07
  • 2015-07-13
  • 1970-01-01
  • 2016-03-24
  • 1970-01-01
  • 1970-01-01
  • 2011-07-19
相关资源
最近更新 更多