不可改变的对象的图案C#-你怎么想?[关闭]
-
06-07-2019 - |
题
我有过程的几个项目开发的一个模式,创建不可改变的(只读)的目的和不可改变的对象的图表。不可改变的对象承受100%的线的安全,因此可以重复使用跨线。在我的工作,我经常使用这种模式在网应用程序配置和其他目的,我负荷和高速缓冲存储器中。缓存的目的应始终是不可改变的因为你要保证他们不是意外改变。
现在,你当然可以很容易地设计成不变的对象,如以下例子:
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; }
}
}
这是简单的类别-但对于更复杂的课程,我不喜欢这一概念的传递所有价值观通过构造。具有制定者在性质更希望和你的代码构建一个新的对象变得更易于阅读。
那么,你如何创造不可改变的对象制定者?
嗯,在我图象开始了完全的可变直到冻结他们有一个单一的方法的呼吁。一旦一个目被冻结,它将保持不变永远的-它不能被变成一个可变的对象。如果你需要一个可变的版本,你只需进行克隆。
好了,现在,一些代码。我已经在下列代码段试着把模式下降到其最简单的形式。该IElement为基础的界面,所有不可改变的对象必须最终实现。
public interface IElement : ICloneable
{
bool IsReadOnly { get; }
void MakeReadOnly();
}
元素类是默认的执行情况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;
}
}
}
现在你可以改变的Id财产和名称的财产作为长作为对象没有被标记为不可改变通过调MakeReadOnly()方法。一旦它被不可改变的,叫器将产生一个ImmutableElementException.
最后的注:完整的模式是复杂得多的代码段所示的在这里。它还包含用于支持集合中的不可变的目的和完整的对象图表的不可改变的对象的图表。完整的模式使得你把整个不可改变的对象图通过调用MakeReadOnly()方法上的最外层的对象。一旦你开始创造更大的对象模型使用这种模式的危险漏对象增加。一个漏水的对象是失败叫FailIfImmutable()方法之前作出改变的对象。测试对于泄漏我们还开发了一个通用的泄漏检测类使用单元的测试。它使用反射测试,如果所有的属性和方法扔ImmutableElementException在不可改变的状态。换言之有效的使用在这里。
我已经长大喜欢这个图案很多,并找到很大的好处。所以我想知道的是如果你们使用类似的模式?如果是的话,你知道的任何良好的资源,这一文件?我基本上在寻找潜在的改进,并对任何标准可能已经存在关于这一主题。
解决方案
有关信息,第二种方法称为<!>“;冰棒不变性<!>”。
Eric Lippert有一系列关于不变性的博客文章这里。我仍然在掌握CTP(C#4.0),但看起来很有意思/可选/命名参数(到.ctor)可能在这里做什么(当映射到只读字段时)... [更新:我在此此处上发表了博客
有关信息,我可能不会制作这些方法virtual
- 我们可能不希望子类能够使其不可冻结。如果您希望他们能够添加额外的代码,我建议如下:
[public|protected] void Freeze()
{
if(!frozen)
{
frozen = true;
OnFrozen();
}
}
protected virtual void OnFrozen() {} // subclass can add code here.
此外 - AOP(如PostSharp)可能是添加所有ThrowIfFrozen()检查的可行选项。
(如果我更改了术语/方法名称,请道歉 - 在撰写回复时SO不会保留原始帖子)
其他提示
另一种选择是创建某种Builder类。
例如,在Java(以及C#和许多其他语言)中,String是不可变的。如果要执行多个操作来创建String,可以使用StringBuilder。这是可变的,然后一旦你完成,你就会让它返回给你最后的String对象。从那时起,它就是不可改变的。
您可以为其他课程做类似的事情。你有不可变元素,然后是ElementBuilder。所有构建器都会存储您设置的选项,然后在最终确定它时构造并返回不可变元素。
这是一个更多的代码,但我认为它比在一个应该是不可变的类的setter上更清晰。
在我最初不得不为每次修改创建一个新的System.Drawing.Point
之后,我几年前完全接受了这个概念。事实上,我现在默认将每个字段创建为readonly
,只有在有令人信服的理由时才将其更改为可变>#8211;这很少见。
我不太关心跨线程问题(我很少使用相关的代码)。由于语义表达,我发现它更好,更好。不可变性是一个很难错误使用的界面的缩影。
您仍在处理状态,因此如果您的对象在变为不可变之前并行化,则仍然可能被咬住。
更实用的方法可能是使用每个setter返回对象的新实例。或者创建一个可变对象并将其传递给构造函数。
(相对)新的软件设计范例称为域驱动设计,它区分了实体对象和值对象。
实体对象被定义为必须映射到持久数据存储中的密钥驱动对象的任何内容,例如员工,客户端或发票等......其中更改对象的属性意味着您需要将更改保存到某个地方的数据存储,并且存在具有相同<!> quot; key <!>的类的多个实例;需要同步它们,或者将它们的持久性协调到数据存储,以便一个实例的更改不会覆盖其他实例。更改实体对象的属性意味着您正在更改有关该对象的内容 - 而不是更改您引用的WHICH对象...
值对象otoh是可以被视为不可变的对象,其效用严格按其属性值定义,并且多个实例不需要以任何方式协调...如地址或电话号码,或汽车上的轮子,或文件中的字母......这些东西完全由它们的属性定义......文本编辑器中的大写“A”对象可以与任何其他大写的“A”对象透明地互换文件,你不需要一把钥匙来区别于其他所有'A'在这个意义上它是不可变的,因为如果你把它改成'B'(就像更改电话号码对象中的电话号码字符串一样,你没有改变与某个可变实体相关的数据,你正在从一个值切换到另一个...就像你改变一个字符串的值...
System.String是具有setter和mutating方法的不可变类的一个很好的例子,只是每个mutating方法返回一个新实例。
扩大@Cory Foy和@Charles Bretana的观点,实体和价值观之间存在差异。值对象应始终是不可变的,我真的不认为对象应该能够自行冻结,或者允许自己在代码库中任意冻结。它有一种非常难闻的气味,我担心它可能很难找到一个物体被冻结的地方,以及为什么它被冻结,以及在调用一个物体之间可以将状态从解冻变为冻结的事实。
这并不是说有时你想给某个(可变的)实体提供某些东西并确保它不会被改变。
因此,不是冻结对象本身,另一种可能是复制ReadOnlyCollection的语义<!> lt; T <!> gt;
List<int> list = new List<int> { 1, 2, 3};
ReadOnlyCollection<int> readOnlyList = list.AsReadOnly();
你的对象可以在需要时将其作为可变部分,然后在你希望它时是不可变的。
注意ReadOnlyCollection <!> lt; T <!> gt;还实现了ICollection <!> lt; T <!> gt;在界面中有Add( T item)
方法。但是,界面中还定义了bool IsReadOnly { get; }
,以便消费者在调用将引发异常的方法之前进行检查。
不同之处在于您不能将IsReadOnly设置为false。集合是或者不是只读的,并且在集合的生命周期内永远不会改变。
在编译时得到C ++为你提供的const-correctness会很好,但是开始有它自己的一组问题,我很高兴C#不会去那里。
ICloneable - 我想我只想回顾以下内容:
不要实施ICloneable
不要在公共API中使用ICloneable
这是一个重要的问题,我已经喜欢看到更多的直接框架/语言支持,以解决。的解决方案都需要大量的样板。这可能是简单的自动执行的一些样板的使用代码生成。
你会产生一个分类包含了所有的可冻结属性。这将是相当简单的做可重复使用T4的模板。
该模板将采取这对于输入:
- namespace
- 类名称
- 财产清单的名称/类型的元组
并将输出的C#文件,内载:
- namespace宣言》
- 分类
- 每一个的特性,与相应的类型,一个背场,吸气,和一个装置,它援引FailIfFrozen方法
AOP标签上可冻结属性也可以工作,但它将需要更多的依赖性,而T4建成新版本的Visual Studio.
另一种情况下这是非常喜欢这个的 INotifyPropertyChanged
接口。解决这一问题可能适用于这个问题。
我对这种模式的问题是你不会对不变性施加任何编译时限制。编码器负责确保将对象设置为不可变,例如将其添加到缓存或其他非线程安全结构。
这就是为什么我会以泛型类的形式扩展这种编码模式的编译时限制,如下所示:
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;
}
}
以下是您将如何使用此示例的示例:
// 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
简化元素属性的提示:使用private set“>自动属性并避免显式声明数据字段。 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;
}
}
这是第9频道的新视频,其中Anders Hejlsberg从采访中的36:30开始谈论C#的不变性。他给出了冰棒不变性的一个非常好的用例,并解释了这是你目前需要自己实现的。听到他的声音听起来是值得考虑更好地支持在未来版本的C#中创建不可变对象图表
针对您尚未讨论的特定问题的其他两个选项:
-
构建自己的反序列化程序,可以调用私有属性setter。虽然在开始时建立解串器的努力将更多,但它使事情更清洁。编译器将使您甚至不会尝试调用setter,并且类中的代码将更容易阅读。
-
在每个接受XElement(或其他一些XML对象模型)的类中放置一个构造函数,并从中填充自己。显然,随着课程数量的增加,这很快就会变得不那么理想了。
醇>
如何使用一个抽象类ThingBase,具有子类MutableThing和ImmutableThing? ThingBase将包含受保护结构中的所有数据,为字段提供公共只读属性,为其结构提供受保护的只读属性。它还将提供一个可重写的AsImmutable方法,它将返回一个ImmutableThing。
MutableThing会使用读/写属性隐藏属性,并提供默认构造函数和接受ThingBase的构造函数。
不可变的东西将是一个密封的类,它会覆盖AsImmutable以简单地返回它自己。它还将提供一个接受ThingBase的构造函数。
我不喜欢能够将对象从可变状态更改为不可变状态的想法,这种想法似乎打败了我的设计点。你什么时候需要这样做?只有代表VALUES的对象才应该是不可变的
您可以将可选的命名参数与nullables一起使用,以使用非常少的样板来创建一个不可变的setter。如果你确实想要将属性设置为null,那么你可能会遇到更多麻烦。
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
)
{
....
}
}
你会像这样使用它
var f = new Foo(10,20,30,40);
var g = f.Set(cat:99);