不可改变的设计:处理构造函数疯狂

时间:2012-09-06 00:53:26

标签: c# immutability

出于各种原因,我想在设计中开始使用更多不可变类型。目前,我正在使用一个现有类的项目:

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个参数?如果没有,如果我要使这个设计不可变,我可以采取哪些步骤来减少构造函数中接受的参数数量?

6 个答案:

答案 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; }
}

使代码再次不易读取,并且不提供编译时保护。但你可以正常创建对象,然后在它们准备好后冻结它们。

构建器模式也是需要考虑的因素,但从我的角度来看,它增加了太多的“服务”代码。