How to implement and extend the Joshua builder template in .net?

Below is the code I tried, is there a better way to do this?

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() { } } 
  • How do we extend such a class? Do we need to write separate builder classes for each of the resulting classes?

     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) { } } 
+17
c #
Nov 24 '08 at 9:12
source share
4 answers

In protocol buffers, we implement such a construction template (greatly simplified):

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

This is not exactly the same as the template in EJ2, but:

  • Copying data is not required during assembly. In other words, while you are setting properties, you are doing this on a real object - you simply cannot see it. This is similar to what StringBuilder does.
  • The builder becomes invalid after calling Build() to guarantee immutability. This, unfortunately, means that it cannot be used as a kind of “prototype” in how the version of EJ2 can.
  • We use properties, not getters and setters, for the most part, that fit nicely into C # 3 object initializers.
  • We also provide setters returning this for users prior to C # 3.

I really did not look at inheritance with the builder pattern - it is still not supported in protocol buffers. I suspect this is rather complicated.

+21
Nov 24 '08 at 9:32
source share

This blog post may be of interest.

An explicit variation on a template in C # is to use an implicit casting operator to make the final Build () call unnecessary:

 public class CustomerBuilder { ...... public static implicit operator Customer( CustomerBuilder builder ) { return builder.Build(); } } 
+4
Nov 24 '08 at 10:05
source share

Edit: I used this again and simplified it to remove redundant check of values ​​in setters.

I recently implemented a version that works well.

Builders are factories that cache the most recent instance. Derived collectors instantiate and clear the cache when something changes.

The base class is simple:

 public abstract class Builder<T> : IBuilder<T> { public static implicit operator T(Builder<T> 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; } } 

The problem we are solving is more subtle. Let them say that we have the concept of Order :

 public class Order { public string ReferenceNumber { get; private set; } public DateTime? ApprovedDateTime { get; private set; } public void Approve() { ApprovedDateTime = DateTime.Now; } } 

ReferenceNumber does not change after creation, so we model it read-only through the constructor:

 public Order(string referenceNumber) { // ... validate ... ReferenceNumber = referenceNumber; } 

How do we restore an existing conceptual Order , for example, from database data?

This is the root of ORM being disabled: it seeks to force public setters on ReferenceNumber and ApprovedDateTime for convenience. What was clear truth is hidden to future readers; we could even say that this is the wrong model. (The same is true for extension points: forcing virtual eliminates the ability of base classes to communicate their intentions.)

A Builder with specialized knowledge is a useful example. An alternative to nested types would be internal access. It allows you to change the domain behavior (POCO) and, as a bonus, the "prototype" template mentioned by Jon Skeet.

First add the internal constructor to Order :

 internal Order(string referenceNumber, DateTime? approvedDateTime) { ReferenceNumber = referenceNumber; ApprovedDateTime = approvedDateTime; } 

Then add Builder with mutable properties:

 public class OrderBuilder : Builder<Order> { 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); } } } 

An interesting bit is SetField calls. Defined by Builder , it encapsulates the template "set the support field, if it is different, then clear the instance", which would otherwise be in the properties settings:

  protected bool SetField<TField>( ref TField field, TField newValue, IEqualityComparer<T> equalityComparer = null) { equalityComparer = equalityComparer ?? EqualityComparer<TField>.Default; var different = !equalityComparer.Equals(field, newValue); if(different) { field = newValue; ClearInstance(); } return different; } 

We use ref so that we can change the support field. We also use the default equalizer, but allow callers to override it.

Finally, when we need to restore Order , we use an implicit cast OrderBuilder :

 Order order = new OrderBuilder { ReferenceNumber = "ABC123", ApprovedDateTime = new DateTime(2008, 11, 25) }; 

It happened a very long time. Hope this helps!

+2
Nov 26 '08 at 1:54
source share

The reason for using the builder template by Joshua Bloch was to create a complex object from parts, as well as to make it immutable.

In this particular case, using optional named parameters in C # 4.0 is cleaner. You refuse some flexibility in the design (do not rename the parameters), but you get a more convenient code for maintenance, easier.

If NutritionFacts Code:

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

Then the client will use it as

  NutritionFacts nf2 = new NutritionFacts(240, 2, calories: 100, fat: 40); 

If the design is more complex, it will need to be fine-tuned; if the “building” of calories is greater than putting an integer, other auxiliary objects may be needed.

0
Jul 12 '10 at 7:45 a.m.
source share



All Articles