Note that there are some explanatory texts on larger screens.

plurals
  1. POImmutable Class Construction Design
    text
    copied!<p>So we all realize the benefits of immutable types, particularly in multithreaded scenarios. (Or at least we should all realize that; see e.g. System.String.)</p> <p>However, what I haven't seen is much discussion for <em>creating</em> immutable instances, specifically design guidelines.</p> <p>For example, suppose we want to have the following immutable class:</p> <pre><code>class ParagraphStyle { public TextAlignment Alignment {get;} public float FirstLineHeadIndent {get;} // ... } </code></pre> <p>The most common approach I've seen is to have mutable/immutable "pairs" of types, e.g. the mutable <a href="http://msdn.microsoft.com/en-us/library/6sh2ey19.aspx" rel="noreferrer">List&lt;T&gt;</a> and immutable <a href="http://msdn.microsoft.com/en-us/library/ms132474.aspx" rel="noreferrer">ReadOnlyCollection&lt;T&gt;</a> types or the mutable <a href="http://msdn.microsoft.com/en-us/library/system.text.stringbuilder.aspx" rel="noreferrer">StringBuilder</a> and immutable <a href="http://msdn.microsoft.com/en-us/library/system.string.aspx" rel="noreferrer">String</a> types.</p> <p>To mimic this existing pattern would require the introduction of some type of "mutable" <code>ParagraphStyle</code> type which "duplicates" the members (to provide setters), and then provide a <code>ParagraphStyle</code> constructor which accepts the mutable type as an argument</p> <pre><code>// Option 1: class ParagraphStyleCreator { public TextAlignment {get; set;} public float FirstLineIndent {get; set;} // ... } class ParagraphStyle { // ... as before... public ParagraphStyle (ParagraphStyleCreator value) {...} } // Usage: var paragraphStyle = new ParagraphStyle (new ParagraphStyleCreator { TextAlignment = ..., FirstLineIndent = ..., }); </code></pre> <p>So, this works, supports code completion within the IDE, and makes things reasonably obvious about how to construct things...but it does seem fairly duplicative.</p> <p>Is there a better way?</p> <p>For example, C# anonymous types are immutable, AND allow using "normal" property setters for initialization:</p> <pre><code>var anonymousTypeInstance = new { Foo = "string", Bar = 42; }; anonymousTypeInstance.Foo = "another-value"; // compiler error </code></pre> <p>Unfortunately, the closest way to duplicate these semantics in C# is to use constructor parameters:</p> <pre><code>// Option 2: class ParagraphStyle { public ParagraphStyle (TextAlignment alignment, float firstLineHeadIndent, /* ... */ ) {...} } </code></pre> <p>But this doesn't "scale" well; if your type has e.g. 15 properties, a constructor with 15 parameters is anything but friendly, and providing "useful" overloads for all 15 properties is a recipe for a nightmare. I'm rejecting this outright.</p> <p>If we try to mimic anonymous types, it seems that we could use "set-once" properties in the "immutable" type, and thus drop the "mutable" variant:</p> <pre><code>// Option 3: class ParagraphStyle { bool alignmentSet; TextAlignment alignment; public TextAlignment Alignment { get {return alignment;} set { if (alignmentSet) throw new InvalidOperationException (); alignment = value; alignmentSet = true; } } // ... } </code></pre> <p>The problem with this is that it it's not obvious that properties can be set only once (the compiler certainly won't complain), and initialization isn't thread-safe. It thus becomes tempting to add a <code>Commit()</code> method so that the object can know that the developer is done setting the properties (thus causing all properties that haven't previously been set to throw if their setter is invoked), but this seems to be a way to make things worse, not better.</p> <p>Is there a better design than the mutable/immutable class split? Or am I doomed to deal with member duplication?</p>
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload