在尝试为我的域建模时,我遇到了以下问题。让我们的形象有一件事:
class Thing
{
public int X { get; set; }
}
东西有一个属性X.然后,有Packs,聚合东西。但该域名要求包装可以容纳的东西有一些限制。例如,Xes的累积值不能高于某个特定限制:
class Pack
{
private readonly List<Thing> myThings = new List<Thing>();
private const int MaxValue = 5;
public void Add(Thing thing)
{
if (myThings.Sum(t => t.X) + thing.X > MaxValue)
throw new Exception("this thing doesn't fit here");
myThings.Add(thing);
}
public int Count
{
get { return myThings.Count; }
}
public Thing this[int index]
{
get { return myThings[index]; }
}
}
所以我在为条件添加Thing之前检查,但它仍然很容易陷入麻烦:
var pack = new Pack();
pack.Add(new Thing { X = 2 });
pack.Add(new Thing { X = 1 });
var thingOne = new Thing { X = 1 };
var thingTwo = new Thing { X = 3 };
//pack.Add(thingTwo); // exception
pack.Add(thingOne); // OK
thingOne.X = 5; // trouble
pack[0].X = 10; // more trouble
在C ++中,解决方案是在插入时复制并在索引器中返回const引用。如何在C#(可能还有Java)中围绕这个问题进行设计?我似乎无法想出一个好的解决方案:
任何想法或首选解决方案?
修改
回到这个问题......我接受了Itay的回复。他是对的。 原始问题是,从一个上下文中,您希望Thing对象是不可变的,并且从不同的上下文中,您会希望它是可变的。这需要一个单独的界面......也许吧。我说“也许”,因为大部分时间,Pack都是物品的聚合物(在DDD意义上),因此是对象的所有者 - 这意味着它不应该让你能够改变拥有的对象(或者返回一个副本或返回一个不可变的接口)。
很高兴在C ++中,const修饰符可以很容易地处理这个特殊的东西。如果你想把事情保持在一致的状态,那似乎就少了很多编码。
答案 0 :(得分:6)
让Thing
永久不变。
class Thing
{
public Thing (int x)
{
X = x;
}
public int X { get; private set; }
}
此外,我认为最好在包中保存一个sum字段,而不是if (myThings.Sum(t => t.X) + thing.X > MaxValue)
,这样你就不必每次重新计算总和。
修改强>
对不起 - 我错过了你说你需要它可变的事实
但是......你的c ++解决方案将如何运作?我不太了解c ++,但c ++常量引用是否会阻止对Pack上实例的更改?
<强> EDIT2 强>
使用界面
public interface IThing
{
int X { get; }
}
public class Thing : IThing
{
int X { get; set; }
}
class Pack
{
private readonly List<IThing> myThings = new List<IThing>();
private const int MaxValue = 5;
public void Add(IThing thing)
{
if (myThings.Sum(t => t.X) + thing.X > MaxValue)
throw new Exception("this thing doesn't fit here");
myThings.Add(new InnerThing(thing));
}
public int Count
{
get { return myThings.Count; }
}
public IThing this[int index]
{
get { return myThings[index]; }
}
private class InnerThing : IThing
{
public InnerThing(IThing thing)
{
X = thing.X;
}
int X { get; private set; }
}
}
答案 1 :(得分:3)
你可以让Thing实现iReadOnlyThing接口,该接口只允许ReadOnly访问Thing的每个属性。
这意味着两次实现大部分属性,这也意味着信任其他编码人员尊重iReadOnlyThing接口的使用,而不是偷偷地将任何iReadOnlyThings转换回物品。
您可能会以与
相同的方式实现只读容器类System.Collections.ObjectModel.ObservableCollection<T>
和
System.Collections.ObjectModel.ReadOnlyObservableCollection<T>
做。 ReadOnlyObservableCollection的构造函数将ObservableCollection作为其参数,并允许对集合进行读访问,而不允许写访问。它的优点是不允许将ReadOnlyObservableCollection强制转换回ObservableCollection。
我觉得我应该指出,WPF开发中使用的Model-View-ViewModel模式实际上是基于你建议的原则,它有一个NotifyPropertyChanged事件,它包含一个标识已更改属性的字符串。 ..
MVVM模式的编码开销很大,但有些人似乎喜欢它......
答案 2 :(得分:2)
在您的陈述后,您确实在X
背后有一些业务逻辑,在X
设置时也必须应用这些逻辑。如果它应该触发一些业务逻辑,你应该仔细考虑如何设计X
。它不仅仅是一种财产。
您可以考虑以下解决方案:
X
吗?即使该类不是不可变的,您仍然可以将X
设为只读。SetX(...)
方法,该方法封装业务逻辑,以检查Thing
是Pack
的一部分,并调用Pack
的验证逻辑。set { }
代替setter方法来代替该逻辑。Pack.Add
中,创建Thing
的副本,该副本实际上是添加验证逻辑的子类。无论你做什么,你都不会解决以下非常基本的设计问题:要么允许更改X,那么你必须在允许更改X的每个部分中引入验证/业务逻辑。或者你制作X只读并调整可能需要相应更改X的应用程序部分(例如删除旧对象,添加新对象......)。
答案 3 :(得分:1)
对于简单对象,您可以使它们不可变。但是不要过度。如果使复杂对象不可变,则可能需要创建一些构建器类以使其可以接受。这通常是不值得的。
并且您不应该创建具有不可变身份的对象。通常,如果您希望对象的行为类似于值类型,则可以使对象不可变。
在我的一个项目中,我通过仅为基类提供getter并将setter隐藏在派生类中来破解不变性。然后,为了使对象不可变,我将它转换为基类。当然,这种类型的不变性不是由运行时强制执行的,如果您需要将类层次结构用于其他目的,则不起作用。
答案 4 :(得分:1)
如果在事情中改变事件怎么样; Pack订阅此事件,当你在Thing中更改X时,Pack在事件处理程序中执行必要的检查,如果更改不合适;抛出异常?
答案 5 :(得分:0)
不幸的是,C#中的任何内容都没有像C / C ++中的const
那么强大,以确保(在编译时)不会更改值。
他们唯一能带来的就是不变性(就像你自己和Itay所提到的那样),但它没有像const
这样的灵活性。 :(
如果你在某些情况下需要它可变,你只能在运行时做这样的事情
class Thing
{
public bool IsReadOnly {get; set;}
private int _X;
public int X
{
get
{
return _X;
}
set
{
if(IsReadOnly)
{
throw new ArgumentException("X");
}
_X = value;
}
}
}
但它会使用try/catch
污染您的代码并更改IsReadOnly
的值。所以不是很优雅,但可能需要考虑。
答案 6 :(得分:0)
我会考虑重新设计你使用Pack的方式。
如果您从集合中删除了您想要修改的东西,而不仅仅是允许直接访问,则会在它被放回时进行检查。
当然,不好的一面是你必须记得把它放回包中!
或者在Pack上提供修改Things的方法,这样你就可以检查所设置的值是否违反了域规则。