Pregunta

Tengo en el transcurso de un par de proyectos desarrollado un patrón para la creación de inmutable (readonly) de los objetos y objeto inmutable gráficos.Los objetos inmutables llevar la ventaja de ser 100% seguro para subprocesos, y por lo tanto pueden ser reutilizados a través de los subprocesos.En mi trabajo estoy muy a menudo el uso de este patrón en las aplicaciones Web para la configuración y otros objetos que me carga y caché en la memoria.Los objetos en caché siempre debe ser inmutable como desee a fin de garantizar que no se cambia de forma inesperada.

Ahora, por supuesto, usted puede diseñar fácilmente objetos inmutables, como en el siguiente ejemplo:

public class SampleElement
{
  private Guid id;
  private string name;

  public SampleElement(Guid id, string name)
  {
    this.id = id;
    this.name = name;
  }

  public Guid Id
  {
    get { return id; }
  }

  public string Name
  {
    get { return name; }
  }
}

Esto está bien para clases sencillas - pero más complejas de las clases no me apetece la noción de transmisión de todos los valores a través de un constructor.Tener incubadoras en las propiedades es más deseable y su código de construcción de un nuevo objeto se hace más fácil de leer.

Entonces, ¿cómo crear objetos inmutables con incubadoras?

Bueno, en mi patrón de objetos de empezar como un ser totalmente mutable hasta que se congelen con una sola llamada al método.Una vez que el objeto está congelada permanece inmutable para siempre -, no puede ser convertido en un objeto mutable de nuevo.Si usted necesita un mutable versión del objeto, simplemente clonar.

Ok, ahora en el código.Tengo en los siguientes fragmentos de código tratado de hervir el patrón hacia abajo a su forma más simple.El IElement es la interfaz base de que todos los objetos inmutables, en última instancia, debe implementar.

public interface IElement : ICloneable
{
  bool IsReadOnly { get; }
  void MakeReadOnly();
}

La clase de Elemento es el valor predeterminado de la aplicación de la IElement interfaz:

public abstract class Element : IElement
{
  private bool immutable;

  public bool IsReadOnly
  {
    get { return immutable; }
  }

  public virtual void MakeReadOnly()
  {
    immutable = true;
  }

  protected virtual void FailIfImmutable()
  {
    if (immutable) throw new ImmutableElementException(this);
  }

  ...
}

Vamos a refactorizar el SampleElement de la clase anterior para implementar el objeto inmutable patrón:

public class SampleElement : Element
{
  private Guid id;
  private string name;

  public SampleElement() {}

  public Guid Id
  {
    get 
    { 
      return id; 
    }
    set
    {
      FailIfImmutable();
      id = value;
    }
  }

  public string Name
  {
    get 
    { 
      return name; 
    }
    set
    {
      FailIfImmutable();
      name = value;
    }
  }
}

Ahora usted puede cambiar el Id de la propiedad y el Nombre de la propiedad, siempre que el objeto no ha sido marcado como inmutable llamando a la MakeReadOnly() método.Una vez que es inmutable, llamar a un setter se producirá un ImmutableElementException.

Nota Final:El patrón completo es más complejo que los fragmentos de código que se muestra aquí.También incluye soporte para las colecciones de objetos inmutables y completa los gráficos de objetos de objeto inmutable gráficos.El patrón completo permite a su vez la totalidad de un objeto gráfico inmutable llamando a la MakeReadOnly() método en la parte más externa del objeto.Una vez que comience la creación de grandes modelos de objetos con este diseño, el riesgo de fugas de los objetos aumenta.Una fuga en el objeto es un objeto que no llame a la FailIfImmutable() método antes de hacer un cambio en el objeto.Para la prueba de fugas también he desarrollado un genérico detector de fugas de clase para su uso en pruebas de unidad.Se utiliza la reflexión para comprobar si todas las propiedades y métodos de tirar la ImmutableElementException en el estado inmutable.En otras palabras TDD se utiliza aquí.

He crecido como este patrón es mucho y encontrar grandes beneficios.Así que lo que me gustaría saber es si alguno de ustedes está utilizando patrones similares?Si sí, ¿sabes de algún buen recursos que el documento?Yo soy esencialmente en busca de posibles mejoras y por las normas que ya existen sobre este tema.

¿Fue útil?

Solución

Para información, el segundo enfoque se llama " inmutabilidad de paletas " ;.

Eric Lippert tiene una serie de entradas de blog sobre inmutabilidad que comienzan aquí . Todavía me estoy familiarizando con el CTP (C # 4.0), pero parece interesante lo que los parámetros opcionales / nombrados (para el .ctor) podrían hacer aquí (cuando se asignan a campos de solo lectura) ... [actualización: he publicado un blog en este aquí ]

Para información, probablemente no haría esos métodos virtual - probablemente no queremos que las subclases puedan hacer que no se pueda congelar. Si desea que puedan agregar código adicional, le sugiero algo como:

[public|protected] void Freeze()
{
    if(!frozen)
    {
        frozen = true;
        OnFrozen();
    }
}
protected virtual void OnFrozen() {} // subclass can add code here.

También: AOP (como PostSharp) podría ser una opción viable para agregar todas esas comprobaciones de ThrowIfFrozen ().

(disculpas si he cambiado los nombres de terminología / método - SO no mantiene la publicación original visible al redactar respuestas)

Otros consejos

Otra opción sería crear algún tipo de clase Builder.

Por ejemplo, en Java (y C # y muchos otros lenguajes), la cadena es inmutable. Si desea realizar varias operaciones para crear una Cadena, use un StringBuilder. Esto es mutable, y luego, una vez que haya terminado, le devolverá el objeto String final. A partir de entonces es inmutable.

Podrías hacer algo similar para tus otras clases. Tienes tu Elemento inmutable, y luego un ElementBuilder. Todo lo que haría el constructor es almacenar las opciones que establezca, luego, cuando lo finalice, construirá y devolverá el Elemento inmutable.

Es un poco más de código, pero creo que es más limpio que tener setters en una clase que se supone que es inmutable.

Después de mi incomodidad inicial por el hecho de que tuve que crear un nuevo System.Drawing.Point en cada modificación, abracé totalmente el concepto hace algunos años. De hecho, ahora creo cada campo como readonly por defecto y solo lo cambio para que sea mutable si hay una razón convincente & # 8211; que sorprendentemente rara vez hay.

Sin embargo, no me importan mucho los problemas de subprocesamiento cruzado (rara vez uso código donde esto es relevante). Simplemente lo encuentro mucho, mucho mejor debido a la expresividad semántica. La inmutabilidad es el epítome de una interfaz que es difícil de usar incorrectamente.

Todavía está lidiando con el estado y, por lo tanto, puede ser mordido si sus objetos están paralelos antes de volverse inmutables.

Una forma más funcional podría ser devolver una nueva instancia del objeto con cada setter. O cree un objeto mutable y páselo al constructor.

El paradigma (relativamente) nuevo de diseño de software llamado diseño impulsado por dominio, hace la distinción entre objetos de entidad y objetos de valor.

Los objetos de entidad se definen como cualquier cosa que tenga que asignarse a un objeto accionado por clave en un almacén de datos persistente, como un empleado o un cliente, o una factura, etc., donde cambiar las propiedades del objeto implica que debe guardar el cambio en un almacén de datos en algún lugar y la existencia de varias instancias de una clase con la misma " key " implica la necesidad de sincronizarlos o coordinar su persistencia en el almacén de datos para que los cambios de una instancia no sobrescriban a los demás. Cambiar las propiedades de un objeto de entidad implica que está cambiando algo sobre el objeto, no cambiando a qué objeto está haciendo referencia ...

Los objetos de valor otoh, son objetos que pueden considerarse inmutables, cuya utilidad se define estrictamente por sus valores de propiedad, y para los cuales varias instancias, no necesitan ser coordinadas de ninguna manera ... como direcciones o números de teléfono, o las ruedas de un automóvil, o las letras en un documento ... estas cosas están totalmente definidas por sus propiedades ... un objeto 'A' en mayúsculas en un editor de texto puede intercambiarse de manera transparente con cualquier otro objeto 'A' en mayúsculas el documento, no necesita una clave para distinguirlo de todas las otras 'A'. En este sentido, es inmutable, porque si lo cambia a 'B' (al igual que al cambiar la cadena del número de teléfono en un objeto de número de teléfono, no está cambiando los datos asociados con alguna entidad mutable, está cambiando de un valor a otro ... tal como cuando cambia el valor de una cadena ...

System.String es un buen ejemplo de una clase inmutable con setters y métodos de mutación, solo que cada método de mutación devuelve una nueva instancia.

Ampliando el punto por @Cory Foy y @Charles Bretana donde hay una diferencia entre entidades y valores. Mientras que los objetos de valor siempre deben ser inmutables, realmente no creo que un objeto deba poder congelarse, o dejarse congelar arbitrariamente en la base de código. Tiene un olor muy malo, y me preocupa que pueda ser difícil rastrear dónde se congeló exactamente un objeto, y por qué estaba congelado, y el hecho de que entre llamadas a un objeto podría cambiar el estado de descongelado a congelado. .

Eso no quiere decir que a veces quieras dar una entidad (mutable) a algo y asegurarte de que no se va a cambiar.

Entonces, en lugar de congelar el objeto en sí, otra posibilidad es copiar la semántica de ReadOnlyCollection < T & Gt;

List<int> list = new List<int> { 1, 2, 3};
ReadOnlyCollection<int> readOnlyList = list.AsReadOnly();

Su objeto puede tomar parte como mutable cuando lo necesita, y luego ser inmutable cuando lo desee.

Tenga en cuenta que ReadOnlyCollection < T & Gt; también implementa ICollection < T & Gt; que tiene un método Add( T item) en la interfaz. Sin embargo, también hay bool IsReadOnly { get; } definido en la interfaz para que los consumidores puedan verificar antes de llamar a un método que arrojará una excepción.

La diferencia es que no puede establecer IsReadOnly en false. Una colección es o no es de solo lectura, y eso nunca cambia durante la vida útil de la colección.

Sería bueno en el momento tener la precisión constante que C ++ le brinda en el momento de la compilación, pero eso comienza a tener su propio conjunto de problemas y me alegra que C # no vaya allí.


ICloneable : pensé en volver a lo siguiente:

  

No implemente ICloneable

     

No use ICloneable en API públicas

Brad Abrams - Directrices de diseño, código administrado y el. NET Framework

Este es un problema importante, y me encantaría ver más soporte directo de marco / lenguaje para resolverlo. La solución que tiene requiere mucha repetitiva. Puede ser simple automatizar algunas de las repeticiones mediante la generación de código.

Generaría una clase parcial que contiene todas las propiedades congelables. Sería bastante simple hacer una plantilla T4 reutilizable para esto.

La plantilla tomaría esto como entrada:

  • espacio de nombres
  • nombre de clase
  • lista de nombre de propiedad / tipo tuplas

Y generaría un archivo C # que contiene:

  • declaración de espacio de nombres
  • clase parcial
  • cada una de las propiedades, con los tipos correspondientes, un campo de respaldo, un getter y un setter que invoca el método FailIfFrozen

Las etiquetas AOP en propiedades congelables también podrían funcionar, pero requerirían más dependencias, mientras que T4 está integrado en las versiones más nuevas de Visual Studio.

Otro escenario que se parece mucho a esto es la interfaz INotifyPropertyChanged. Es probable que las soluciones para ese problema sean aplicables a este problema.

Mi problema con este patrón es que no estás imponiendo restricciones de tiempo de compilación a la inmutabilidad. El codificador es responsable de asegurarse de que un objeto esté configurado como inmutable antes, por ejemplo, de agregarlo a un caché u otra estructura no segura para subprocesos.

Es por eso que extendería este patrón de codificación con una restricción de tiempo de compilación en forma de una clase genérica, como esta:

public class Immutable<T> where T : IElement
{
    private T value;

    public Immutable(T mutable) 
    {
        this.value = (T) mutable.Clone();
        this.value.MakeReadOnly();
    }

    public T Value 
    {
        get 
        {
            return this.value;
        }
    }

    public static implicit operator Immutable<T>(T mutable) 
    {
        return new Immutable<T>(mutable);
    }

    public static implicit operator T(Immutable<T> immutable)
    {
        return immutable.value;
    }
}

Aquí hay una muestra de cómo usaría esto:

// All elements of this list are guaranteed to be immutable
List<Immutable<SampleElement>> elements = 
    new List<Immutable<SampleElement>>();

for (int i = 1; i < 10; i++) 
{
    SampleElement newElement = new SampleElement();
    newElement.Id = Guid.NewGuid();
    newElement.Name = "Sample" + i.ToString();

    // The compiler will automatically convert to Immutable<SampleElement> for you
    // because of the implicit conversion operator
    elements.Add(newElement);
}

foreach (SampleElement element in elements)
    Console.Out.WriteLine(element.Name);

elements[3].Value.Id = Guid.NewGuid();      // This will throw an ImmutableElementException

Sólo un consejo para simplificar las propiedades del elemento:Uso las propiedades automáticas con private set y evitar declarar explícitamente que el campo de datos.por ejemplo,

public class SampleElement {
  public SampleElement(Guid id, string name) {
    Id = id;
    Name = name;
  }

  public Guid Id {
    get; private set;
  }

  public string Name {
    get; private set;
  }
}

Aquí hay un nuevo video en el Canal 9 donde Anders Hejlsberg a partir de las 36:30 en la entrevista comienza a hablar sobre la inmutabilidad en C #. Da un muy buen caso de uso para la inmutabilidad de paletas y explica cómo esto es algo que actualmente debe implementar usted mismo. Fue música para mis oídos escucharlo decir que vale la pena pensar en un mejor soporte para crear gráficos de objetos inmutables en futuras versiones de C #

Experto a experto: Anders Hejlsberg - El futuro de C #

Otras dos opciones para su problema particular que no se han discutido:

  1. Cree su propio deserializador, uno que pueda llamar a un configurador de propiedad privada. Si bien el esfuerzo para construir el deserializador al principio será mucho más, hace que las cosas estén más limpias. El compilador evitará que incluso intentes llamar a los setters y el código en tus clases será más fácil de leer.

  2. Coloque un constructor en cada clase que tome un XElement (o algún otro tipo de modelo de objeto XML) y se complete a partir de él. Obviamente, a medida que aumenta el número de clases, esto rápidamente se vuelve menos deseable como solución.

¿Qué tal tener una clase abstracta ThingBase, con subclases MutableThing e ImmutableThing? ThingBase contendría todos los datos en una estructura protegida, proporcionando propiedades públicas de solo lectura para los campos y propiedades protegidas de solo lectura para su estructura. También proporcionaría un método AsImmutable reemplazable que devolvería un ImmutableThing.

MutableThing sombrearía las propiedades con propiedades de lectura / escritura, y proporcionaría un constructor predeterminado y un constructor que acepte una ThingBase.

Lo inmutable sería una clase sellada que anula AsImmutable para simplemente devolverse. También proporcionaría un constructor que acepte una ThingBase.

No me gusta la idea de poder cambiar un objeto de un estado mutable a un estado inmutable, ese tipo de cosas parece derrotar el punto de diseño para mí. ¿Cuándo necesitas hacer eso? Solo los objetos que representan VALORES deben ser inmutables

Puede usar argumentos con nombre opcionales junto con valores nulables para hacer un setter inmutable con muy poco repetitivo. Si realmente desea establecer una propiedad como nula, es posible que tenga más problemas.

class Foo{ 
    ...
    public Foo 
        Set
        ( double? majorBar=null
        , double? minorBar=null
        , int?        cats=null
        , double?     dogs=null)
    {
        return new Foo
            ( majorBar ?? MajorBar
            , minorBar ?? MinorBar
            , cats     ?? Cats
            , dogs     ?? Dogs);
    }

    public Foo
        ( double R
        , double r
        , int l
        , double e
        ) 
    {
        ....
    }
}

Lo usarías así

var f = new Foo(10,20,30,40);
var g = f.Set(cat:99);
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top