【问题标题】:Immutable Design: Dealing with Constructor Insanity不可变设计:处理构造函数的疯狂
【发布时间】:2012-09-06 00:53:26
【问题描述】:

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

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

【问题讨论】:

标签: c# immutability


【解决方案1】:

要减少参数的数量,您可以将它们分组到合理的集合中,但要拥有真正不可变的对象,您必须在构造函数/工厂方法中对其进行初始化。

一些变体是创建“builder”类,您可以使用流畅的界面进行配置,而不是请求最终对象。如果您实际上计划在代码的不同位置创建许多这样的对象,这将是有意义的,否则在一个位置中的多个参数可能是可接受的权衡。

var immutable = new MyImmutableObjectBuilder()
  .SetProp1(1)
  .SetProp2(2)
  .Build();

【讨论】:

    【解决方案2】:

    这是否意味着这个类的构造函数在不可变设计中真的需要有 13 个参数?

    一般来说,是的。具有 13 个属性的不可变类型需要一些方法来初始化所有这些值。

    如果它们没有全部使用,或者如果某些属性可以根据其他属性确定,那么您也许可以拥有一个或多个带有更少参数的重载构造函数。但是,构造函数(无论类型是否不可变)确实应该完全初始化该类型的数据,以使该类型在逻辑上“正确”和“完整”。

    这个类代表了一些确实具有这么多属性的磁盘格式,因此在这一点上将其重构为更小的位几乎是不可能的。

    如果“磁盘上的格式”是在运行时确定的,您可能有一个工厂方法或构造函数来获取初始化数据(即:文件名?等)并为您构建完全初始化的类型.

    【讨论】:

      【解决方案3】:

      也许保持当前类不变,尽可能提供合理的默认值并重命名为 IssueRecordOptions。将其用作不可变 IssueRecord 的单个初始化参数。

      【讨论】:

      • 如果系统的一部分只需要不变性要求,这是一个不错的选择。但是,您仍在处理可变类型,因为您必须改变原始的“选项”类型。
      【解决方案4】:

      您可以在构造函数中使用命名参数和可选参数的组合。如果值总是不同,那么是的,你被一个疯狂的构造函数困住了。

      【讨论】:

        【解决方案5】:

        你可以创建一个结构体,但是你仍然需要声明这个结构体。但是总是有数组之类的。如果它们都是相同的数据类型,您可以通过多种方式对它们进行分组,例如数组、列表或字符串。看来你是对的,你所有的不可变类型都必须以某种方式通过构造函数,通过 13 个参数,或者通过结构、数组、列表等...

        【讨论】:

        • 哦,我才发现大部分都是字符串,所以我说用数组是对的。
        • 数组真的不行,因为它破坏了结构中参数的语义。使用该类的人必须记住,数组中的第 5 个元素表示“详细信息”或类似的东西,这比让类保持可变更糟糕。
        • 对,那么结构是您唯一的其他真正选择。另外,如果我是正确的,在 C# 中,结构可以使用默认值,这在您的情况下可能很有用。
        • 结构并没有真正的帮助——如果你要创建一个不可变的结构,你仍然需要一个构造函数来初始化数据。 (话虽这么说,这几乎违反了何时选择结构的所有规则(> 16 字节,包含引用类型,不代表单个逻辑值等),因此在这里使用类几乎肯定更合适...... )
        • @ReedCopsey:带有暴露字段的结构是在这里使用的最佳选择。在构造函数之外修改this 的结构是有问题的,但是具有暴露字段的结构——与那些暴露读写属性的结构不同——从不修改this。这种结构的语义与类的语义不同,但在许多情况下,它们比可变类更清晰、更清晰,同时比不可变类或所谓的“不可变”结构更方便、更清晰。跨度>
        【解决方案6】:

        如果您的意图是在编译期间禁止赋值,那么您必须坚持使用构造函数赋值和私有 setter。但是它有很多缺点 - 你不能使用新成员初始化,也不能使用 xml 反序列化等。

        我会建议这样的事情:

            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 更具描述性和实用性。当您将它传递到其他地方时,您可以轻松创建不可变版本。在不可变对象上工作的代码应该具有只读逻辑,因此它不应该真正关心它是否与 IssuerRecord 类型相同。我创建了每个字段的副本,而不是仅仅包装对象,因为它可能仍会在其他地方更改,但它可能不是必需的,尤其是对于顺序同步调用。但是,将完整的不可变副本存储到某个集合“供以后”使用会更安全。当您希望某些代码禁止修改但仍能够接收对象状态的更新时,它可能是应用程序的包装器。

        var record = new IssuerRecord { PropA = "aa" };
        if(!Verify(new ImmutableIssuerRecord(record))) return false;
        

        如果您用 C++ 术语思考,您可以将 ImmutableIssuerRecords 视为“IssuerRecord const”。您必须格外小心以保护您的不可变对象拥有的对象,这就是为什么我建议为所有子对象创建一个副本(子记录示例)。

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

        这会再次降低代码的可读性,并且不提供编译时保护。但您可以照常创建对象,然后在它们准备好后冻结它们。

        构建器模式也值得考虑,但从我的角度来看,它添加了太多“服务”代码。

        【讨论】:

        • 你可以使用 XML 反序列化就好了。您只需要使用 DataContractSerializer 而不是 XmlSerializer。这对我来说很好。
        • 是的,这是我的第二个建议,只是这会让你的应用看起来像一个 Rube Goldberg 机器。
        猜你喜欢
        • 1970-01-01
        • 2011-01-26
        • 2013-02-12
        • 2011-01-28
        • 1970-01-01
        • 2013-12-18
        相关资源
        最近更新 更多