c# - patron - ¿Cómo implementar y extender el patrón de constructor de Joshua en.net?
implementar patron observer c# (4)
- ¿Cómo podemos implementar el patrón Builder de Java efectivo de Joshua en C #?
Debajo está el código que he intentado, ¿hay una mejor manera de hacer esto?
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 extendemos tal clase? ¿Necesitamos escribir clases de generador separadas 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) { } }
Esta entrada de blog puede ser de interés
Una clara variación en el patrón en C # es el uso de un operador de conversión implícito para hacer que la llamada final a Build () sea innecesaria:
public class CustomerBuilder
{
......
public static implicit operator Customer( CustomerBuilder builder )
{
return builder.Build();
}
}
En Protocol Buffers, implementamos el patrón de generador de esta manera (muy simplificado):
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 lo mismo que el patrón en EJ2, pero:
- No se requiere copiar datos en el momento de 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 a
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 que los incubadores devuelvan
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 Buffers de protocolo. Sospecho que es bastante complicado.
La razón para usar el patrón de construcción de Joshua Bloch fue crear un objeto complejo a partir de partes y también hacerlo inmutable.
En este caso particular, usar parámetros nombrados opcionales en C # 4.0 es más limpio. 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 de NutritionFacts es:
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;
}
}
Entonces un cliente lo usaría como
NutritionFacts nf2 = new NutritionFacts(240, 2, calories: 100, fat: 40);
Si la construcción es más compleja, sería necesario ajustarla; si la "construcción" de calorías es más que un número entero, es concebible que se necesiten otros objetos auxiliares.
Editar: 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 clase base es 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;
}
}
El problema que estamos resolviendo es más sutil. Digamos que tenemos el concepto de una 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 lo modelamos como de solo lectura a través del constructor:
public Order(string referenceNumber)
{
// ... validate ...
ReferenceNumber = referenceNumber;
}
¿Cómo reconstituimos un Order
conceptual existente a partir de, por ejemplo, datos de la base de datos?
Esta es la raíz de la desconexión de ORM : tiende a forzar a los instaladores 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 sus intenciones).
Un Builder
con conocimiento especial es un patrón útil. Una alternativa a los tipos anidados sería internal
acceso internal
. Permite la mutabilidad, el comportamiento del dominio (POCO) y, como bonificación, el patrón "prototipo" mencionado por Jon Skeet.
Primero, agrega un constructor internal
a 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); }
}
}
Lo interesante son las llamadas a SetField
. Definido por Builder
, encapsula el patrón de "establecer el campo de respaldo si es diferente, luego borre la instancia" que de otro modo estaría en los establecedores de propiedades:
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;
}
Usamos ref
para permitirnos modificar el campo de respaldo. También usamos el comparador de igualdad predeterminado pero permite que los llamadores lo anulen.
Finalmente, cuando necesitamos reconstituir una Order
, usamos OrderBuilder
con la OrderBuilder
implícita:
Order order = new OrderBuilder
{
ReferenceNumber = "ABC123",
ApprovedDateTime = new DateTime(2008, 11, 25)
};
Esto se puso realmente largo. ¡Espero eso ayude!