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, 

因此,基本上当用户尝试通过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 中搜索上帝对象给我的印象是这是坏的和错误的

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

评论?}

4

5 回答 5

1

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

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

于 2010-11-08T18:51:08.513 回答
1

您可以执行类似 C++ STL 特征类之类的操作 - 实现一个SpaceBase<Ship, Traits>具有两个参数化Types 的泛型 - 一个定义SpaceShip成员,另一个使用类来约束 sSpaceBase及其s来封装基的特征,例如对船舶的限制可以包含。SpaceShipSpaceBaseTraits

于 2010-11-08T18:51:35.070 回答
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告诉它有多少引用,或者控制它自己的使用。

您可以拥有的最好的合同是理解合同...WorldState同意如果 a 正在使用该项目,则不删除Plan它,并Plan同意告诉WorldState它正在使用该项目。如果 aPlan不坚持其合同的结束,那么它可能最终处于无效状态。运气不好,Plan这就是你不遵守规则的结果。

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

我看到的一个巨大差距:在您的示例中,Plan您创建的内容与WorldStatestuffAtMyHouse 无关。例如,您可以创建一个Plan将您的狗带到海滩,并且Plan会非常高兴(Item当然,您必须创建一个 dog)。 编辑:你的意思是传递stuffAtMyHousePlan构造函数,而不是myHouse

因为它们没有系好,所以您目前不在乎是否将自行车从 stuffAtMyHouse 中移除......因为您目前所说的是“我不在乎自行车从哪里开始,我不在乎它属于哪里,把它带到海滩上”。但你的意思(我相信)是“把我的自行车从我家拿走,去海滩。” Plan需要有一个起始上下文WorldState

TLDR: 您可以希望的最好的解耦是让我们Plan选择WorldState在删除项目之前应该查询什么方法。

HTH,
詹姆斯



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


一、实施最大船舶尺寸等方法SpaceBase.Dock(myShip)

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

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

您的怀疑是正确的……上帝的对象通常是坏的;明确界定的职责使它们从设计中消失得无影无踪。


二、SpaceBase 的可查询属性

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

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


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

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

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


四。MaxShipSize 设置器上的验证

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

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


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

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

HTH,
詹姆斯

于 2010-11-09T00:38:12.060 回答
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 回答