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

匿名 (未验证) 提交于 2019-12-03 02:13:02

问题:

I have over the course of a few projects developed a pattern for creating immutable (readonly) objects and immutable object graphs. Immutable objects carry the benefit of being 100% thread safe and can therefore be reused across threads. In my work I very often use this pattern in Web applications for configuration settings and other objects that I load and cache in memory. Cached objects should always be immutable as you want to guarantee they are not unexpectedly changed.

Now, you can of course easily design immutable objects as in the following example:

public class SampleElement {   private Guid id;   private string name;    public SampleElement(Guid id, string name)   {     this.id = id;     this.name = name;   }    public Guid Id   {     get { return id; }   }    public string Name   {     get { return name; }   } } 

This is fine for simple classes - but for more complex classes I do not fancy the concept of passing all values through a constructor. Having setters on the properties is more desirable and your code constructing a new object gets easier to read.

So how do you create immutable objects with setters?

Well, in my pattern objects start out as being fully mutable until you freeze them with a single method call. Once an object is frozen it will stay immutable forever - it cannot be turned into a mutable object again. If you need a mutable version of the object, you simply clone it.

Ok, now on to some code. I have in the following code snippets tried to boil the pattern down to its simplest form. The IElement is the base interface that all immutable objects must ultimately implement.

public interface IElement : ICloneable {   bool IsReadOnly { get; }   void MakeReadOnly(); } 

The Element class is the default implementation of the IElement interface:

public abstract class Element : IElement {   private bool immutable;    public bool IsReadOnly   {     get { return immutable; }   }    public virtual void MakeReadOnly()   {     immutable = true;   }    protected virtual void FailIfImmutable()   {     if (immutable) throw new ImmutableElementException(this);   }    ... } 

Let's refactor the SampleElement class above to implement the immutable object pattern:

public class SampleElement : Element {   private Guid id;   private string name;    public SampleElement() {}    public Guid Id   {     get      {        return id;      }     set     {       FailIfImmutable();       id = value;     }   }    public string Name   {     get      {        return name;      }     set     {       FailIfImmutable();       name = value;     }   } } 

You can now change the Id property and the Name property as long as the object has not been marked as immutable by calling the MakeReadOnly() method. Once it is immutable, calling a setter will yield an ImmutableElementException.

Final note: The full pattern is more complex than the code snippets shown here. It also contains support for collections of immutable objects and complete object graphs of immutable object graphs. The full pattern enables you to turn an entire object graph immutable by calling the MakeReadOnly() method on the outermost object. Once you start creating larger object models using this pattern the risk of leaky objects increases. A leaky object is an object that fails to call the FailIfImmutable() method before making a change to the object. To test for leaks I have also developed a generic leak detector class for use in unit tests. It uses reflection to test if all properties and methods throw the ImmutableElementException in the immutable state. In other words TDD is used here.

I have grown to like this pattern a lot and find great benefits in it. So what I would like to know is if any of you are using similar patterns? If yes, do you know of any good resources that document it? I am essentially looking for potential improvements and for any standards that might already exist on this topic.

回答1:

For info, the second approach is called "popsicle immutability".

Eric Lippert has a series of blog entries on immutability starting here. I'm still getting to grips with the CTP (C# 4.0), but it looks interesting what optional / named parameters (to the .ctor) might do here (when mapped to readonly fields)... [update: I've blogged on this here]

For info, I probably wouldn't make those methods virtual - we probably don't want subclasses being able to make it non-freezable. If you want them to be able to add extra code, I'd suggest something like:

[public|protected] void Freeze() {     if(!frozen)     {         frozen = true;         OnFrozen();     } } protected virtual void OnFrozen() {} // subclass can add code here. 

Also - AOP (such as PostSharp) might be a viable option for adding all those ThrowIfFrozen() checks.

(apologies if I have changed terminology / method names - SO doesn't keep the original post visible when composing replies)



回答2:

Another option would be to create some kind of Builder class.

For an example, in Java (and C# and many other languages) String is immutable. If you want to do multiple operations to create a String you use a StringBuilder. This is mutable, and then once you're done you have it return to you the final String object. From then on it's immutable.

You could do something similar for your other classes. You have your immutable Element, and then an ElementBuilder. All the builder would do is store the options you set, then when you finalize it it constructs and returns the immutable Element.

It's a little more code, but I think it's cleaner than having setters on a class that's supposed to be immutable.



回答3:

After my initial discomfort about the fact that I had to create a new System.Drawing.Point on each modification, I've wholly embraced the concept some years ago. In fact, I now create every field as readonly

I don't care very much about cross-threading issues, though (I rarely use code where this is relevant). I just find it much, much better because of the semantic expressiveness. Immutability is the very epitome of an interface which is hard to use incorrectly.



回答4:

You are still dealing with state, and thus can still be bitten if your objects are parallelized before being made immutable.

A more functional way might be to return a new instance of the object with each setter. Or create a mutable object and pass that in to the constructor.



回答5:

The (relatively) new Software Design paradigm called Domain Driven design, makes the distinction between entity objects and value objects.

Entity Objects are defined as anything that has to map to a key-driven object in a persistent data store, like an employee, or a client, or an invoice, etc... where changing the properties of the object implies that you need to save the change to a data store somewhere, and the existence of multiple instances of a class with the same "key" imnplies a need to synchronize them, or coordinate their persistence to the data store so that one instance' changes do not overwrite the others. Changing the properties of an entity object implies you are changing something about the object - not changing WHICH object you are referencing...

Value objects otoh, are objects that can be considered immutable, whose utility is defined strictly by their property values, and for which multiple instances, do not need to be coordinated in any way... like addresses, or telephone numbers, or the wheels on a car, or the letters in a document... these things are totally defined by their properties... an uppercase 'A' object in an text editor can be interchanged transparently with any other uppercase 'A' object throughout the document, you don't need a key to distinguish it from all the other 'A's In this sense it is immutable, because if you change it to a 'B' (just like changing the phone number string in a phone number object, you are not changing the data associated with some mutable entity, you are switching from one value to another... just as when you change the value of a string...



回答6:

System.String is a good example of a immutable class with setters and mutating methods, only that each mutating method returns a new instance.



回答7:

Expanding on the point by @Cory Foy and @Charles Bretana where there is a difference between entities and values. Whereas value-objects should always be immutable, I really don't think that an object should be able to freeze themselves, or allow themselves to be frozen arbitrarily in the codebase. It has a really bad smell to it, and I worry that it could get hard to track down where exactly an object was frozen, and why it was frozen, and the fact that between calls to an object it could change state from thawed to frozen.

That isn't to say that sometimes you want to give a (mutable) entity to something and ensure it isn't going to be changed.

So, instead of freezing the object itself, another possibility is to copy the semantics of ReadOnlyCollection< T >

List<int> list = new List<int> { 1, 2, 3}; ReadOnlyCollection<int> readOnlyList = list.AsReadOnly(); 

Your object can take a part as mutable when it needs it, and then be immutable when you desire it to be.

Note that ReadOnlyCollection< T > also implements ICollection< T > which has an Add( T item) method in the interface. However there is also bool IsReadOnly { get; } defined in the interface so that consumers can check before calling a method that will throw an exception.

The difference is that you can't just set IsReadOnly to false. A collection either is or isn't read only, and that never changes for the lifetime of the collection.

It would be nice at time to have the const-correctness that C++ gives you at compile time, but that starts to have it's own set of problems and I'm glad C# doesn't go there.


ICloneable - I thought I'd just refer back to the following:

Do not implement ICloneable

Do not use ICloneable in public APIs

Brad Abrams - Design Guidelines, Managed code and the .NET Framework



回答8:

This is an important problem, and I've love to see more direct framework/language support to solve it. The solution you have requires a lot of boilerplate. It might be simple to automate some of the boilerplate by using code generation.

You'd generate a partial class that contains all the freezable properties. It would be fairly simple to make a reusable T4 template for this.

The template would take this for input:

  • namespace
  • class name
  • list of property name/type tuples

And would output a C# file, containing:

  • namespace declaration
  • partial class
  • each of the properties, with the corresponding types, a backing field, a getter, and a setter which invokes the FailIfFrozen method

AOP tags on freezable properties could also work, but it would require more dependencies, whereas T4 is built into newer versions of Visual Studio.

Another scenario which is very much like this is the INotifyPropertyChanged interface. Solutions for that problem are likely to be applicable to this problem.



回答9:

My problem with this pattern is that you're not imposing any compile-time restraints upon immutability. The coder is responsible for making sure an object is set to immutable before for example adding it to a cache or another non-thread-safe structure.

That's why I would extend this coding pattern with a compile-time restraint in the form of a generic class, like this:

public class Immutable<T> where T : IElement {     private T value;      public Immutable(T mutable)      {         this.value = (T) mutable.Clone();         this.value.MakeReadOnly();     }      public T Value      {         get          {             return this.value;         }     }      public static implicit operator Immutable<T>(T mutable)      {         return new Immutable<T>(mutable);     }      public static implicit operator T(Immutable<T> immutable)     {         return immutable.value;     } } 

Here's a sample how you would use this:

// All elements of this list are guaranteed to be immutable List<Immutable<SampleElement>> elements =      new List<Immutable<SampleElement>>();  for (int i = 1; i < 10; i++)  {     SampleElement newElement = new SampleElement();     newElement.Id = Guid.NewGuid();     newElement.Name = "Sample" + i.ToString();      // The compiler will automatically convert to Immutable<SampleElement> for you     // because of the implicit conversion operator     elements.Add(newElement); }  foreach (SampleElement element in elements)     Console.Out.WriteLine(element.Name);  elements[3].Value.Id = Guid.NewGuid();      // This will throw an ImmutableElementException 


回答10:

Just a tip to simplify the element properties: Use automatic properties with private set and avoid explicitly declaring the data field. e.g.

public class SampleElement {   public SampleElement(Guid id, string name) {     Id = id;     Name = name;   }    public Guid Id {     get; private set;   }    public string Name {     get; private set;   } } 


回答11:

Here is a new video on Channel 9 where Anders Hejlsberg from 36:30 in the interview starts talking about immutability in C#. He gives a very good use case for popsicle immutability and explains how this is something you are currently required to implement yourself. It was music to my ears hearing him say it is worth thinking about better support for creating immutable object graphs in future versions of C#

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



回答12:

Two other options for your particular problem that haven't been discussed:

  1. Build your own deserializer, one that can call a private property setter. While the effort in building the deserializer at the beginning will be much more, it makes things cleaner. The compiler will keep you from even attempting to call the setters and the code in your classes will be easier to read.

  2. Put a constructor in each class that takes an XElement (or some other flavor of XML object model) and populates itself from it. Obviously as the number of classes increases, this quickly becomes less desirable as a solution.



回答13:

How about having an abstract class ThingBase, with subclasses MutableThing and ImmutableThing? ThingBase would contain all the data in a protected structure, providing public read-only properties for the fields and protected read-only property for its structure. It would also provide an overridable AsImmutable method which would return an ImmutableThing.

MutableThing would shadow the properties with read/write properties, and provide both a default constructor and a constructor that accepts a ThingBase.

Immutable thing would be a sealed class that overrides AsImmutable to simply return itself. It would also provide a constructor that accepts a ThingBase.



回答14:

I dont like the idea of being able to change an object from a mutable to an immutable state, that kind of seems to defeat the point of design to me. When are you needing to do that? Only objects which represent VALUES should be immutable



回答15:

You can use optional named arguments together with nullables to make an immutable setter with very little boilerplate. If you really do want to set a property to null then you may have some more troubles.

class Foo{      ...     public Foo          Set         ( double? majorBar=null         , double? minorBar=null         , int?        cats=null         , double?     dogs=null)     {         return new Foo             ( majorBar ?? MajorBar             , minorBar ?? MinorBar             , cats     ?? Cats             , dogs     ?? Dogs);     }      public Foo         ( double R         , double r         , int l         , double e         )      {         ....     } } 

You would use it like so

var f = new Foo(10,20,30,40); var g = f.Set(cat:99); 


标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!