2010-02-04 11 views
5

De modo que todos nos damos cuenta de los beneficios de los tipos inmutables, particularmente en escenarios multiproceso. (O al menos todos deberíamos darnos cuenta, ver por ejemplo System.String.)Diseño de construcción de clase inmutable

Sin embargo, lo que no he visto es mucha discusión para creando instancias inmutables, específicamente las pautas de diseño.

Por ejemplo, supongamos que queremos tener la siguiente clase inmutable:

class ParagraphStyle { 
    public TextAlignment Alignment {get;} 
    public float FirstLineHeadIndent {get;} 
    // ... 
} 

El enfoque más común que he visto es tener "pares" mutables/inmutables de tipos, por ejemplo los tipos mutables List<T> e inmutables ReadOnlyCollection<T> o los tipos mutables StringBuilder e inmutables String.

Para imitar este patrón existente requeriría la introducción de algún tipo de "mutable" ParagraphStyle tipo que "duplicados" los miembros (para proporcionar fijadores), y luego proporcionar un constructor ParagraphStyle que acepta el tipo mutable como un argumento

// Option 1: 
class ParagraphStyleCreator { 
    public TextAlignment {get; set;} 
    public float FirstLineIndent {get; set;} 
    // ... 
} 

class ParagraphStyle { 
    // ... as before... 
    public ParagraphStyle (ParagraphStyleCreator value) {...} 
} 

// Usage: 
var paragraphStyle = new ParagraphStyle (new ParagraphStyleCreator { 
    TextAlignment = ..., 
    FirstLineIndent = ..., 
}); 

Por lo tanto, esto funciona, admite la finalización de código dentro del IDE, y hace las cosas razonablemente obvias sobre cómo construir cosas ... pero parece bastante duplicativo.

¿Hay una manera mejor?

Por ejemplo, C# tipos anónimos son inmutables, y permitir el uso de fijadores "normales" de propiedad de inicialización:

var anonymousTypeInstance = new { 
    Foo = "string", 
    Bar = 42; 
}; 
anonymousTypeInstance.Foo = "another-value"; // compiler error 

Por desgracia, el camino más cercano para duplicar esta semántica en C# es el uso de parámetros del constructor:

// Option 2: 
class ParagraphStyle { 
    public ParagraphStyle (TextAlignment alignment, float firstLineHeadIndent, 
      /* ... */) {...} 
} 

Pero esto no se "escala" bien; si su tipo tiene, p. 15 propiedades, un constructor con 15 parámetros es cualquier cosa menos amigable, y proporcionar sobrecargas "útiles" para las 15 propiedades es una receta para una pesadilla. Estoy rechazando esto directamente.

Si tratamos de imitar los tipos anónimos, parece que podríamos usar "set-una vez" propiedades de tipo "inmutable", y por lo tanto dejar caer la "mutable" variante:

// Option 3: 
class ParagraphStyle { 
    bool alignmentSet; 
    TextAlignment alignment; 

    public TextAlignment Alignment { 
     get {return alignment;} 
     set { 
      if (alignmentSet) throw new InvalidOperationException(); 
      alignment = value; 
      alignmentSet = true; 
     } 
    } 
    // ... 
} 

El problema con esto es que no es obvio que las propiedades se puedan establecer solo una vez (el compilador ciertamente no se quejará), y la inicialización no es segura para subprocesos. Por lo tanto, resulta tentador agregar un método Commit() para que el objeto sepa que el desarrollador finalizó la configuración de las propiedades (lo que provoca que todas las propiedades que no se hayan configurado previamente arrojen si se invoca su definidor), pero esto parece ser una manera de empeorar las cosas, no mejor.

¿Hay un mejor diseño que la división de clase mutable/inmutable? ¿O estoy condenado a lidiar con la duplicación de miembros?

+0

Me doy cuenta de que esto puede ser difícil de creer, pero estoy trabajando con una base de código C# existente. Transmitir todo el elemento a F # no es una opción, e incluso si fuera una opción, la licencia MSR-SSLA que se publica en las bibliotecas F # no parece adecuada para mi uso, específicamente (ii) (c). – jonp

+0

Esto parece una duplicación de: http://stackoverflow.com/questions/263585/immutable-object-pattern-in-c-what-do-you-think – jonp

+0

Puede que no sea una respuesta, pero he tratado con requisitos similares en el pasado mediante la implementación de ISupportInitialize. Después de una llamada a EndInit() te vuelves inmutable, y throw cuando se llaman setters. No es seguro en tiempo de compilación, pero la documentación y el lanzamiento temprano y a menudo ayuda. – Will

Respuesta

2

En un par de proyectos estaba usando un enfoque fluido. Es decir. la mayoría de las propiedades genéricas (por ejemplo, nombre, ubicación, título) se definen a través de ctor, mientras que otras se actualizan con métodos de Set que devuelven una nueva instancia inmutable.

class ParagraphStyle { 
    public TextAlignment Alignment {get; private set;} 
    public float FirstLineHeadIndent {get; private set;} 
    // ... 
    public ParagraphStyle WithAlignment(TextAlignment ta) { 
     var newStyle = (ParagraphStyle)MemberwiseClone(); 
     newStyle.TextAlignment = ta; 
    } 
    // ... 
} 

MemberwiseClone está bien aquí tan pronto nuestra clase sea realmente profundamente inmutable.

+1

Si bien esto funciona y es útil, tampoco es una solución escalable. Si necesita establecer, p. 15 valores, los métodos del mutador WithFoo() resultan en 14 instancias "temporales" que se crean antes de que tenga su resultado final. Dependiendo de qué tan "pesado" sea su tipo, esto podría ser una cantidad considerable de basura. WithFoo() tampoco es compatible con los inicializadores de colecciones, lo que podría no ser una gran pérdida, pero los quiero mucho ;-). El modismo de Foo + FooBuilder parece ser un compromiso razonable. – jonp

+0

Probé este enfoque en otro momento en uno de los proyectos recientes y estoy completamente de acuerdo, es aburrido crear todos estos envoltorios. Estoy de acuerdo en que Builder es mucho más elegante y bastante legible. Por cierto, ¿qué hay de las estructuras profundamente anidadas? Las estructuras de datos de FP promueven la huella de memoria más baja posible mediante la reutilización de miembros sin cambios. – olegz

+0

También puede proporcionar un constructor del constructor fluido que toma el objeto inmutable como argumento para inicializar los campos internos con él. Entonces, puedes usar 'WithFoo()' solo en esos 'Foos' que necesitas modificar :) –

Cuestiones relacionadas