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:
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.
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.
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.
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? }