如何设计C#/ Java中缺少const?

时间:2010-10-14 09:47:47

标签: c# const

在尝试为我的域建模时,我遇到了以下问题。让我们的形象有一件事:

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)中围绕这个问题进行设计?我似乎无法想出一个好的解决方案:

  1. 让事情变得不可变 - 但如果它需要变成什么呢?
  2. 用事件/观察者观察包装中的东西 - 但这意味着Pack强加了Thing的设计;如果东西有更多属性怎么办?然后我最终只会遇到一个事件,因为Pack需要观察变化 - 这对我来说似乎很尴尬。
  3. 任何想法或首选解决方案?

    修改

    回到这个问题......我接受了Itay的回复。他是对的。 原始问题是,从一个上下文中,您希望Thing对象是不可变的,并且从不同的上下文中,您会希望它是可变的。这需要一个单独的界面......也许吧。我说“也许”,因为大部分时间,Pack都是物品的聚合物(在DDD意义上),因此是对象的所有者 - 这意味着它不应该让你能够改变拥有的对象(或者返回一个副本或返回一个不可变的接口)。

    很高兴在C ++中,const修饰符可以很容易地处理这个特殊的东西。如果你想把事情保持在一致的状态,那似乎就少了很多编码。

7 个答案:

答案 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(...)方法,该方法封装业务逻辑,以检查ThingPack的一部分,并调用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的方法,这样你就可以检查所设置的值是否违反了域规则。