2008-12-10 14 views
13

Estoy aprendiendo sobre DDD y me he encontrado con la afirmación de que los "objetos de valor" deberían ser inmutables. Entiendo que esto significa que el estado de los objetos no debería cambiar después de haber sido creado. Esta es una especie de nueva forma de pensar para mí, pero tiene sentido en muchos casos.Cómo diseñar un objeto inmutable con inicialización compleja

Ok, entonces empiezo a crear objetos de valor inmutables.

  • me aseguro de que tomen todo el estado como parámetros al constructor,
  • no añado emisores de propiedad,
  • y asegúrese de que no se permiten los métodos de modificar el contenido (sólo volver nuevos casos)

Pero ahora quiero crear este objeto de valor que contendrá 8 valores numéricos diferentes. Si creo un constructor que tenga 8 parámetros numéricos, creo que no será muy fácil de usar, o más bien, será fácil cometer un error al pasar los números. Esto no puede ser un buen diseño.

Así que la pregunta es: ¿Hay alguna otra forma de mejorar mi objeto inmutable ... alguna magia que se pueda hacer en C# para superar una larga lista de parámetros en el constructor? Estoy muy interesado en escuchar sus ideas ..

ACTUALIZACIÓN: Antes de que nadie lo menciona, una idea se ha discutido aquí: Immutable object pattern in C# - what do you think?

estaría interesado en escuchar otras sugerencias o comentarios sin embargo.

+2

También debe hacer que todos los campos sean solo de lectura. Hace que la inmutabilidad sea más declarativa – JaredPar

Respuesta

22

utilizar un constructor:

public class Entity 
{ 
    public class Builder 
    { 
    private int _field1; 
    private int _field2; 
    private int _field3; 

    public Builder WithField1(int value) { _field1 = value; return this; } 
    public Builder WithField2(int value) { _field2 = value; return this; } 
    public Builder WithField3(int value) { _field3 = value; return this; } 

    public Entity Build() { return new Entity(_field1, _field2, _field3); } 
    } 

    private int _field1; 
    private int _field2; 
    private int _field3; 

    private Entity(int field1, int field2, int field3) 
    { 
    // Set the fields. 
    } 

    public int Field1 { get { return _field1; } } 
    public int Field2 { get { return _field2; } } 
    public int Field3 { get { return _field3; } } 

    public static Builder Build() { return new Builder(); } 
} 

A continuación, cree que le gusta:

Entity myEntity = Entity.Build() 
        .WithField1(123) 
        .WithField2(456) 
        .WithField3(789) 
        .Build() 

Si algunos de los parámetros son opcionales no será necesario llamar al método WithXXX y pueden tener valores por defecto .

+0

Esto parece ser una buena opción. Un código extra, pero creo que obtienes un poco de claridad. Voy a probar esto. –

+3

La desventaja de esto es que no obtiene el beneficio de los inicializadores de objetos C# 3. Esto se puede solucionar teniendo propiedades (así como los métodos para clientes pre-C# 3): nuevo Entity.Builder {Field1 = 123, Field2 = 456, Field3 = 789} .Build() –

+0

Gracias Jon - No lo había hecho Pensé en ese enfoque, ya que estoy atrapado en el mundo de 1.1 y 2.0 en el trabajo y hace poco comencé a jugar con C# 3 en casa. –

3

De la parte superior de mi cabeza, dos respuestas diferentes vienen a la mente ...

... la primera, y probablemente más simple, es el uso de una factoría de objetos (o constructor) como un ayudante que asegura hacer las cosas bien.

inicialización del objeto se vería así:

var factory = new ObjectFactory(); 
factory.Fimble = 32; 
factory.Flummix = "Nearly"; 
var mine = factory.CreateInstance(); 

... la segunda es la creación de su objeto como, mutable, objeto convencional con una función de bloqueo() o Congelar(). Todos sus mutadores deberían verificar si el objeto ha sido bloqueado y lanzar una excepción si lo tiene.

inicialización del objeto se vería así:

var mine = new myImmutableObject(); 
mine.Fimble = 32; 
mine.Flummix = "Nearly"; 
mine.Lock(); // Now it's immutable. 

Qué método para tomar depende mucho de su contexto - una fábrica tiene la ventaja de ser conveniente si usted tiene una serie de objetos similares a construir, pero sí introduce otra clase para escribir y mantener. Un objeto bloqueable significa que solo hay una clase, pero otros usuarios pueden tener errores inesperados en el tiempo de ejecución y las pruebas son más difíciles.

+1

Tenga en cuenta que en ambos casos, C# 3.0 hace que la inicialización sea más compacta. –

8

Por el momento, tendrías que usar un constructor con muchas args o un constructor. En C# 4.0 (VS2010), puede usar argumentos nombrados/opcionales para lograr algo similar a los inicializadores de objetos C# 3.0 - consulte here. El ejemplo en el blog es:

Person p = new Person (forename: "Fred", surname: "Flintstone"); 

Pero se puede ver fácilmente cómo se puede aplicar algo similar para cualquier constructor (u otro método complejo). Compare con la sintaxis objeto inicializador C# 3.0 (con un tipo mutable):

Person p = new Person { Forename = "Fred", Surname = "Flintstone" }; 

No hay mucho que decirles aparte, la verdad.

Jon Skeet también ha publicado algunas reflexiones sobre este tema, here.

+0

¡Solución perfecta para este problema si usa C# 4.0! ¡Gracias! – rene

1

Aunque probablemente sea parte del dominio de lo que está haciendo, y por lo tanto mi sugerencia puede ser inválida, ¿qué hay sobre intentar desglosar los 8 parámetros en grupos lógicos?

Cada vez que veo montones de parámetros, siento que el objeto/método/contructor debería ser más simple.

+0

Ya he hecho mucho de eso, y utilizando el principio de que ninguna clase debería tener más de 7 u 8 campos, terminé con varios objetos de valor con agrupaciones bastante lógicas. Pero un valioso comentario, Chii. –

1

He estado atónito con la misma pregunta que los constructores complejos también es un mal diseño para mí. Tampoco soy un gran admirador del concepto de constructor, ya que parece demasiado código adicional para mantener. Lo que necesitamos es inmutabilidad de paleta, lo que significa que un objeto comienza como mutable donde se le permite usar los establecedores de propiedades. Cuando se establecen todas las propiedades, debe haber una forma de congelar el objeto en un estado inmutable. Desafortunadamente, esta estrategia no se admite de forma nativa en el lenguaje C#. Por lo tanto, terminé el diseño de mi propio patrón para la creación de objetos inmutables como se describe en esta pregunta:

Immutable object pattern in C# - what do you think?

Anders Hejlsberg está hablando de apoyo a este tipo de inmutabilidad de 36:30 en la siguiente entrevista:

Expert to Expert: Anders Hejlsberg - The Future of C#

1

Puede usar la reflexión para inicializar todos los campos del objeto y la pereza para hacer métodos similares al "setter" (utilizando el estilo funcional monádico) para encadenar los métodos/funciones establecidos juntos.

Por ejemplo:

Puede utilizar esta clase de base:

public class ImmutableObject<T> 
{ 
    private readonly Func<IEnumerable<KeyValuePair<string, object>>> initContainer; 

    protected ImmutableObject() {} 

    protected ImmutableObject(IEnumerable<KeyValuePair<string,object>> properties) 
    { 
     var fields = GetType().GetFields().Where(f=> f.IsPublic); 

     var fieldsAndValues = 
      from fieldInfo in fields 
      join keyValuePair in properties on fieldInfo.Name.ToLower() equals keyValuePair.Key.ToLower() 
      select new {fieldInfo, keyValuePair.Value}; 

     fieldsAndValues.ToList().ForEach(fv=> fv.fieldInfo.SetValue(this,fv.Value)); 

    } 

    protected ImmutableObject(Func<IEnumerable<KeyValuePair<string,object>>> init) 
    { 
     initContainer = init; 
    } 

    protected T setProperty(string propertyName, object propertyValue, bool lazy = true) 
    { 

     Func<IEnumerable<KeyValuePair<string, object>>> mergeFunc = delegate 
                     { 
                      var propertyDict = initContainer == null ? ObjectToDictonary() : initContainer(); 
                      return propertyDict.Select(p => p.Key == propertyName? new KeyValuePair<string, object>(propertyName, propertyValue) : p).ToList(); 
                     }; 

     var containerConstructor = typeof(T).GetConstructors() 
      .First(ce => ce.GetParameters().Count() == 1 && ce.GetParameters()[0].ParameterType.Name == "Func`1"); 

     return (T) (lazy ? containerConstructor.Invoke(new[] {mergeFunc}) : DictonaryToObject<T>(mergeFunc())); 
    } 

    private IEnumerable<KeyValuePair<string,object>> ObjectToDictonary() 
    { 
     var fields = GetType().GetFields().Where(f=> f.IsPublic); 
     return fields.Select(f=> new KeyValuePair<string,object>(f.Name, f.GetValue(this))).ToList(); 
    } 

    private static object DictonaryToObject<T>(IEnumerable<KeyValuePair<string,object>> objectProperties) 
    { 
     var mainConstructor = typeof (T).GetConstructors() 
      .First(c => c.GetParameters().Count()== 1 && c.GetParameters().Any(p => p.ParameterType.Name == "IEnumerable`1")); 
     return mainConstructor.Invoke(new[]{objectProperties}); 
    } 

    public T ToObject() 
    { 
     var properties = initContainer == null ? ObjectToDictonary() : initContainer(); 
     return (T) DictonaryToObject<T>(properties); 
    } 
} 

se puede implementar de esta manera:

public class State:ImmutableObject<State> 
{ 
    public State(){} 
    public State(IEnumerable<KeyValuePair<string,object>> properties):base(properties) {} 
    public State(Func<IEnumerable<KeyValuePair<string, object>>> func):base(func) {} 

    public readonly int SomeInt; 
    public State someInt(int someInt) 
    { 
     return setProperty("SomeInt", someInt); 
    } 

    public readonly string SomeString; 
    public State someString(string someString) 
    { 
     return setProperty("SomeString", someString); 
    } 
} 

y se puede utilizar como esto:

//creating new empty object 
var state = new State(); 

// Set fields, will return an empty object with the "chained methods". 
var s2 = state.someInt(3).someString("a string"); 
// Resolves all the "chained methods" and initialize the object setting all the fields by reflection. 
var s3 = s2.ToObject(); 
0

Taka una mirada en la biblioteca Remute https://github.com/ababik/Remute

Puede producir un nuevo objeto inmutable aplicando la expresión lambda a la existente. Sin generación de código o código de placa de caldera como patrón de generador.

E.g.

var entity = new Entity(field1, field2, field3); 
entity = remute.With(entity, x => x.Field1, "foo"); 

También funciona con estructuras anidadas inmutables.

Cuestiones relacionadas