No declare interfaces para objetos inmutables.

27

No declare interfaces para objetos inmutables.

[EDITAR] Cuando los objetos en cuestión representan objetos de transferencia de datos (DTO) o datos antiguos simples (POD)

¿Es esa una pauta razonable?

Hasta ahora, a menudo he creado interfaces para clases selladas que son inmutables (los datos no se pueden cambiar). He tratado de tener cuidado de no usar la interfaz en ningún lugar donde me preocupe la inmutabilidad.

Desafortunadamente, la interfaz comienza a impregnar el código (y no es solo mi código lo que me preocupa). Terminas pasando una interfaz y luego quieres pasarla a un código que realmente quiere asumir que lo que se le pasa es inmutable.

Debido a este problema, estoy considerando nunca declarar interfaces para objetos inmutables.

Esto podría tener ramificaciones con respecto a las Pruebas de Unidad, pero aparte de eso, ¿esto parece una guía razonable?

¿O hay otro patrón que debería usar para evitar el problema de "interfaz de expansión" que estoy viendo?

(Estoy usando estos objetos inmutables por varias razones: principalmente para la seguridad de los subprocesos, ya que escribo mucho código multiproceso; pero también porque significa que puedo evitar hacer copias defensivas de los objetos pasados ​​a los métodos. El código se vuelve mucho más simple en muchos casos en los que sabe que algo es inmutable, lo cual no ocurre si le han entregado una interfaz. De hecho, a menudo ni siquiera puede hacer una copia defensiva de un objeto al que se hace referencia a través de una interfaz si no proporciona operación de clonación o cualquier forma de serialización ...)

[EDITAR]

Para proporcionar mucho más contexto para mis razones para querer hacer que los objetos sean inmutables, vea esta publicación de blog de Eric Lippert:

http://blogs.msdn.com/b/ericlippert/archive/tags/immutability/

También debo señalar que estoy trabajando con algunos conceptos de nivel inferior aquí, como elementos que se están manipulando / pasando en colas de trabajo de subprocesos múltiples. Estos son esencialmente DTO.

También Joshua Bloch recomienda el uso de objetos inmutables en su libro Effective Java .


Seguir

Gracias por los comentarios, todos. He decidido seguir adelante y usar esta guía para los DTO y sus afines. Hasta ahora está funcionando bien, pero solo ha pasado una semana ... Aún así, se ve bien.

Hay algunos otros asuntos relacionados con esto sobre los que quiero preguntar; notablemente algo que llamo "Inmutabilidad profunda o superficial" (nomenclatura que robé de la clonación profunda y superficial), pero esa es una pregunta para otro momento.

Matthew Watson
fuente
3
+1 gran pregunta. Explique por qué es tan importante para usted que el objeto sea de esa clase inmutable en particular en lugar de algo que simplemente implemente la misma interfaz. Es posible que sus problemas estén arraigados en otra parte. La inyección de dependencias es extremadamente importante para las pruebas unitarias, pero tiene muchos otros beneficios importantes que desaparecen si obliga a su código a requerir una clase inmutable particular.
Steven Doggart
1
Estoy un poco confundido por tu uso del término inmutable . Para ser claros, ¿se refiere a una clase sellada, que no puede heredarse ni anularse, o quiere decir que los datos que expone son de solo lectura y no se pueden cambiar una vez que se crea el objeto. En otras palabras, ¿desea garantizar que el objeto es de un tipo particular o que su valor nunca cambiará? En mi primer comentario, supuse que significaba lo primero, pero ahora con su edición, suena más como lo último. ¿O te preocupan ambos?
Steven Doggart
3
Estoy confundido, ¿por qué no dejar a los setters fuera de la interfaz? Pero, por otro lado, me canso de ver interfaces para objetos que realmente son objetos de dominio y / o DTO que requieren fábricas para esos objetos ... me causa disonancia cognitiva.
Michael Brown
2
¿Qué pasa con las interfaces para clases que son todas inmutables? Por ejemplo, Java Número de clase permite definir una List<Number>que puede contener Integer, Float, Long, BigDecimal, etc ... Todos los cuales son inmutables sí mismos.
1
@MikeBrown Supongo que solo porque la interfaz implica inmutabilidad no significa que la implementación la imponga. Podría dejarlo en manos de la convención (es decir, documentar la interfaz de que la inmutabilidad es un requisito), pero podría terminar con algunos problemas realmente desagradables si alguien viola la convención.
Vaughandroid

Respuestas:

17

En mi opinión, su regla es buena (o al menos no es mala), pero solo por la situación que está describiendo. No diría que estoy de acuerdo con eso en todas las situaciones, por lo que, desde el punto de vista de mi pedante interno, debo decir que su regla es técnicamente demasiado amplia.

Por lo general, no definiría objetos inmutables a menos que se utilicen esencialmente como objetos de transferencia de datos (DTO), lo que significa que contienen propiedades de datos pero muy poca lógica y sin dependencias. Si ese es el caso, como parece que está aquí, diría que es seguro usar los tipos concretos directamente en lugar de las interfaces.

Estoy seguro de que habrá algunos puristas de pruebas unitarias que no estarán de acuerdo, pero en mi opinión, las clases de DTO pueden excluirse de forma segura de los requisitos de pruebas unitarias y de inyección de dependencia. No es necesario usar una fábrica para crear un DTO, ya que no tiene dependencias. Si todo crea los DTO directamente según sea necesario, entonces no hay forma de inyectar un tipo diferente de todos modos, por lo que no hay necesidad de una interfaz. Y como no contienen lógica, no hay nada que probar en la unidad. Incluso si contienen algo de lógica, siempre y cuando no tengan dependencias, entonces debería ser trivial probar la lógica de la unidad, si es necesario.

Como tal, creo que establecer una regla de que todas las clases de DTO no implementarán una interfaz, aunque sea potencialmente innecesario, no dañará el diseño de su software. Dado que tiene este requisito de que los datos deben ser inmutables y no puede aplicarlos a través de una interfaz, entonces diría que es totalmente legítimo establecer esa regla como un estándar de codificación.

Sin embargo, el problema más grande es la necesidad de aplicar estrictamente una capa DTO limpia. Mientras sus clases inmutables sin interfaz solo existan en la capa DTO, y su capa DTO permanezca libre de lógica y dependencias, entonces estará a salvo. Sin embargo, si comienza a mezclar sus capas y tiene clases sin interfaz que se duplican como clases de capa empresarial, creo que comenzará a tener muchos problemas.

Steven Doggart
fuente
El código que espera tratar específicamente con un DTO o un objeto de valor inmutable debe usar las características de ese tipo en lugar de las de una interfaz, pero eso no significa que no haya casos de uso para una interfaz implementada por dicha clase y también por otras clases, con métodos que prometen devolver valores que serán válidos durante un tiempo mínimo (por ejemplo, si la interfaz se pasa a una función, los métodos deberían devolver valores válidos hasta que esa función regrese). Tal interfaz puede ser útil si hay varios tipos que encapsulan datos similares y uno desea ...
supercat
... para tener un medio de copiar datos de uno a otro. Si todos los tipos que incluyen ciertos datos implementan la misma interfaz para leerlos, entonces dichos datos pueden copiarse fácilmente entre todos los tipos, sin requerir que cada uno sepa cómo importar de los demás.
supercat
En ese punto, también puede eliminar sus captadores (y establecedores, si sus DTO son mutables por alguna razón estúpida) y hacer públicos los campos. Estas nunca se va a poner ninguna lógica, ¿verdad?
Kevin
@Kevin, supongo, pero con la conveniencia moderna de las propiedades automotrices, es muy fácil convertirlas en propiedades, ¿por qué no? Supongo que si el rendimiento y la eficiencia son la mayor preocupación, entonces quizás eso sea importante. En cualquier caso, la pregunta es, ¿qué nivel de "lógica" permiten en un DTO? Incluso si pone algo de lógica de validación en sus propiedades, no sería un problema probarlo unitario siempre que no tuviera dependencias que requirieran burlarse. Mientras el DTO no use ningún objeto comercial de dependencia, entonces es seguro tener una pequeña lógica incorporada en ellos porque aún serán verificables.
Steven Doggart
Personalmente, prefiero mantenerlos lo más limpios posible de ese tipo de cosas, pero siempre hay ocasiones en las que es necesario doblegar las reglas, por lo que es mejor dejar la puerta abierta por si acaso.
Steven Doggart
7

Solía ​​hacer un gran alboroto por hacer que mi código fuera invulnerable al mal uso. Hice interfaces de solo lectura para ocultar miembros mutantes, agregué muchas restricciones a mis firmas genéricas, etc., etc. Resultó que la mayoría de las veces tomé decisiones de diseño porque no confiaba en mis compañeros de trabajo imaginarios. "Quizás algún día contraten a un nuevo chico de nivel de entrada y él no sabrá que la clase XYZ no puede actualizar DTO ABC. ¡Oh, no!" Otras veces me estaba enfocando en el problema equivocado, ignorando la solución obvia, no viendo el bosque a través de los árboles.

Ya nunca creo interfaces para mis DTO. Trabajo bajo el supuesto de que las personas que tocan mi código (principalmente yo) saben lo que está permitido y lo que tiene sentido. Si sigo cometiendo el mismo error estúpido, generalmente no trato de fortalecer mis interfaces. Ahora paso la mayor parte de mi tiempo tratando de entender por qué sigo cometiendo el mismo error. Por lo general, es porque estoy analizando algo en exceso o me falta un concepto clave. Mi código ha sido mucho más fácil de trabajar desde que dejé de ser paranoico. También termino con menos "marcos" que requieren un conocimiento interno para trabajar en el sistema.

Mi conclusión ha sido encontrar la cosa más simple que funciona. La complejidad adicional de hacer interfaces seguras solo desperdicia el tiempo de desarrollo y complica el código de otra manera simple. Preocúpese por cosas como esta cuando tenga 10,000 desarrolladores usando sus bibliotecas. Créeme, te salvará de mucha tensión innecesaria.

Travis Parks
fuente
Por lo general, guardo interfaces para cuando necesito la inyección de dependencias para las pruebas unitarias.
Travis Parks
5

Parece una buena guía, pero por extrañas razones. He tenido varios lugares donde una interfaz (o clase base abstracta) proporciona acceso uniforme a una serie de objetos inmutables. Las estrategias tienden a caer aquí. Los objetos de estado tienden a caer aquí. No creo que sea demasiado irrazonable dar forma a una interfaz para que parezca inmutable y documentarla como tal en su API.

Dicho esto, las personas tienden a interconectar en exceso los objetos de datos antiguos simples (en adelante, POD) e incluso estructuras simples (a menudo inmutables). Si su código no tiene alternativas sensatas a alguna estructura fundamental, no necesita una interfaz. No, las pruebas unitarias no son una razón suficiente para cambiar su diseño (el acceso burlado a la base de datos no es la razón por la que proporciona una interfaz para eso, es flexibilidad para futuros cambios), no es el fin del mundo si sus pruebas usa esa estructura básica básica tal cual.

Telastyn
fuente
2

El código se vuelve mucho más simple en muchos casos cuando se sabe que algo es inmutable, lo que no se hace si se le ha entregado una interfaz.

No creo que sea una preocupación de la que deba preocuparse el implementador del accesorio. Si interface Xse pretende que sea inmutable, ¿no es responsabilidad del implementador de la interfaz asegurarse de que implementen la interfaz de una manera inmutable?

Sin embargo, en mi opinión, no existe una interfaz inmutable: el contrato de código especificado por una interfaz se aplica solo a los métodos expuestos de un objeto, y nada sobre las partes internas de un objeto.

Es mucho más común ver la inmutabilidad implementada como un decorador en lugar de una interfaz, pero la viabilidad de esa solución realmente depende de la estructura de su objeto y la complejidad de su implementación.

Jonathan Rich
fuente
1
¿Podría dar un ejemplo de implementación de la inmutabilidad como decorador?
vaughandroid
No es trivial, no creo, pero es un ejercicio de pensamiento relativamente simple: si MutableObjecttiene nmétodos que cambian de estado y mmétodos que devuelven el estado, ImmutableDecoratorpuede continuar exponiendo los métodos que devuelven el estado ( m) y, según el entorno, afirmar o lanzar una excepción cuando se llama a uno de los métodos mutables.
Jonathan Rich
Realmente no me gusta convertir la certeza de tiempo de compilación en la posibilidad de excepciones de tiempo de ejecución ...
Matthew Watson
Bien, pero ¿cómo puede saber ImmutableDecorator si un método dado cambia de estado o no?
vaughandroid
No puede estar seguro en ningún caso si una clase que implementa su interfaz es inmutable con respecto a los métodos definidos por su interfaz. @Baqueta El decorador debe tener conocimiento de la implementación de la clase base.
Jonathan Rich
0

Terminas pasando una interfaz y luego quieres pasarla a un código que realmente quiere asumir que lo que se le pasa es inmutable.

En C #, un método que espera un objeto de tipo "inmutable" no debe tener un parámetro de un tipo de interfaz porque las interfaces de C # no pueden especificar el contrato de inmutabilidad. Por lo tanto, la directriz que se propone en sí misma no tiene sentido en C # porque no puede hacerlo en primer lugar (y es poco probable que las futuras versiones de los lenguajes le permitan hacerlo).

Su pregunta surge de un sutil malentendido de los artículos de Eric Lippert sobre inmutabilidad. Eric no definió las interfaces IStack<T>y no IQueue<T>especificó contratos para pilas y colas inmutables. Ellos no. Los definió por conveniencia. Estas interfaces le permitieron definir diferentes tipos de pilas y colas vacías. Podemos proponer un diseño e implementación diferentes de una pila inmutable usando un solo tipo sin requerir una interfaz o un tipo separado para representar la pila vacía, pero el código resultante no se verá tan limpio y sería un poco menos eficiente.

Ahora ceñámonos al diseño de Eric. Un método que requiere una pila inmutable debe tener un parámetro de tipo en Stack<T>lugar de la interfaz general IStack<T>que representa el tipo de datos abstractos de una pila en el sentido general. No es obvio cómo hacer esto cuando se usa la pila inmutable de Eric y no lo discutió en sus artículos, pero es posible. El problema es con el tipo de pila vacía. Puede resolver este problema asegurándose de que nunca obtenga una pila vacía. Esto se puede garantizar presionando un valor ficticio como el primer valor en la pila y nunca reventando. De esta manera, puede emitir de forma segura los resultados de Pushy Pophacia Stack<T>.

Tener Stack<T>implementos IStack<T>puede ser útil. Puede definir métodos que requieren una pila, cualquier pila, no necesariamente una pila inmutable. Estos métodos pueden tener un parámetro de tipo IStack<T>. Esto le permite pasar pilas inmutables. Idealmente, IStack<T>sería parte de la biblioteca estándar en sí. En .NET, no existe IStack<T>, pero hay otras interfaces estándar que la pila inmutable puede implementar, lo que hace que el tipo sea más útil.

El diseño alternativo y la implementación de la pila inmutable a la que me referí anteriormente usan una interfaz llamada IImmutableStack<T>. Por supuesto, insertar "Inmutable" en el nombre de la interfaz no hace que todos los tipos que lo implementan sean inmutables. Sin embargo, en esta interfaz, el contrato de inmutabilidad es solo verbal. Un buen desarrollador debe respetarlo.

Si está desarrollando una biblioteca interna pequeña, puede estar de acuerdo con todos los miembros del equipo para cumplir este contrato y puede usarlo IImmutableStack<T>como tipo de parámetros. De lo contrario, no debe usar el tipo de interfaz.

Me gustaría agregar, ya que etiquetó la pregunta C #, que en la especificación C #, no existen DTO y POD. Por lo tanto, descartarlos o definirlos con precisión mejora la pregunta.

Hadi Brais
fuente