出于各种原因,我想在设计中开始使用更多不可变类型。目前,我正在使用一个现有类的项目:
public class IssueRecord
{
// The real class has more readable names :)
public string Foo { get; set; }
public string Bar { get; set; }
public int Baz { get; set; }
public string Prop { get; set; }
public string Prop2 { get; set; }
public string Prop3 { get; set; }
public string Prop4 { get; set; }
public string Prop5 { get; set; }
public string Prop6 { get; set; }
public string Prop7 { get; set; }
public string Prop8 { get; set; }
public string Prop9 { get; set; }
public string PropA { get; set; }
}
这个类代表了一些确实具有这么多属性的磁盘格式,因此在这一点上将其重构为较小的位是非常不可能的。
这是否意味着此类的构造函数确实需要在不可变设计中包含13个参数?如果没有,如果我要使这个设计不可变,我可以采取哪些步骤来减少构造函数中接受的参数数量?
答案 0 :(得分:14)
要减少参数的数量,可以将它们分组为合理的集合,但要拥有真正的不可变对象,必须在构造函数/工厂方法中对其进行初始化。
一些变体是创建“构建器”类,您可以使用流畅的接口配置,而不是请求最终对象。如果您实际上计划在代码的不同位置创建许多此类对象,这将是有意义的,否则在一个地方的许多参数可能是可接受的权衡。
var immutable = new MyImmutableObjectBuilder()
.SetProp1(1)
.SetProp2(2)
.Build();
答案 1 :(得分:13)
这是否意味着此类的构造函数确实需要在不可变设计中包含13个参数?
总的来说,是的。具有13个属性的不可变类型将需要一些初始化所有这些值的方法。
如果未全部使用它们,或者可以根据其他属性确定某些属性,则可能有一个或多个具有较少参数的重载构造函数。但是,构造函数(无论类型是否是不可变的)实际应该以类型逻辑上“正确”和“完整”的方式完全初始化类型的数据。
这个类代表了一些确实具有这么多属性的磁盘格式,因此在这一点上将其重构为较小的位是非常不可能的。
如果“磁盘格式”是在运行时确定的东西,你可能有一个工厂方法或构造函数来获取初始化数据(即:文件名?等)并为你构建完全初始化的类型
答案 2 :(得分:3)
也许按原样保留当前的类,如果可能,提供合理的默认值并重命名为IssueRecordOptions。将其用作不可变的IssueRecord的单个初始化参数。
答案 3 :(得分:3)
您可以在构造函数中使用命名参数和可选参数的组合。如果值总是不同的,那么是的,你就会陷入一个疯狂的构造函数。
答案 4 :(得分:2)
你可以创建一个结构,但是你仍然需要声明结构。但总有阵列等。如果它们都是相同的数据类型,您可以通过多种方式对它们进行分组,例如数组,列表或字符串。看起来你是对的,你的所有不可变类型必须以某种方式通过构造函数,通过13个参数,或通过结构,数组,列表等...
答案 5 :(得分:0)
如果您的意图是在编译期间禁止分配,那么您必须坚持构造函数分配和私有设置者。但是它有许多缺点 - 你不能使用新的成员初始化,也不能使用xml deseralization等。
我会建议这样的事情:
public class IssuerRecord
{
public string PropA { get; set; }
public IList<IssuerRecord> Subrecords { get; set; }
}
public class ImmutableIssuerRecord
{
public ImmutableIssuerRecord(IssuerRecord record)
{
PropA = record.PropA;
Subrecords = record.Subrecords.Select(r => new ImmutableIssuerRecord(r));
}
public string PropA { get; private set; }
// lacks Count and this[int] but it's IReadOnlyList<T> is coming in 4.5.
public IEnumerable<ImmutableIssuerRecord> Subrecords { get; private set; }
// you may want to get a mutable copy again at some point.
public IssuerRecord GetMutableCopy()
{
var copy = new IssuerRecord
{
PropA = PropA,
Subrecords = new List<IssuerRecord>(Subrecords.Select(r => r.GetMutableCopy()))
};
return copy;
}
}
这里的IssuerRecord更具描述性和实用性。当您将其传递到其他地方时,您可以轻松创建不可变版本。在immutable上运行的代码应该具有只读逻辑,因此它不应该真正关心它是否与IssuerRecord类型相同。我创建了每个字段的副本,而不是仅仅包装对象,因为它可能仍然在其他地方更改,但它可能不是必需的,尤其是对于顺序同步调用。但是,将完整的不可变副本存储到某个集合“以后”更安全。对于应用程序,当您希望某些代码禁止修改但仍能够接收对象状态的更新时,它可能是一个包装器。
var record = new IssuerRecord { PropA = "aa" };
if(!Verify(new ImmutableIssuerRecord(record))) return false;
如果您认为在C ++术语中,您可以将ImmutableIssuerRecords视为“IssuerRecord const”。你必须采取extracare来保护你的不可变对象拥有的对象,这就是为什么我建议为所有孩子创建一个副本(Subrecords示例)。
ImmutableIssuerRecord.Subrecors在这里是IEnumerable,缺少Count和this [],但是IReadOnlyList是4.5,如果需要你可以从文档中复制它(并且以后可以很容易地进行迁移)。
还有其他方法,例如Freezable:
public class IssuerRecord
{
private bool isFrozen = false;
private string propA;
public string PropA
{
get { return propA; }
set
{
if( isFrozen ) throw new NotSupportedOperationException();
propA = value;
}
}
public void Freeze() { isFrozen = true; }
}
使代码再次不易读取,并且不提供编译时保护。但你可以正常创建对象,然后在它们准备好后冻结它们。
构建器模式也是需要考虑的因素,但从我的角度来看,它增加了太多的“服务”代码。