开发者

How to design around lack of const in C#/Java?

开发者 https://www.devze.com 2023-01-19 19:43 出处:网络
While trying to model my domain, I came across the following problem. Let\'s image we have a Thing: class Thing

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开发者_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.


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


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


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


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.


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?


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.


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.

0

精彩评论

暂无评论...
验证码 换一张
取 消