如何在.net中实现和扩展Joshua的构建器模式?
- 我们如何在C#中实现Joshua的Effective Java的Builder模式?
下面是我试过的代码,有更好的方法吗?
public class NutritionFacts { public static NutritionFacts.Builder Build(string name, int servingSize, int servingsPerContainer) { return new NutritionFacts.Builder(name, servingSize, servingsPerContainer); } public sealed class Builder { public Builder(String name, int servingSize, int servingsPerContainer) { } public Builder totalFat(int val) { } public Builder saturatedFat(int val) { } public Builder transFat(int val) { } public Builder cholesterol(int val) { } //... 15 more setters public NutritionFacts build() { return new NutritionFacts(this); } } private NutritionFacts(Builder builder) { } protected NutritionFacts() { } }
-
我们如何扩展这样的课程? 我们是否需要为每个派生类编写单独的构建器类?
public class MoreNutritionFacts : NutritionFacts { public new static MoreNutritionFacts.Builder Build(string name, int servingSize, int servingsPerContainer) { return new MoreNutritionFacts.Builder(name, servingSize, servingsPerContainer); } public new sealed class Builder { public Builder(String name, int servingSize, int servingsPerContainer) {} public Builder totalFat(int val) { } public Builder saturatedFat(int val) { } public Builder transFat(int val) { } public Builder cholesterol(int val) { } //... 15 more setters public Builder newProperty(int val) { } public MoreNutritionFacts build() { return new MoreNutritionFacts(this); } } private MoreNutritionFacts(MoreNutritionFacts.Builder builder) { } }
在Protocol Buffers中,我们实现了这样的构建器模式(大大简化):
public sealed class SomeMessage { public string Name { get; private set; } public int Age { get; private set; } // Can only be called in this class and nested types private SomeMessage() {} public sealed class Builder { private SomeMessage message = new SomeMessage(); public string Name { get { return message.Name; } set { message.Name = value; } } public int Age { get { return message.Age; } set { message.Age = value; } } public SomeMessage Build() { // Check for optional fields etc here SomeMessage ret = message; message = null; // Builder is invalid after this return ret; } } }
这与EJ2中的模式不完全相同,但是:
- 在构建时不需要复制数据。 换句话说,当你设置属性时,你在真实对象上这样做 – 你还没有看到它。 这类似于
StringBuilder
function。 - 调用
Build()
以保证不变性后,构建器变为无效。 遗憾的是,它不能像EJ2版本那样用作一种“原型”。 - 在大多数情况下,我们使用属性而不是getter和setter – 这与C#3的对象初始化器非常吻合。
- 我们还提供了为了预先C#3用户而返回
this
setter。
我还没有真正研究过构建模式的inheritance – 无论如何它都不支持Protocol Buffers。 我怀疑这很棘手。
此博客文章可能会引起您的兴趣
C#中模式的一个巧妙变化是使用隐式强制转换运算符来最终调用Build():
public class CustomerBuilder { ...... public static implicit operator Customer( CustomerBuilder builder ) { return builder.Build(); } }
编辑:我再次使用它并简化它以删除setter中的冗余值检查。
我最近实现了一个很好的版本。
构建器是缓存最新实例的工厂。 派生的构建器在任何更改时创建实例并清除缓存。
基类很简单:
public abstract class Builder : IBuilder { public static implicit operator T(Builder builder) { return builder.Instance; } private T _instance; public bool HasInstance { get; private set; } public T Instance { get { if(!HasInstance) { _instance = CreateInstance(); HasInstance = true; } return _instance; } } protected abstract T CreateInstance(); public void ClearInstance() { _instance = default(T); HasInstance = false; } }
我们正在解决的问题更加微妙。 假设我们有一个Order
的概念:
public class Order { public string ReferenceNumber { get; private set; } public DateTime? ApprovedDateTime { get; private set; } public void Approve() { ApprovedDateTime = DateTime.Now; } }
ReferenceNumber
在创建后不会更改,因此我们通过构造函数将其建模为只读:
public Order(string referenceNumber) { // ... validate ... ReferenceNumber = referenceNumber; }
我们如何从数据库数据中重新构建现有的概念性Order
?
这是ORM断开连接的根源:它倾向于强制使用ReferenceNumber
和ApprovedDateTime
上的公共setter以方便技术。 未来的读者隐藏着什么是明确的事实; 我们甚至可以说这是一个不正确的模型。 (对于扩展点也是如此:强制virtual
删除了基类传达其意图的能力。)
具有特殊知识的Builder
是一种有用的模式。 嵌套类型的替代方法是internal
访问。 它支持可变性,域行为(POCO),以及作为奖励的Jon Skeet提到的“原型”模式。
首先,向Order
添加internal
构造函数:
internal Order(string referenceNumber, DateTime? approvedDateTime) { ReferenceNumber = referenceNumber; ApprovedDateTime = approvedDateTime; }
然后,添加一个具有可变属性的Builder
:
public class OrderBuilder : Builder { private string _referenceNumber; private DateTime? _approvedDateTime; public override Order Create() { return new Order(_referenceNumber, _approvedDateTime); } public string ReferenceNumber { get { return _referenceNumber; } set { SetField(ref _referenceNumber, value); } } public DateTime? ApprovedDateTime { get { return _approvedDateTime; } set { SetField(ref _approvedDateTime, value); } } }
有趣的是SetField
调用。 由Builder
定义,它封装了“设置支持字段,如果不同,然后清除实例”的模式,否则将在属性设置器中:
protected bool SetField( ref TField field, TField newValue, IEqualityComparer equalityComparer = null) { equalityComparer = equalityComparer ?? EqualityComparer.Default; var different = !equalityComparer.Equals(field, newValue); if(different) { field = newValue; ClearInstance(); } return different; }
我们使用ref
来允许我们修改支持字段。 我们还使用默认的相等比较器,但允许调用者覆盖它。
最后,当我们需要重新构建一个Order
,我们将OrderBuilder
与隐式OrderBuilder
一起使用:
Order order = new OrderBuilder { ReferenceNumber = "ABC123", ApprovedDateTime = new DateTime(2008, 11, 25) };
这真的很长。 希望能帮助到你!
使用Joshua Bloch的构建器模式的原因是用部件创建一个复杂的对象,并使其不可变。
在这种特殊情况下,在C#4.0中使用可选的命名参数更清晰。 您放弃了一些设计灵活性(不要重命名参数),但是您可以更轻松地获得更好的可维护代码。
如果NutritionFacts代码是:
public class NutritionFacts { public int servingSize { get; private set; } public int servings { get; private set; } public int calories { get; private set; } public int fat { get; private set; } public int carbohydrate { get; private set; } public int sodium { get; private set; } public NutritionFacts(int servingSize, int servings, int calories = 0, int fat = 0, int carbohydrate = 0, int sodium = 0) { this.servingSize = servingSize; this.servings = servings; this.calories = calories; this.fat = fat; this.carbohydrate = carbohydrate; this.sodium = sodium; } }
然后客户端将其用作
NutritionFacts nf2 = new NutritionFacts(240, 2, calories: 100, fat: 40);
如果结构更复杂,则需要进行调整; 如果卡路里的“构建”不仅仅是整数,那么可以想象其他辅助对象是必需的。