2008-11-24 10 views
17

A continuación se muestra el código que he intentado, ¿hay una mejor manera de hacerlo?¿Cómo implementar y extender el patrón de constructor de Joshua en .net?

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() { } 
    } 
  • ¿Cómo podemos extender dicha clase? ¿Es necesario escribir clases de constructor separado para cada una de las clases derivadas?

    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) { } 
    } 
    

Respuesta

21

En búferes de protocolo, se implementa el patrón constructor como esto (muy simplificada):

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

esto no es exactamente el mismo que el patrón en EJ2, pero:

  • No se requiere copiar datos en el momento de la compilación. En otras palabras, mientras configura las propiedades, lo hace en el objeto real; simplemente no puede verlo todavía. Esto es similar a lo que hace StringBuilder.
  • El generador se vuelve inválido después de llamar al Build() para garantizar la inmutabilidad. Desafortunadamente, esto significa que no se puede usar como una especie de "prototipo" en la forma en que lo hace la versión de EJ2.
  • Usamos propiedades en lugar de getters y setters, en su mayor parte, que encajan bien con los inicializadores de objetos de C# 3.
  • También proporcionamos setters que devuelven this por el bien de los usuarios anteriores a C# 3.

Realmente no he investigado la herencia con el patrón del generador, de todos modos no es compatible con los búfers de protocolo. Sospecho que es bastante complicado.

+0

>> "que encaja bien con los inicializadores de objetos de C# 3." En este caso se creará como nuevo SomeMessage.Build (Name =" GK ", Age = 0). Pero es tan bueno como escribir nuevo SomeMessage (Name =" GK ", Age = 0) ¿correcto? En este caso, Necesitamos el método de compilación? ¿Cómo nos ayudará a evitar constructores telescópicos? –

+1

No, se escribirá como nuevo SomeMessage.Builder {Name = "GK", Age = 0}. Tenga en cuenta la diferencia entre corchetes y llaves. La diferencia es que solo necesita especificar las propiedades que desea. Tenga en cuenta que con C# 4, un único constructor con muchos parámetros opcionales proporcionará otra opción. –

+0

Ok. Ahora lo tengo. Nuevo SomeMessage {Name = "GK", Age = 0 } no es posible, las propiedades son privadas, solo el constructor puede acceder. –

4

This blog entry podría ser de interés

Una variación ordenada en el patrón en C# es el uso de un operador de conversión implícita para hacer la llamada final a construir() innecesaria:

public class CustomerBuilder 
{ 

    ......  

    public static implicit operator Customer(CustomerBuilder builder) 
    { 
     return builder.Build(); 
    } 
} 
+0

Eso es un buen truco. –

+0

He utilizado el código (pero ligeramente modificado) de la publicación del blog en varios sistemas de producción y creo que funciona muy bien – Kane

2

Edición: Utilicé esto de nuevo y lo simplifiqué para eliminar la verificación de valor redundante en setters.

Recientemente implementé una versión que funciona bien.

Los constructores son fábricas que almacenan en caché la instancia más reciente. Los constructores derivados crean instancias y borran la caché cuando algo cambia.

La base de clase es sencillo:

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

El problema que estamos resolviendo es más sutil.Digamos que tenemos el concepto de un Order:

public class Order 
{ 
    public string ReferenceNumber { get; private set; } 

    public DateTime? ApprovedDateTime { get; private set; } 

    public void Approve() 
    { 
     ApprovedDateTime = DateTime.Now; 
    } 
} 

ReferenceNumber no cambia después de la creación, por lo que modelarlo de sólo lectura a través del constructor:

public Order(string referenceNumber) 
{ 
    // ... validate ... 

    ReferenceNumber = referenceNumber; 
} 

¿Cómo podemos reconstituir una ya existente conceptual Order de, por ejemplo, datos de la base de datos?

Esta es la raíz de la desconexión ORM: tiende a forzar los configuradores públicos en ReferenceNumber y ApprovedDateTime para mayor comodidad técnica. Lo que era una verdad clara está oculto para los lectores futuros; incluso podríamos decir que es un modelo incorrecto. (Lo mismo es cierto para los puntos de extensión: forzar virtual elimina la capacidad de las clases base para comunicar su intención.)

A Builder con conocimiento especial es un patrón útil. Una alternativa a los tipos anidados sería el acceso internal. Permite la mutabilidad, el comportamiento del dominio (POCO) y, como bonificación, el patrón "prototipo" mencionado por Jon Skeet.

En primer lugar, añadir un constructor de internal-Order:

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

A continuación, agregue un Builder con propiedades mutables:

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

La parte interesante es la SetField llamadas. Definido por Builder, que encapsula el patrón de "establecer el campo de respaldo si es diferente, a continuación, desactive la instancia" que sería de otra manera en los emisores de propiedad:

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

Utilizamos ref que nos permitirá modificar el campo respaldo. También usamos el comparador de igualdad predeterminado pero permite que los llamadores lo anulen.

Por último, cuando tenemos que reconstituir una Order, utilizamos OrderBuilder con la conversión implícita:

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

Esto puso muy larga. ¡Espero eso ayude!

+0

Gracias por la respuesta Bryan. De hecho, es una implementación muy clara del patrón. Jon, alasdairg y Jaime también tienen algunas implementaciones similares/diferentes. Personalmente, prefiero tener el método estático de compilación en el tipo, que devuelve la clase de constructor. –

+0

Todavía estoy tratando de averiguar cómo extender una clase de generador de este tipo. Si tenemos un SpecialOrder (bad name !!), entonces tenemos que ir a SpecialOrderBuilder, pero en ese caso el usuario (alguien está usando su código) debe conocer la implementación del constructor de cada uno. –

+0

En mi ejemplo anterior, repito el código en MoreNutritionFacts.Builder, que tampoco es bueno. ¿Hay una implementación más limpia/correcta para esto? Como dijo Jon, parece ser complicado. –

0

La razón para usar el patrón de constructor de Joshua Bloch era crear un objeto complejo sin partes y también hacerlo inmutable.

En este caso particular, el uso de parámetros nominales opcionales en C# 4.0 es más claro. Pierdes algo de flexibilidad en el diseño (no renombras los parámetros), pero obtienes un código mejor y más fácil de mantener.

Si el código es 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; 
    } 
    } 

A continuación, un cliente podría utilizarlo como

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

Si la construcción es más compleja esto tendría que ser ajustado; si la "construcción" de calorías es más que un número entero, es concebible que se necesiten otros objetos auxiliares.

Cuestiones relacionadas