如何设计具有复杂初始化的不可变对象

我正在学习DDD,并且遇到了“值对象”应该是不可变的声明。 我理解这意味着对象状态在创建后不应该更改。 这对我来说是一种新的思维方式,但在许多情况下它是有道理的。

好的,所以我开始创建不可变的值对象。

  • 我确保他们将整个状态作为构造函数的参数,
  • 我不添加属性设置器,
  • 并确保不允许任何方法修改内容(仅返回新实例)。

但现在我想创建这个包含8个不同数值的值对象。 如果我创建一个具有8个数字参数的构造函数,我觉得它不会很容易使用,或者更确切地说 – 传递数字时很容易出错。 这不是一个好的设计。

所以问题是:是否还有其他方法可以使我的不可变对象变得更好..,在C#中可以用来克服构造函数中的长参数列表的任何魔法? 我很想听听你的想法..

更新:在任何人提到它之前,这里讨论了一个想法: C#中的不可变对象模式 – 你怎么看?

有兴趣听听其他建议或评论。

使用构建器:

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

然后创建它像:

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

如果某些参数是可选的,则无需调用WithXXX方法,它们可以具有默认值。

目前,您必须使用具有大量args或构建器的构造函数。 在C#4.0(VS2010)中,您可以使用命名/可选参数来实现类似于C#3.0对象初始化器的操作 – 请参阅此处 。 博客上的例子是:

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

但是你可以很容易地看到类似的东西如何适用于任何构造函数(或其他复杂的方法)。 与C#3.0对象初始化程序语法(具有可变类型)进行比较:

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

真的,没什么好说的。

Jon Skeet也在这里发表了关于这个主题的一些想法。

在我的头脑中,脑海中浮现出两个不同的答案……

……第一个,也可能是最简单的,是使用对象工厂(或构建器)作为帮助器,确保您正确行事。

对象初始化将如下所示:

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

…第二个是使用Lock()或Freeze()函数将对象创建为传统的可变对象。 你的所有mutators应检查对象是否已被锁定,如果有,则抛出exception。

对象初始化将如下所示:

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

采用哪种方法很大程度上取决于您的上下文 – 如果您要构造一系列类似的对象,工厂具有方便的优势,但它确实引入了另一个类来编写和维护。 可锁定对象意味着只有一个类,但其他用户可能会遇到意外的运行时错误,并且测试更难。

虽然它可能是你正在做的领域的一部分,因此我的建议可能无效,那么试图将8个参数分解为逻辑组呢?

每当我看到大量参数时,我觉得对象/方法/构造函数应该更简单。

我一直对同样的问题感到困惑,因为复杂的构造函数对我来说也是糟糕的设计。 我也不是构建器概念的忠实粉丝,因为它似乎需要太多额外的代码来维护。 我们需要的是冰棒不变性,这意味着一个对象开始时是可变的,你可以使用属性设置器。 设置所有属性后,必须有一种方法将对象冻结为不可变状态。 遗憾的是,这种策略在C#语言中不受支持。 因此,我最终设计了自己的模式来创建不可变对象,如下所述:

C#中不可变的对象模式 – 你怎么看?

安德斯·海尔斯伯格(Anders Hejlsberg)在接下来的采访中谈到了从36:30开始对这种不变性的支持:

专家专家:Anders Hejlsberg – C#的未来

您可以使用reflection来初始化对象的所有字段和懒惰以使“setter”像方法(使用monadic函数样式)以将设置的方法/函数链接在一起。

例如:

您可以使用此基类:

 public class ImmutableObject { private readonly Func>> initContainer; protected ImmutableObject() {} protected ImmutableObject(IEnumerable> 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>> init) { initContainer = init; } protected T setProperty(string propertyName, object propertyValue, bool lazy = true) { Func>> mergeFunc = delegate { var propertyDict = initContainer == null ? ObjectToDictonary () : initContainer(); return propertyDict.Select(p => p.Key == propertyName? new KeyValuePair(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(mergeFunc())); } private IEnumerable> ObjectToDictonary() { var fields = GetType().GetFields().Where(f=> f.IsPublic); return fields.Select(f=> new KeyValuePair(f.Name, f.GetValue(this))).ToList(); } private static object DictonaryToObject(IEnumerable> 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(properties); } } 

可以像这样实现:

 public class State:ImmutableObject { public State(){} public State(IEnumerable> properties):base(properties) {} public State(Func>> 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); } } 

并可以像这样使用:

 //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(); 

塔卡看看Remute图书馆https://github.com/ababik/Remute

您可以生成将lambda表达式应用于现有表达式的新不可变对象。 没有代码生成或类似Builder模式的样板代码。

例如

 var entity = new Entity(field1, field2, field3); entity = remute.With(entity, x => x.Field1, "foo"); 

它也适用于嵌套的不可变结构。