2008-12-10 73 views
13

我正在学习DDD,并且遇到了“值对象”应该是不可变的声明。我知道这意味着对象状态在创建后不应该改变。这对我来说是一种新的思维方式,但在许多情况下它是有意义的。如何设计一个具有复杂初始化的不可变对象

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

  • 我要确保他们采取了整个状态参数构造函数,
  • 我不加属性setter,
  • 并确保没有方法允许修改内容(只返回新实例)。

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

所以问题是:有没有让我不可变对象更好的..任何其他方式,可以在C#来完成,以克服在构造一个长参数列表任何魔法?我在听你的想法很感兴趣..

UPDATE:之前有人提到它,一个想法已经在这里讨论: Immutable object pattern in C# - what do you think?

是否有兴趣听取其他建议或意见,虽然。

+2

您还应该将所有字段设为只读。它使不变性更具说明性 – JaredPar 2008-12-10 05:57:09

回答

22

使用建设者:

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方法,他们可以有默认值。

+0

这种接缝是一个不错的选择。一些额外的代码,但我认为你得到一些清晰。我会试试这个。 – 2008-12-10 06:06:39

+3

这样做的缺点是你没有得到C#3对象初始化器的好处。这可以通过具有属性(以及针对C#3之前的客户端的方法)来解决:new Entity.Builder {Field1 = 123,Field2 = 456,Field3 = 789} .Build() – 2008-12-10 06:25:58

+0

感谢Jon - 当我被困在1.1和2.0世界的工作中,并且最近才开始在家玩c#3时,我想到了这种方法。 – 2008-12-10 06:41:34

3

关闭我的头顶,两个不同的答案浮现在脑海中......

...第一,大概最简单的,就是用一个对象工厂(或生成器)为确保您有一个帮手把事情做好。

对象的初始化是这样的:

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

...第二是创建对象作为一个传统的,可变的,对象与锁()或冻结()函数。你所有的mutators都应该检查对象是否被锁定,如果有,就抛出异常。

对象的初始化是这样的:

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

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

8

此刻,你必须使用一个有很多参数或构建器的构造函数。在C#4.0(VS2010)中,可以使用命名/可选参数来实现类似于C#3.0对象初始化程序的功能 - 请参阅here。对博客的例子是:

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

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

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

没有太多的分辨它们,真的。

Jon Skeet对此主题也发表了一些想法,here

1

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

每当我看到堆的参数,我觉得对象/方法/构造器应该更简单。

1

我一直对同样的问题感到惊讶,因为复杂的构造函数对我来说也是不好的设计。我也不是建造者概念的忠实粉丝,因为它似乎需要额外的代码来维护。我们需要的是冰棍不可变性,这意味着一个对象在允许使用属性设置器的地方以可变的方式开始。当所有的属性设置时,必须有一种方法将对象冻结成不可变的状态。不幸的是,这种策略在C#语言中本质上不受支持。因此我最终设计自己的模式来作为这个问题的描述创建一个不可改变的对象:

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

安德斯·海尔斯伯格在随后的采访中谈到了这种类型的不变性的支持36:30:

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

1

可以使用反射以初始化该对象和懒惰的所有字段做出“设置器”之类方法,以便(使用一元功能的风格),以链的设定方法/函数在一起。

例如:

你可以使用这个基类:

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

可以实现像这样:

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

,并可以这样使用:

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

塔卡一看在Remute库https://github.com/ababik/Remute

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

E.g.

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

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