如何在解耦对象之间强制执行约束?

发布于 10-01 08:58 字数 6276 浏览 6 评论 0 原文

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

完全编辑的帖子

好的,我将尝试详细说明我的具体问题。我意识到我将领域逻辑与接口/表示逻辑混合在一起,但说实话,我不确定在哪里将其分开。请耐心等待:)

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

  • 项目位置是基本的无行为数据项。

    类项目 { ... }
    
    类位置 { ... }
    
  • 世界状态是项目-位置对的集合。 WorldState 是可变的:用户可以添加和删除项目,或更改其位置。

    class WorldState : ICollection>> { }
    
  • 计划代表在所需时间将物品移动到不同位置。这些可以导入到项目中或在程序中生成。它引用 WorldState 来获取各种对象的初始位置。计划也是可变的。

    类计划:IList>>
    {
       世界状态 StartState { 获取; }
    }
    
  • 模拟然后执行计划。它封装了许多相当复杂的行为和其他对象,但最终结果是一个 SimulationResult,它是一组指标,基本上描述了此成本以及计划的实现情况(想想项目三角)

    类模拟 
    {
       公共模拟结果执行(计划计划);
    }
    
    模拟结果类
    {
       公共计划计划{ 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”时,它会触发对 Plan 对象的扫描并在调用 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. 第三种选择可能是使用 PostSharp 或 IoC/依赖注入容器的一些非常古怪的 aop。我还没有完全准备好走这条路。

  2. 管理所有检查等的 God 对象 - 只需在 stackoverflow 中搜索 god 对象 给我的印象这是坏且错误的

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

评论? }

Note - I have moved the original post to the bottom because I think it is still of value to newcomers to this thread. What follows directly below is an attempt at rewriting the question based on feedback.

Completely Redacted Post

Ok, I'll try to elaborate a bit more on my specific problem. I realise I am blending domain logic with interfacing/presentation logic a little but to be honest I am not sure where to seperate it. Please bear with me :)

I am writing an application that (among other things) performs logistics simulations for moving stuff around. The basic idea is that the user sees a Project, similar to Visual Studio, where she can add, remove, name, organise, annotate and so on various objects which I am about to outline:

  • Items and Locations are basic behaviourless data items.

    class Item { ... }
    
    class Location { ... }
    
  • A WorldState is a Collection of item-location pairs. A WorldState is mutable: The user is able to add and remove items, or change their location.

    class WorldState : ICollection<Tuple<Item,Location>> { }
    
  • A Plan represents the movement of items to different locations at desired times. These can either be imported into the Project or generated within the program. It references a WorldState to get the initial location of various objects. A Plan is also mutable.

    class Plan : IList<Tuple<Item,Location,DateTime>>
    {
       WorldState StartState { get; }
    }
    
  • A Simulation then executes a Plan. It encapsulates a lot of rather complex behaviour, and other objects, but the end result is a SimulationResult which is a set of metrics that basically describe how much this cost and how well the Plan was fulfilled (think the Project Triangle)

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

The basic idea is that the users can create these objects, wire them together, and potentially re-use them. A WorldState may be used by multiple Plan objects. A Simulation may then be run over multiple Plans.

At the risk of being horribly verbose, an example

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

The problem is when something like this is executed:

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

So basically when a user attempts to delete an item from a WorldState (and maybe the entire Project) via a world.RemoveItem(item) call, I want to ensure that the item is not referred to in any Plan objects which use that WorldState. If it is, I want to tell the user "Hey! The following Plan X is using this Item! Go and deal with that before trying to remove it!". The sort of behaviour I do not want from a world.RemoveItem(item) call is:

  • Deleting the item but still having the Plan reference it.
  • Deleting the item but having the Plan silently delete all elements in its list that refer to the item. (actually, this is probably desireable but only as a secondary option).

So my question is basically how can such desired behaviour be implemented with in a cleanly decoupled fashion. I had considered making this a purview of the user interface (so when user presses 'del' on an item, it triggers a scan of the Plan objects and performs a check before calling world.RemoveItem(item)) - but (a) I am also allowing the user to write and execute custom scripts so they can invoke world.RemoveItem(item) themselves, and (b) I'm not convinced this behaviour is a purely "user interface" issue.

Phew. Well I hope someone is still reading...

Original Post

Suppose I have the following classes:

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

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

So suppose a constraint exists whereby a Spaceship size must be smaller than or equal to the MaximumShipSize of its Home.

So how do we deal with this?

Traditionally I've done something coupled like this:

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

This is manageable for a simple example like this (so probably a bad example), but I'm finding as the constraints get larger and and more complex, and I want more related features (e.g. implement a method bool CanChangeMaximumShipSizeTo(double) or additional methods which will collect the ships which are too large) I end up writing more unnecessary bidirectional relationships (in this case SpaceBase-Spaceship is arguably appropriate) and complicated code which is largely irrelevant from the owners side of the equation.

So how is this sort of thing normally dealt with? Things I've considered:

  1. I considered using events, similar to the ComponentModel INotifyPropertyChanging/PropertyChanging pattern, except that the EventArgs would have some sort of Veto() or Error() capability (much like winforms allows you to consume a key or suppress a form exit). But I'm not sure whether this constitutes eventing abuse or not.

  2. Alternatively, managing events myself via an explicitly defined interface, e.g

asdf I need this line here or the formatting won't work

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

But I'm not sure if this is any better. I'm also unsure if rolling my own events in this manner would have certain performance implications or there are other reasons why this might be a good/bad idea.

  1. Third alternative is maybe some very wacky aop using PostSharp or an IoC/Dependency Injection container. I'm not quite ready to go down that path yet.

  2. God object which manages all the checks and so forth - just searching stackoverflow for god object gives me the impression this is bad and wrong

My main concern is this seems like a fairly obvious problem and what I thought would be a reasonably common one, but I haven't seen any discussions about it (e.g. System.ComponentModel providse no facilities to veto PropertyChanging events - does it?); this makes me afraid that I've (once again) failed to grasp some fundamental concepts in coupling or (worse) object-oriented design in general.

Comments?
}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

去了角落 2024-10-08 08:58:22

基于修改后的问题:

我认为 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 它正在使用一个项目。如果计划无法履行合同,那么它最终可能会处于无效状态。运气不好,计划,这就是你不遵守规则所得到的结果。

您不使用事件的原因是因为您需要返回值。另一种方法是让 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,
詹姆斯

Based on the revised question:

I'm thinking the WorldState class needs a delegate... And Plan would set a method that should be called to test if an item is in use. Sortof like:

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

}

Then, in the plan's constructor, set the delegate to be called

public class plan {

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

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

}

This is very rough, of course, I'll try to add more after lunch : ) But you get the general idea.

(Post-Lunch :)
The downside is that you have to rely on the Plan to set the delegate... but there's simply no way to avoid that. There's no way for an Item to tell how many references there are to it, or to control its own usage.

The best you can have is an understood contract... WorldState agrees not to remove an item if a Plan is using it, and Plan agrees to tell WorldState that it's using an item. If a Plan doesn't hold up its end of the contract, then it may end up in an invalid state. Tough luck, Plan, that's what you get for not following the rules.

The reason you don't use events is because you need a return value. An alternative would be to have WorldState expose a method to add 'listeners' of type IPlan, where IPlan defines CheckItemForUse(Item item). But you'd still have to rely that a Plan notifies WorldState to ask before removing an item.

One huge gap that I'm seeing: In your example, the Plan you create is not tied to the WorldState stuffAtMyHouse. You could create a Plan to take your dog to the beach, for example, and Plan would be perfectly happy (you'd have to create a dog Item, of course). Edit: do you mean to pass stuffAtMyHouse to the Plan constructor, instead of myHouse?

Because they're not tied, you currently don't care if you remove bicycle from stuffAtMyHouse... because what you're currently saying is "I don't care where the bicycle starts, and I don't care where it belongs, just take it to the beach". But what you mean (I believe) is "Take my bicycle from my house and go to the beach." The Plan needs to have a starting WorldState context.

TLDR: The best decoupling you can hope for is to let Plan choose what method WorldState should query before removing an item.

HTH,
James

Original Answer
It's not 100% clear to me what your goal is, and maybe it's just the forced example. Some possibilities:

I. Enforcing the maximum ship size on methods such as SpaceBase.Dock(myShip)

Pretty straight-forward... the SpaceBase tracks the size when called and throws a TooBigToDockException to the ship attempting to dock if it's too big. In this case, there's not really any coupling... you wouldn't notify the ship of the new max ship size, because managing the max ship size isn't the ship's responsibility.

If the max ship size decreases, you would force the ship to undock... again, the ship doesn't need to know the new max size (though an event or interface to tell it that it's now floating in space might be appropriate). The ship would have no say or veto on the decision... The base has decided it's too big and has booted it.

Your suspicions are correct... God objects are usually bad; clearly-delineated responsibilities make them vanish from the design in puffs of smoke.

II. A queryable property of the SpaceBase

If you want to let a ship ask you if it's too big to dock, you can expose this property. Again, you're not really coupled... you're just letting the ship make a decision to dock or not dock based on this property. But the base doesn't trust the ship to not-dock if it's too big... the base will still check on a call to Dock() and throw an exception.

The responsibility for checking dock-related constraints lies firmly with the base.

III. As true coupling, when the information is necessary to both parties

In order to dock, the base may need to control the ship. Here an interface is appropriate, ISpaceShip, which might have methods such as Rotate(), MoveLeft(), and MoveRight().

Here you avoid coupling by the virtue of the interface itself... Every ship will implement Rotate() differently... the base doesn't care, so long as it can call Rotate() and have the ship turn in place. A NoSuchManeuverException might be thrown by the ship if it doesn't know how to rotate, in which case the base makes a decision to try something different or reject the dock. The objects communicate, but they are not coupled beyond the Interface (contract), and the base still has the responsibility of docking.

IV. Validation on the MaxShipSize setter

You talk about throwing an exception to the caller if it tries to set the MaxShipSize to smaller than the docked ships. I have to ask, though, who is trying to set the MaxShipSize, and why? Either the MaxShipSize should have been set in the constructor and be immutable, or setting the size should follow natural rules, e.g. you can't set the ship size smaller than its current size, because in the real world you would expand a SpaceBase, but never shrink it.

By preventing illogical changes, you render the forced undocking and the communication that goes along with it moot.

The point I'm trying to make is that when you feel like your code is getting unnecessarily complicated, you're almost always right, and your first consideration should be the underlying design. And that in code, less is always more. When you talk about writing Veto() and Error(), and additional methods to 'collect ships that are too large', I become concerned that the code will turn into a Rube Goldberg machine. And I think that separated responsibilities and encapsulation will whittle away much of the unnecessary complication you're experiencing.

It's like a sink with plumbing issues... you can put in all sorts of bends and pipes, but the right solution is usually simple, straight-forward, and elegant.

HTH,
James

风铃鹿 2024-10-08 08:58:22

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

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

You know that a Spaceship must have a Size; put the Size in the base class, and implement validation checks in the accessor there.

I know this seems excessively focused on your specific implementation, but the point here is that your expectations aren't as decoupled as you expect; if you have a hard expectation in the base class of something in the derived class, your base class is making a fundamental expectation of the derived class providing an implementation of that; might as well migrate that expectation directly to the base class, where you can manage the constraints better.

辞旧 2024-10-08 08:58:22

您可以执行类似 C++ STL 特征类的操作 - 实现一个通用 SpaceBase,它有两个参数化 Type - 一个定义 SpaceShip 成员,另一个使用 SpaceBaseTraits 类来约束 SpaceBase 及其 SpaceShip 类,以封装基础的特性(例如限制)在它可以容纳的船上。

You could do something like C++ STL traits classes - implement a generic SpaceBase<Ship, Traits> which has two parameterizing Types - one that defines the SpaceShip member, and the other that constrains the SpaceBase and its SpaceShips using a SpaceBaseTraits class to encapsulate the characteristics of the base such as limitations on ships it can contain.

迷路的信 2024-10-08 08:58:22

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

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

The INotifyPropertyChanging interface was designed for data binding, which explains why it doesn't have abilities you're looking for. I might try something like this:

interface ISpacebaseInterceptor<T>
{ 
    bool RequestChange(T newValue); 
    void NotifyChange(T newValue); 
} 
冷弦 2024-10-08 08:58:22

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

首先,为什么允许更改 Starport.MaximumShipSize ?当我们“调整”星港的大小时,所有的飞船不都应该起飞吗?

这些问题是为了更好地理解需要做什么(并且没有“正确和错误”的答案,有“我的和你的”)。

从另一个角度看问题:

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

You want to apply constraints on actions, but applying them on the data.

Firstly, why changing Starport.MaximumShipSize is allowed? When we "resize" the Starport shouldn't all the Ships take off?

Those are the kind of questions to understand better what needs to be done (and there is no "right and wrong" answer, there is "mine and yours").

Look at the problem from other angle:

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
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文