Cuando diseñe clases para contener su modelo de datos que he leído, puede ser útil crear objetos inmutables, pero ¿en qué momento la carga de las listas de parámetros del constructor y las copias profundas se vuelven demasiado y tiene que abandonar la restricción inmutable?
Por ejemplo, aquí hay una clase inmutable para representar una cosa con nombre (estoy usando la sintaxis de C # pero el principio se aplica a todos los lenguajes OO)
class NamedThing
{
private string _name;
public NamedThing(string name)
{
_name = name;
}
public NamedThing(NamedThing other)
{
this._name = other._name;
}
public string Name
{
get { return _name; }
}
}
Las cosas con nombre se pueden construir, consultar y copiar en cosas nuevas con nombre, pero el nombre no se puede cambiar.
Todo esto está bien, pero ¿qué sucede cuando quiero agregar otro atributo? Tengo que agregar un parámetro al constructor y actualizar el constructor de la copia; lo cual no es demasiado trabajo, pero los problemas comienzan, por lo que puedo ver, cuando quiero hacer que un objeto complejo sea inmutable.
Si la clase contiene muchos atributos y colecciones, que contienen otras clases complejas, me parece que la lista de parámetros del constructor se convertiría en una pesadilla.
Entonces, ¿en qué punto una clase se vuelve demasiado compleja para ser inmutable?
Respuestas:
Cuando se convierten en una carga? Muy rápidamente (especialmente si su idioma de elección no proporciona suficiente soporte sintáctico para la inmutabilidad).
La inmutabilidad se vende como la bala de plata para el dilema multinúcleo y todo eso. Pero la inmutabilidad en la mayoría de los lenguajes OO lo obliga a agregar artefactos y prácticas artificiales en su modelo y proceso. Para cada clase inmutable compleja debe tener un constructor igualmente complejo (al menos internamente). No importa cómo lo diseñe, todavía presenta un acoplamiento fuerte (por lo tanto, es mejor que tengamos una buena razón para presentarlos).
No es necesariamente posible modelar todo en clases pequeñas no complejas. Entonces, para clases y estructuras grandes, las dividimos artificialmente, no porque eso tenga sentido en nuestro modelo de dominio, sino porque tenemos que lidiar con su compleja creación de instancias y constructores en código.
Es aún peor cuando las personas llevan la idea de la inmutabilidad demasiado lejos en un lenguaje de propósito general como Java o C #, haciendo que todo sea inmutable. Luego, como resultado, verá personas forzando construcciones de expresión s en lenguajes que no admiten tales cosas con facilidad.
La ingeniería es el acto de modelar a través de compromisos y compensaciones. Hacer que todo sea inmutable por edicto porque alguien leyó que todo es inmutable en lenguaje funcional X o Y (un modelo de programación completamente diferente), eso no es aceptable. Eso no es buena ingeniería.
Las cosas pequeñas, posiblemente unitarias, pueden hacerse inmutables. Las cosas más complejas pueden volverse inmutables cuando tiene sentido . Pero la inmutabilidad no es una bala de plata. La capacidad de reducir errores, aumentar la escalabilidad y el rendimiento, esa no es la única función de la inmutabilidad. Es una función de las prácticas de ingeniería adecuadas . Después de todo, la gente ha escrito un buen software escalable sin inmutabilidad.
La inmutabilidad se convierte en una carga realmente rápida (se agrega a la complejidad accidental) si se hace sin una razón, cuando se hace fuera de lo que tiene sentido en el contexto de un modelo de dominio.
Yo, por mi parte, trato de evitarlo (a menos que esté trabajando en un lenguaje de programación con un buen soporte sintáctico).
fuente
Pasé por una fase de insistir en que las clases sean inmutables siempre que sea posible. Tenía constructores para casi todo, matrices inmutables, etc., etc. Encontré que la respuesta a su pregunta es simple: ¿en qué punto las clases inmutables se convierten en una carga? Muy rápidamente. Tan pronto como desee serializar algo, debe poder deserializarlo, lo que significa que debe ser mutable; Tan pronto como desee utilizar un ORM, la mayoría de ellos insisten en que las propiedades sean mutables. Y así.
Finalmente reemplacé esa política con interfaces inmutables a objetos mutables.
Ahora el objeto tiene flexibilidad, pero aún puede decirle al código de llamada que no debe editar estas propiedades.
fuente
IComparable<T>
garantiza que siX.CompareTo(Y)>0
yY.CompareTo(Z)>0
, entoncesX.CompareTo(Z)>0
. Las interfaces tienen contratos . Si el contrato deIImmutableList<T>
especifica que los valores de todos los artículos y propiedades deben "establecerse en piedra" antes de que cualquier instancia se exponga al mundo exterior, entonces todas las implementaciones legítimas lo harán. Nada impide que unaIComparable
implementación viole la transitividad, pero las implementaciones que lo hacen son ilegítimas. Si unSortedDictionary
mal funcionamiento cuando se le da un ilegítimoIComparable
, ...IReadOnlyList<T>
serán inmutable, dado que (1) no hay tal requisito se indica en la documentación de la interfaz, y (2) la aplicación más común,List<T>
, ni siquiera es de sólo lectura ? No tengo muy claro qué es ambiguo acerca de mis términos: una colección es legible si los datos que contiene pueden leerse. Es de solo lectura si puede prometer que los datos contenidos no se pueden cambiar a menos que el código contenga alguna referencia externa que lo cambie. Es inmutable si puede garantizar que no se puede cambiar, punto.No creo que haya una respuesta general a esto. Cuanto más compleja es una clase, más difícil es razonar sobre sus cambios de estado y más costosa es crear nuevas copias de ella. Por lo tanto, por encima de cierto nivel (personal) de complejidad, será demasiado doloroso hacer / mantener una clase inmutable.
Tenga en cuenta que una clase demasiado compleja o una larga lista de parámetros de métodos son olores de diseño per se, independientemente de la inmutabilidad.
Por lo general, la solución preferida sería dividir dicha clase en varias clases distintas, cada una de las cuales puede hacerse mutable o inmutable por sí sola. Si esto no es factible, se puede convertir en mutable.
fuente
Puede evitar el problema de la copia si almacena todos sus campos inmutables en un interno
struct
. Esto es básicamente una variación del patrón de recuerdo. Luego, cuando desee hacer una copia, simplemente copie el recuerdo:fuente
Tienes un par de cosas en el trabajo aquí. Los conjuntos de datos inmutables son excelentes para la escalabilidad multiproceso. Esencialmente, puede optimizar su memoria bastante para que un conjunto de parámetros sea una instancia de la clase, en todas partes. Debido a que los objetos nunca cambian, no tiene que preocuparse por sincronizar el acceso a sus miembros. Eso es bueno. Sin embargo, como usted señala, cuanto más complejo es el objeto, más necesita mutabilidad. Comenzaría con el razonamiento en este sentido:
En lenguajes que solo admiten objetos inmutables (como Erlang), si hay alguna operación que parece modificar el estado de un objeto inmutable, el resultado final es una nueva copia del objeto con el valor actualizado. Por ejemplo, cuando agrega un elemento a un vector / lista:
Esa puede ser una forma sensata de trabajar con objetos más complicados. A medida que agrega un nodo de árbol, por ejemplo, el resultado es un nuevo árbol con el nodo agregado. El método en el ejemplo anterior devuelve una nueva lista. En el ejemplo de este párrafo
tree.add(newNode)
, devolvería un nuevo árbol con el nodo agregado. Para los usuarios, resulta fácil trabajar con ellos. Para los escritores de la biblioteca se vuelve tedioso cuando el lenguaje no admite la copia implícita. Ese umbral depende de tu propia paciencia. Para los usuarios de su biblioteca, el límite más sensato que he encontrado es de tres a cuatro parámetros como máximo.fuente
Si tiene varios miembros finales de clase y no desea que estén expuestos a todos los objetos que necesitan crearlo, puede usar el patrón de construcción:
La ventaja es que puede crear fácilmente un nuevo objeto con solo un valor diferente de un nombre diferente.
fuente
newObject
.NamedThing
en este caso)Builder
se reutiliza a, existe un riesgo real de que ocurra lo que mencioné. Alguien podría estar construyendo muchos objetos y decidir que, dado que la mayoría de las propiedades son las mismas, simplemente reutilizarBuilder
y, de hecho, ¡hagámoslo como un singleton global que se inyecte a la dependencia! Whoops Errores importantes introducidos. Entonces, creo que este patrón de instancia mixta vs. estática es malo.En mi opinión, no vale la pena molestarse en hacer que las clases pequeñas sean inmutables en idiomas como el que está mostrando. Estoy usando pequeños aquí y no complejos , porque incluso si agrega diez campos a esa clase y realmente les hace operaciones sofisticadas, dudo que tome kilobytes, mucho menos megabytes, mucho menos gigabytes, por lo que cualquier función que use instancias de su class simplemente puede hacer una copia barata de todo el objeto para evitar modificar el original si quiere evitar causar efectos secundarios externos.
Estructuras de datos persistentes
Donde encuentro uso personal para la inmutabilidad es para grandes estructuras de datos centrales que agregan un montón de datos pequeños como instancias de la clase que está mostrando, como una que almacena un millón
NamedThings
. Al pertenecer a una estructura de datos persistente que es inmutable y estar detrás de una interfaz que solo permite acceso de solo lectura, los elementos que pertenecen al contenedor se vuelven inmutables sin que la clase de elemento (NamedThing
) tenga que ocuparse de él.Copias Baratas
La estructura de datos persistente permite que las regiones se transformen y se hagan únicas, evitando modificaciones al original sin tener que copiar la estructura de datos en su totalidad. Esa es la verdadera belleza de esto. Si quisiera escribir ingenuamente funciones que eviten los efectos secundarios que ingresan una estructura de datos que toma gigabytes de memoria y solo modifica el valor de la memoria de un megabyte, entonces tendría que copiar todo el asunto para evitar tocar la entrada y devolver un nuevo salida. Puede copiar gigabytes para evitar efectos secundarios o causar efectos secundarios en ese escenario, por lo que debe elegir entre dos opciones desagradables.
Con una estructura de datos persistente, le permite escribir dicha función y evitar hacer una copia de toda la estructura de datos, solo requiere aproximadamente un megabyte de memoria adicional para la salida si su función solo transformó el valor de la memoria de un megabyte.
Carga
En cuanto a la carga, hay una inmediata al menos en mi caso. Necesito esos constructores de los que la gente habla o "transitorios" como los llamo para poder expresar efectivamente transformaciones a esa estructura de datos masiva sin tocarla. Código como este:
... entonces tiene que escribirse así:
Pero a cambio de esas dos líneas de código adicionales, la función ahora es segura para llamar a través de subprocesos con la misma lista original, no causa efectos secundarios, etc. También hace que sea realmente fácil hacer que esta operación sea una acción que el usuario puede deshacer ya que deshacer puede almacenar una copia superficial barata de la lista anterior.
Excepción-Seguridad o recuperación de errores
No todos podrían beneficiarse tanto como yo de las estructuras de datos persistentes en contextos como estos (encontré mucho uso para ellos en sistemas de deshacer y edición no destructiva, que son conceptos centrales en mi dominio VFX), pero una cosa es aplicable a casi todos deben tener en cuenta la excepción de seguridad o recuperación de errores .
Si desea hacer que la función de mutación original sea segura para excepciones, entonces necesita una lógica de reversión, para lo cual la implementación más simple requiere copiar toda la lista:
En este punto, la versión mutable segura para excepciones es aún más costosa desde el punto de vista computacional y podría decirse que es aún más difícil de escribir correctamente que la versión inmutable que utiliza un "generador". Y muchos desarrolladores de C ++ simplemente descuidan la seguridad de excepciones y tal vez eso esté bien para su dominio, pero en mi caso me gusta asegurarme de que mi código funcione correctamente incluso en el caso de una excepción (incluso escribir pruebas que arrojan excepciones deliberadamente a la excepción de prueba) seguridad), y eso hace que tenga que poder revertir cualquier efecto secundario que una función cause a la mitad de la función si algo arroja.
Cuando desee estar a salvo de excepciones y recuperarse de errores con gracia sin que su aplicación se bloquee y se queme, entonces debe revertir / deshacer cualquier efecto secundario que una función pueda causar en caso de error / excepción. Y allí el constructor puede ahorrar más tiempo de programador de lo que cuesta junto con el tiempo de cálculo porque: ...
Volviendo a la pregunta fundamental:
Siempre son una carga en los idiomas que giran más en torno a la mutabilidad que a la inmutabilidad, por lo que creo que debería usarlos donde los beneficios superan significativamente los costos. Pero a un nivel lo suficientemente amplio para estructuras de datos lo suficientemente grandes, creo que hay muchos casos en los que es una compensación digna.
También en el mío, solo tengo unos pocos tipos de datos inmutables, y todas ellas son enormes estructuras de datos destinadas a almacenar grandes cantidades de elementos (píxeles de una imagen / textura, entidades y componentes de un ECS, y vértices / bordes / polígonos de una malla).
fuente