14

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

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

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

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

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

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

不过,有兴趣听到其他建议或意见。

4

6 回答 6

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

于 2008-12-10T05:56:33.230 回答
9

目前,您必须使用具有大量参数的构造函数或构建器。在 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 也在这里发表了一些关于这个主题的想法。

于 2008-12-10T08:30:37.270 回答
3

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

... 第一个,也可能是最简单的,是使用对象工厂(或构建器)作为帮助器,以确保您把事情做好。

对象初始化如下所示:

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

...第二个是将您的对象创建为具有 Lock() 或 Freeze() 函数的常规、可变对象。你所有的修改器都应该检查对象是否被锁定,如果有则抛出异常。

对象初始化如下所示:

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

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

于 2008-12-10T06:12:11.420 回答
2

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

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

于 2008-12-10T08:40:44.253 回答
1

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

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

Anders Hejlsberg 在以下采访中从 36:30 开始谈论对这种类型不变性的支持:

专家对专家:Anders Hejlsberg - C# 的未来

于 2009-05-06T06:23:40.867 回答
1

您可以使用反射来初始化对象的所有字段,并使用惰性来制作类似“setter”的方法(使用单子函数样式),以便将设置的方法/函数链接在一起。

例如:

您可以使用这个基类:

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();
于 2014-06-26T13:17:18.313 回答