【发布时间】:2010-09-20 18:51:31
【问题描述】:
在几个项目的过程中,我开发了一种用于创建不可变(只读)对象和不可变对象图的模式。不可变对象具有 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 创建不可变对象?
嗯,在我的模式中,对象一开始是完全可变的,直到你用一个方法调用冻结它们。一旦一个对象被冻结,它将永远保持不可变——它不能再次变成一个可变对象。如果您需要对象的可变版本,只需克隆它即可。
好的,现在开始一些代码。我在下面的代码中 sn-ps 试图将模式归结为最简单的形式。 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。
最后说明: 完整的模式比这里显示的代码 sn-ps 更复杂。它还包含对不可变对象集合和不可变对象图的完整对象图的支持。完整模式使您可以通过调用最外层对象的 MakeReadOnly() 方法将整个对象图变为不可变。一旦开始使用这种模式创建更大的对象模型,泄漏对象的风险就会增加。泄漏对象是在对对象进行更改之前未能调用 FailIfImmutable() 方法的对象。为了测试泄漏,我还开发了一个用于单元测试的通用泄漏检测器类。它使用反射来测试是否所有属性和方法都将 ImmutableElementException 抛出为不可变状态。 换句话说,这里使用的是 TDD。
我越来越喜欢这种模式,并从中发现了很多好处。所以我想知道的是你们中是否有人使用类似的模式?如果是,您是否知道任何记录它的好资源?我本质上是在寻找潜在的改进以及可能已经存在于该主题的任何标准。
【问题讨论】:
-
.NET Framework 指南建议不要实现 ICloneable 或在公共 API 中使用它,因为 ICloneable 的协定没有指定满足协定所需的克隆实现类型。
-
如果您使用的是 WPF,其中大部分已经可以通过从 Freezable 类下降来获得。
-
谢谢乔 - 不知道这门课。看起来很不错。
-
你可能会考虑抛出一个已经存在的 ReadOnlyException,而不是创建你自己的 ImmutableElementException。
标签: c# functional-programming design-patterns immutability