首页 文章

不可改变的阶级结构设计

提问于
浏览
5

所以我们都认识到不可变类型的好处,特别是在多线程场景中 . (或者至少我们都应该意识到这一点;参见例如System.String . )

但是,我没有看到的是关于创建不可变实例的讨论,特别是设计指南 .

例如,假设我们想要具有以下不可变类:

class ParagraphStyle {
    public TextAlignment Alignment {get;}
    public float FirstLineHeadIndent {get;}
    // ...
}

我见过的最常见的方法是拥有可变/不可变的类型,例如:可变List<T>和不可变ReadOnlyCollection<T>类型或可变StringBuilder和不可变String类型 .

要模仿这个现有模式,需要引入某种类型的"mutable" ParagraphStyle 类型"duplicates"成员(提供setter),然后提供一个 ParagraphStyle 构造函数,它接受mutable类型作为参数

// Option 1:
class ParagraphStyleCreator {
    public TextAlignment {get; set;}
    public float FirstLineIndent {get; set;}
    // ...
}

class ParagraphStyle {
    // ... as before...
    public ParagraphStyle (ParagraphStyleCreator value) {...}
}

// Usage:
var paragraphStyle = new ParagraphStyle (new ParagraphStyleCreator {
    TextAlignment = ...,
    FirstLineIndent = ...,
});

因此,这有效,支持IDE中的代码完成,并使得如何构建事物的事情相当明显......但它确实看起来相当重复 .

有没有更好的办法?

例如,C#匿名类型是不可变的,并且允许使用“普通”属性设置器进行初始化:

var anonymousTypeInstance = new {
    Foo = "string",
    Bar = 42;
};
anonymousTypeInstance.Foo = "another-value"; // compiler error

不幸的是,在C#中复制这些语义的最接近的方法是使用构造函数参数:

// Option 2:
class ParagraphStyle {
    public ParagraphStyle (TextAlignment alignment, float firstLineHeadIndent,
            /* ... */ ) {...}
}

但这并不能很好地“扩展”;如果您的类型有例如15个属性,一个带有15个参数的构造函数是友好的,并且为所有15个属性提供“有用”的重载是一个噩梦的秘诀 . 我完全拒绝了这一点 .

如果我们试图模仿匿名类型,似乎我们可以在“immutable”类型中使用“set-once”属性,从而删除“mutable”变体:

// Option 3:
class ParagraphStyle {
    bool alignmentSet;
    TextAlignment alignment;

    public TextAlignment Alignment {
        get {return alignment;}
        set {
            if (alignmentSet) throw new InvalidOperationException ();
            alignment = value;
            alignmentSet = true;
        }
    }
    // ...
}

这个问题是它抱怨,并且初始化不是线程安全的 . 因此,添加 Commit() 方法变得很诱人,以便对象可以知道开发人员已完成设置属性(从而导致之前未设置的所有属性在调用其setter时抛出),但这似乎是一个让事情变得更糟,不是更好的方法 .

有没有比可变/不可变类拆分更好的设计?或者我注定要处理成员重复?

1 回答

  • 2

    在一些项目中,我使用了流利的方法 . 即大多数通用属性(例如名称,位置, Headers )是通过ctor定义的,而其他通过Set方法返回新的不可变实例来更新 .

    class ParagraphStyle {
      public TextAlignment Alignment {get; private set;}
      public float FirstLineHeadIndent {get; private set;}
      // ...
      public ParagraphStyle WithAlignment(TextAlignment ta) {
          var newStyle = (ParagraphStyle)MemberwiseClone();
          newStyle.TextAlignment = ta;
      }
      // ...
    }
    

    这里的MemberwiseClone很好,我们的课程真的非常不可改变 .

相关问题