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.
fuente
List<Number>
que puede contenerInteger
,Float
,Long
,BigDecimal
, etc ... Todos los cuales son inmutables sí mismos.Respuestas:
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.
fuente
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.
fuente
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.
fuente
No creo que sea una preocupación de la que deba preocuparse el implementador del accesorio. Si
interface X
se 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.
fuente
MutableObject
tienen
métodos que cambian de estado ym
métodos que devuelven el estado,ImmutableDecorator
puede 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.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 noIQueue<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 generalIStack<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 dePush
yPop
haciaStack<T>
.Tener
Stack<T>
implementosIStack<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 tipoIStack<T>
. Esto le permite pasar pilas inmutables. Idealmente,IStack<T>
sería parte de la biblioteca estándar en sí. En .NET, no existeIStack<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.
fuente