How to design an immutable object with complex initialization

后端 未结 6 1152
北恋
北恋 2021-02-02 15:59

I\'m learning about DDD, and have come across the statement that \"value-objects\" should be immutable. I understand that this means that the objects state should not change aft

相关标签:
6条回答
  • 2021-02-02 16:34

    Use a builder:

    public class Entity
    {
       public class Builder
       {
         private int _field1;
         private int _field2;
         private int _field3;
    
         public Builder WithField1(int value) { _field1 = value; return this; }
         public Builder WithField2(int value) { _field2 = value; return this; }
         public Builder WithField3(int value) { _field3 = value; return this; }
    
         public Entity Build() { return new Entity(_field1, _field2, _field3); }
       }
    
       private int _field1;
       private int _field2;
       private int _field3;
    
       private Entity(int field1, int field2, int field3) 
       {
         // Set the fields.
       }
    
       public int Field1 { get { return _field1; } }
       public int Field2 { get { return _field2; } }
       public int Field3 { get { return _field3; } }
    
       public static Builder Build() { return new Builder(); }
    }
    

    Then create it like:

    Entity myEntity = Entity.Build()
                       .WithField1(123)
                       .WithField2(456)
                       .WithField3(789)
                      .Build()
    

    If some of the parameters are optional you won't need to call the WithXXX method and they can have default values.

    0 讨论(0)
  • 2021-02-02 16:36

    Off the top of my head, two different answers come to mind ...

    ... the first, and probably simplest, is to use an object factory (or builder) as a helper that ensures you get things right.

    Object initialization would look like this:

    var factory = new ObjectFactory();
    factory.Fimble = 32;
    factory.Flummix = "Nearly";
    var mine = factory.CreateInstance();
    

    ... the second is to create your object as a conventional, mutable, object with a Lock() or Freeze() function. All of your mutators should check to see if the object has been locked, and throw an exception if it has.

    Object initialization would look like this:

    var mine = new myImmutableObject();
    mine.Fimble = 32;
    mine.Flummix = "Nearly";
    mine.Lock(); // Now it's immutable.
    

    Which method to take depends a lot on your context - a factory has the advantage of being convenient if you have a series of similar objects to construct, but it does introduce another class to write and maintain. A lockable object means there is only one class, but other users might get unexpected runtime errors, and testing is harder.

    0 讨论(0)
  • 2021-02-02 16:41

    Although it is probably part of the domain of what you are doing, and thus my suggestion may be invalid, what about attempting to break down the 8 parameters into logical groups?

    Whenever I see heaps of parameters, i feel like the object/method/contructor ought to be simpler.

    0 讨论(0)
  • 2021-02-02 16:48

    You can use reflection in order to initialize all the fields of the object and laziness to make "setter" like methods (using monadic functional style) in order to chain the set methods/functions together.

    For example:

    You can use this base class:

    public class ImmutableObject<T>
    {
        private readonly Func<IEnumerable<KeyValuePair<string, object>>> initContainer;
    
        protected ImmutableObject() {}
    
        protected ImmutableObject(IEnumerable<KeyValuePair<string,object>> properties)
        {
            var fields = GetType().GetFields().Where(f=> f.IsPublic);
    
            var fieldsAndValues =
                from fieldInfo in fields
                join keyValuePair in properties on fieldInfo.Name.ToLower() equals keyValuePair.Key.ToLower()
                select new  {fieldInfo, keyValuePair.Value};
    
            fieldsAndValues.ToList().ForEach(fv=> fv.fieldInfo.SetValue(this,fv.Value));
    
        }
    
        protected ImmutableObject(Func<IEnumerable<KeyValuePair<string,object>>> init)
        {
            initContainer = init;
        }
    
        protected T setProperty(string propertyName, object propertyValue, bool lazy = true)
        {
    
            Func<IEnumerable<KeyValuePair<string, object>>> mergeFunc = delegate
                                                                            {
                                                                                var propertyDict = initContainer == null ? ObjectToDictonary () : initContainer();
                                                                                return propertyDict.Select(p => p.Key == propertyName? new KeyValuePair<string, object>(propertyName, propertyValue) : p).ToList();
                                                                            };
    
            var containerConstructor = typeof(T).GetConstructors()
                .First( ce => ce.GetParameters().Count() == 1 && ce.GetParameters()[0].ParameterType.Name == "Func`1");
    
            return (T) (lazy ?  containerConstructor.Invoke(new[] {mergeFunc}) :  DictonaryToObject<T>(mergeFunc()));
        }
    
        private IEnumerable<KeyValuePair<string,object>> ObjectToDictonary()
        {
            var fields = GetType().GetFields().Where(f=> f.IsPublic);
            return fields.Select(f=> new KeyValuePair<string,object>(f.Name, f.GetValue(this))).ToList();
        }
    
        private static object DictonaryToObject<T>(IEnumerable<KeyValuePair<string,object>> objectProperties)
        {
            var mainConstructor = typeof (T).GetConstructors()
                .First(c => c.GetParameters().Count()== 1 && c.GetParameters().Any(p => p.ParameterType.Name == "IEnumerable`1") );
            return mainConstructor.Invoke(new[]{objectProperties});
        }
    
        public T ToObject()
        {
            var properties = initContainer == null ? ObjectToDictonary() : initContainer();
            return (T) DictonaryToObject<T>(properties);
        }
    }
    

    Can be implemented like so:

    public class State:ImmutableObject<State>
    {
        public State(){}
        public State(IEnumerable<KeyValuePair<string,object>> properties):base(properties) {}
        public State(Func<IEnumerable<KeyValuePair<string, object>>> func):base(func) {}
    
        public readonly int SomeInt;
        public State someInt(int someInt)
        {
            return setProperty("SomeInt", someInt);
        }
    
        public readonly string SomeString;
        public State someString(string someString)
        {
            return setProperty("SomeString", someString);
        }
    }
    

    and can be used like this:

    //creating new empty object
    var state = new State();
    
    // Set fields, will return an empty object with the "chained methods".
    var s2 = state.someInt(3).someString("a string");
    // Resolves all the "chained methods" and initialize the object setting all the fields by reflection.
    var s3 = s2.ToObject();
    
    0 讨论(0)
  • 2021-02-02 16:56

    I have been boggled with the same question as complex constructors is also bad design to me. I am also not a big fan of the builder concept as it seems like too much extra code to maintain. What we need is popsicle immutability, which means that an object starts out as mutable where you are allowed to use the property setters. When all properties are set there must be a way of freezing the object into an immutable state. This strategy is unfortunately not supported natively in the C# language. I therefore ended up designing my own pattern for creating immutable objects as described in this question:

    Immutable object pattern in C# - what do you think?

    Anders Hejlsberg is talking about support for this type of immutability from 36:30 in the following interview:

    Expert to Expert: Anders Hejlsberg - The Future of C#

    0 讨论(0)
  • 2021-02-02 16:58

    At the moment, you'd have to use a constructor with lots of args, or a builder. In C# 4.0 (VS2010), you can use named/optional arguments to achieve something similar to C# 3.0 object-initializers - see here. The example on the blog is:

      Person p = new Person ( forename: "Fred", surname: "Flintstone" );
    

    But you can easily see how something similar can apply for any constructor (or other complex method). Compare to the C# 3.0 object-initializer syntax (with a mutable type):

     Person p = new Person { Forename = "Fred", Surname = "Flintstone" };
    

    Not much to tell them apart, really.

    Jon Skeet has posted some thoughts on this subject too, here.

    0 讨论(0)
提交回复
热议问题