在几个项目的过程中,我开发了一种用于创建不可变(只读)对象和不可变对象图的模式。不可变对象具有 100% 线程安全的优点,因此可以跨线程重用。在我的工作中,我经常在 Web 应用程序中将这种模式用于配置设置以及我在内存中加载和缓存的其他对象。缓存对象应该始终是不可变的,因为您希望确保它们不会被意外更改。
现在,您当然可以轻松地设计不可变对象,如下例所示:
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; }
}
}
这对于简单的类来说很好——但对于更复杂的类,我不喜欢通过构造函数传递所有值的概念。在属性上有设置器更可取,并且构建新对象的代码更易于阅读。
那么如何使用 setter 创建不可变对象呢?
好吧,在我的模式中,对象一开始是完全可变的,直到你用一个方法调用冻结它们。一旦一个对象被冻结,它将永远保持不可变——它不能再次变成一个可变对象。如果您需要对象的可变版本,您只需克隆它。
好的,现在开始一些代码。我在以下代码片段中尝试将模式归结为最简单的形式。IElement 是所有不可变对象最终必须实现的基本接口。
public interface IElement : ICloneable
{
bool IsReadOnly { get; }
void MakeReadOnly();
}
Element 类是 IElement 接口的默认实现:
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);
}
...
}
让我们重构上面的 SampleElement 类来实现不可变对象模式:
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;
}
}
}
您现在可以通过调用 MakeReadOnly() 方法更改 Id 属性和 Name 属性,只要该对象未被标记为不可变。一旦它是不可变的,调用 setter 将产生 ImmutableElementException。
最后说明:完整模式比此处显示的代码片段更复杂。它还包含对不可变对象集合和不可变对象图的完整对象图的支持。完整模式使您可以通过调用最外层对象的 MakeReadOnly() 方法将整个对象图变为不可变。一旦开始使用这种模式创建更大的对象模型,泄漏对象的风险就会增加。泄漏对象是在对对象进行更改之前未能调用 FailIfImmutable() 方法的对象。为了测试泄漏,我还开发了一个用于单元测试的通用泄漏检测器类。它使用反射来测试是否所有属性和方法都将 ImmutableElementException 抛出为不可变状态。换句话说,这里使用了 TDD。
我已经非常喜欢这种模式,并从中发现了巨大的好处。所以我想知道的是你们中是否有人使用类似的模式?如果是,您是否知道任何记录它的好资源?我本质上是在寻找潜在的改进以及在这个主题上可能已经存在的任何标准。