如何设计解决 C#/Java 中缺少 const 的问题?

发布于 2024-09-27 14:14:54 字数 1666 浏览 6 评论 0原文

在尝试对我的域进行建模时,我遇到了以下问题。让我们想象一下,我们有一个东西:

class Thing
{
    public int X { get; set; }
}

东西有一个属性 X。然后,有一些包,它聚合了东西。但领域要求对包可以容纳的东西有一些限制。举例来说,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]; }
    }
}

因此,我在将事物添加到包之前检查条件,但仍然很容易陷入麻烦:

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. 使 Thing 不可变 - 但如果它需要可变怎么办?
  2. 使用 event/observer 观察 Pack 中的事物 - 但这意味着 Pack 强加了 Thing 的设计;如果事物有更多属性怎么办?然后,由于需要 Pack 来观察变化,我最终将只进行一项活动 - 这对我来说似乎很尴尬。

有什么想法或首选解决方案吗?

编辑:

回到这个问题...我已经接受了 Itay 的回复。他是对的。 最初的问题是,在一个上下文中,您希望 Thing 对象是不可变的,而在不同的上下文中,您会希望它是可变的。这需要一个单独的界面......也许吧。我说“也许”,因为大多数时候,Pack 是事物的聚合(在 DDD 意义上),因此是对象的所有者 - 这意味着它不应该让您能够更改所拥有的对象(或者返回一个副本或返回一个不可变的接口)。

很高兴在 C++ 中这个特殊的事情可以通过 const 修饰符如此轻松地处理。如果你想让事情保持一致的状态,那么编码似乎要少得多。

While trying to model my domain, I came across the following problem. Let's image we have a Thing:

class Thing
{
    public int X { get; set; }
}

Things have a property X. Then, there are Packs, which aggregate Things. But the domain requires that there is some restriction on the Things the Packs can hold. Let it be for example that the cumulative value of Xes can't be higher then some specific limit:

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

So I check before adding a Thing to a Pack for the condition, but it's still so easy to get into troubles:

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

In C++ the solution would be to make a copy upon insertion and return const reference in the indexer. How to design around this problem in C# (and probably Java)? I just can't seem to think of a good solution:

  1. make Thing immutable - but what if it needs to be mutable?
  2. watch the Things in Pack with event/observer - but that means that Pack imposes the design of Thing; what if Things have more properties? Then I'll end up with just one event due to the need for Pack to watch for changes - that seems awkward to me.

Any ideas or preferred solutions?

EDIT:

Coming back to this question... I've accepted the reply by Itay. He's right.
The original issue was that from one context you would want a Thing object to be immutable, and from a different context, you would want it to be mutable. And that calls for a seperate interface... maybe. I said "maybe", because most of the time, Pack would be an Aggregate of Things (in the DDD sense) and therefore be the owner of the objects - which means it should not give you the ability to change the owned object (either return a copy or return an immutable interface).

It's nice that in C++ this particular thing can be handled so easily by the const modifier. Seems like a lot less coding if you want to keep things in a consistent state.

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

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

发布评论

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

评论(7

时光磨忆 2024-10-04 14:14:54

使 Thing 不可变。

class Thing
{
    public Thing (int x)
    {
       X = x;
    }
    public int X { get; private set; }
}

另外,我认为最好在 pack 中保存一个 sum 字段,而不是 if (myThings.Sum(t => tX) + thing.X > MaxValue) ,这样你就不用每次重新计算总和。

编辑
抱歉 - 我错过了您说过您需要它可变的事实。
但是...你的 C++ 解决方案如何工作?我不太了解 C++,但 C++ 常量引用不会阻止包上实例的更改吗?

编辑2
使用接口

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

Make Thing immutable.

class Thing
{
    public Thing (int x)
    {
       X = x;
    }
    public int X { get; private set; }
}

In addition, instead of if (myThings.Sum(t => t.X) + thing.X > MaxValue) I think it is better to hold a sum field in pack so you don't have to recalculate the sum each time.

EDIT
Sorry - I missed the fact that you stated that you need it mutable.
But... How would your c++ solution work? I don't know much c++ but doesn't c++ constant reference will prevent the change on instances that on the Pack?

EDIT2
Use an interface

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; }
    }
}
晨曦慕雪 2024-10-04 14:14:54

您可以让 Thing 实现 iReadOnlyThing 接口,该接口仅对 Thing 的每个属性具有只读访问权限。

这意味着将大部分属性实现两次,也意味着相信其他编码人员尊重 iReadOnlyThing 接口的使用,而不是偷偷地将任何 iReadOnlyThings 转换回 Things。

相同的方式实现只读容器类

System.Collections.ObjectModel.ObservableCollection<T>

您可能会以与和

System.Collections.ObjectModel.ReadOnlyObservableCollection<T>

。 ReadOnlyObservableCollection 的构造函数采用 ObservableCollection 作为其参数,并允许对集合进行读访问,但不允许写访问。它的优点是不允许将 ReadOnlyObservableCollection 强制转换回 ObservableCollection。

我觉得我应该指出,WPF 开发中使用的模型-视图-视图模型模式实际上确实按照您建议的原则工作,即拥有一个 NotifyPropertyChanged 事件,其中包含一个标识已更改属性的字符串

... MVVM 模式的编码开销很大,但有些人似乎喜欢它......

You could have Thing implement the interface iReadOnlyThing, which only has ReadOnly acess to each of Thing's properties.

It means implementing most of your properties twice, and it also means trusting other coders to respect the use of the iReadOnlyThing interface and not sneakily cast any iReadOnlyThings back to Things.

You might go so far as implementing a read-only container class in the same way that

System.Collections.ObjectModel.ObservableCollection<T>

and

System.Collections.ObjectModel.ReadOnlyObservableCollection<T>

do. ReadOnlyObservableCollection's constructor takes an ObservableCollection as its argument and allows read access to the collection without allowing write access. It has the advantage of not allowing the ReadOnlyObservableCollection to be cast back to an ObservableCollection.

I feel I should point out that the Model-View-ViewModel pattern used in WPF development does, in fact work on the principle that you suggest, of having a single NotifyPropertyChanged event, which contains a string identifying the property that changed...

The coding overhead of MVVM pattern is significant, but some people seem to like it...

稀香 2024-10-04 14:14:54

按照您的陈述,您确实在 X 背后有一些业务逻辑,在设置 X 时也必须应用这些业务逻辑。如果要触发某些业务逻辑,您应该仔细考虑如何设计 X。它不仅仅是一种财产。

您可以考虑以下解决方案:

  • 您真的需要允许更改 X 吗?即使该类不是不可变的,您仍然可以将 X 设置为只读。
  • 提供一个 SetX(...) 方法,该方法封装了业务逻辑,以检查 Thing 是否是 Pack 的一部分并调用该 >Pack 的验证逻辑。
  • 当然,您也可以使用属性 set { } 来代替 setter 方法来实现该逻辑。
  • Pack.Add 中,创建 Thing 的副本,它实际上是添加验证逻辑的子类。
  • ...

无论你做什么,你都无法回避以下非常基本的设计问题:要么你允许改变X,那么你必须在允许改变X的每个部分中引入验证/业务逻辑。或者你让X只读并调整应用程序中可能想要相应更改 X 的部分(例如删除旧对象、添加新对象...)。

Following your statements you really have some business logic behind the X which must also be applied when X is set. You should think carefully about how you design X if it is supposed to trigger some business logic. Its more than merely a property.

You might consider the following solutions:

  • Do you really need to allow to change X? Even if the class is not immutable you can still make X read-only.
  • Provide a SetX(...) method which encapsulates the business logic to check whether the Thing is part of a Pack and call that Pack's validation logic.
  • Of course instead of a setter method you can use the property set { } as well for that logic.
  • In Pack.Add, create a copy of the Thing, which is actually a subclass that adds the validation logic.
  • ...

Whatever you do, you won't get around the following very basic design question: Either you allow for changing X, then you have to introduce the validation / business logic in every part that allows for changing X. Or you make X read-only and adapt the parts of the application that might want to change X accordingly (e.g. remove old object, add new one...).

铜锣湾横着走 2024-10-04 14:14:54

对于简单的对象,您可以使它们不可变。但不要过度。如果您使复杂对象不可变,您可能需要创建一些构建器类才能使用它。这通常是不值得的。

并且您不应该创建具有不可变身份的对象。通常,如果您希望对象表现得像值类型,则可以将其设置为不可变。

在我的一个项目中,我通过只为基类提供 getter 并将 setter 隐藏在派生类中来实现不变性。然后为了使对象不可变,我将其强制转换为基类。当然,这种类型的不变性不是由运行时强制执行的,并且如果您需要类层次结构用于其他目的,则这种不变性不起作用。

For simple objects you can make them immutable. But don't overdo it. If you make complex objects immutable you probably need to create some builder class to make working with it acceptable. And that's usually not worth it.

And you shouldn't make objects which have an identity immutable. Typically you make an object immutable if you want it to behave like a value type.

In one of my projects I hacked in immutability by giving the base class only getters and hiding the setters in the derived class. Then to make the object immutable I'd cast it to the baseclass. Of course this type of immutability isn't enforced by the runtime and doesn't work if you need your class hierarchy for other purposes.

顾冷 2024-10-04 14:14:54

在 Thing 中添加 Change 事件怎么样? Pack 订阅此事件,当您更改 Thing 中的 X 时,Pack 会在事件处理程序中进行必要的检查,如果更改不适合;抛出异常?

How about having Changing event in Thing; Pack subscribes to this event and when you change the X in Thing, Pack do the necessary checking in the event handler and if the change doesnt fit; throws exception?

找回味觉 2024-10-04 14:14:54

不幸的是,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 的值。所以不是很优雅,但也许值得考虑。

Unfortunately there is nothing in C# that has so much power like const in C/C++ to make sure (at compile time) that a value won't be changed.

The only thing that comes them nearly is immutability (like mentioned by yourself and Itay), but it doesn't have the flexibility like const. :(

If you need it mutable in some scenarios you can only do something at runtime like this

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

but it pollutes your code with try/catch and changing the value of IsReadOnly. So not very elegant, but maybe something to consider.

ゃ人海孤独症 2024-10-04 14:14:54

我会考虑重新设计您使用该包的方式。

如果您从集合中删除要修改的事物,并且不仅仅允许直接访问,则在将其放回时将会对其进行检查。

当然,缺点是您必须记住将其放回包装中!

或者在 Pack 上提供方法来修改事物,以便您可以检查正在设置的值是否违反域规则。

I would consider redesigning the way you use the Pack.

If you remove the Thing you wish to modify from the collection, and not just allow direct access, it would be checked when it is put back.

Of course, the down side is you having to remember to put it back in the Pack!

Alternatively provide methods on the Pack to modify Things, so you can check the value being set does not violate domain rules.

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