17

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

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

4

6 回答 6

14

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

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

var immutable = new MyImmutableObjectBuilder()
  .SetProp1(1)
  .SetProp2(2)
  .Build();
于 2012-09-06T01:17:39.130 回答
13

Does this mean that the constructor on this class really needs to have 13 parameters in an immutable design?

In general, yes. An immutable type with 13 properties will require some means of initializing all of those values.

If they are not all used, or if some properties can be determined based on the other properties, then you can perhaps have one or more overloaded constructors with fewer parameters. However, a constructor (whether or not the type is immutable) really should fully initialize the data for the type in a way that the type is logically "correct" and "complete."

This class represents some on-disk format which really does have this many properties, so refactoring it into smaller bits is pretty much out of the question at this point.

If the "on-disk format" is something that's being determined at runtime, you could potentially have a factory method or constructor which takes the initialization data (ie: the filename? etc) and builds the fully-initialized type for you.

于 2012-09-06T01:05:41.373 回答
3

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

于 2012-09-06T01:03:16.967 回答
3

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

于 2012-09-06T01:04:55.780 回答
2

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

于 2012-09-06T00:58:05.570 回答
0

如果您的意图是在编译期间禁止分配,那么您必须坚持构造函数分配和私有设置器。但是它有很多缺点——你不能使用新成员初始化,也不能使用 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; }
}

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

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

于 2012-09-06T03:04:39.127 回答