注意- 我已将原始帖子移至底部,因为我认为它对于该线程的新手仍然有价值。下面直接尝试根据反馈重写问题。
完全编辑的帖子
好的,我将尝试详细说明我的具体问题。我意识到我正在将域逻辑与接口/表示逻辑混合一点,但老实说我不确定在哪里分开它。请多多包涵 :)
我正在编写一个应用程序,该应用程序(除其他外)执行物流模拟以移动物品。基本思想是用户看到一个类似于 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 可以说是合适的)和复杂的代码,这在很大程度上与等式的所有者无关。
那么这种事情一般是怎么处理的呢?我考虑过的事情:
我考虑使用事件,类似于 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 中搜索上帝对象给我的印象是这是坏的和错误的
我主要担心的是这似乎是一个相当明显的问题,我认为这是一个相当普遍的问题,但我还没有看到任何关于它的讨论(例如 System.ComponentModel 没有提供否决PropertyChanging 事件的设施 - 是吗?);这让我担心我(再次)未能掌握耦合或(更糟糕的)一般面向对象设计中的一些基本概念。
评论?}