2008-12-10 75 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"); 

它也適用於嵌套的不可變結構。