Tengo un problema de diseño con respecto a las propiedades de .NET.
interface IX
{
Guid Id { get; }
bool IsInvalidated { get; }
void Invalidate();
}
Problema:
Esta interfaz tiene dos propiedades de solo lectura, Id
y IsInvalidated
. Sin embargo, el hecho de que sean de solo lectura no garantiza que sus valores permanezcan constantes.
Digamos que era mi intención dejar muy en claro que ...
Id
representa un valor constante (que, por lo tanto, puede almacenarse en caché de forma segura), mientras queIsInvalidated
podría cambiar su valor durante la vida útil de unIX
objeto (y, por lo tanto, no debería almacenarse en caché).
¿Cómo podría modificar interface IX
para hacer ese contrato suficientemente explícito?
Mis propios tres intentos de solución:
La interfaz ya está bien diseñada. La presencia de un método llamado
Invalidate()
permite a un programador inferir que el valor de la propiedad con un nombre similarIsInvalidated
podría verse afectado por ella.Este argumento es válido solo en los casos en que el método y la propiedad tienen un nombre similar.
Aumente esta interfaz con un evento
IsInvalidatedChanged
:bool IsInvalidated { get; } event EventHandler IsInvalidatedChanged;
La presencia de un
…Changed
evento para losIsInvalidated
estados que esta propiedad puede cambiar su valor, y la ausencia de un evento similar paraId
es una promesa de que la propiedad no cambiará su valor.Me gusta esta solución, pero es una gran cantidad de cosas adicionales que pueden no utilizarse en absoluto.
Reemplace la propiedad
IsInvalidated
con un métodoIsInvalidated()
:bool IsInvalidated();
Esto podría ser un cambio demasiado sutil. Se supone que es una pista de que un valor se calcula nuevamente cada vez, lo que no sería necesario si fuera una constante. El tema de MSDN "Elegir entre propiedades y métodos" tiene esto que decir al respecto:
Utilice un método, en lugar de una propiedad, en las siguientes situaciones. […] La operación devuelve un resultado diferente cada vez que se llama, incluso si los parámetros no cambian.
¿Qué tipo de respuestas espero?
Estoy más interesado en soluciones completamente diferentes al problema, junto con una explicación de cómo superaron mis intentos anteriores.
Si mis intentos son lógicamente defectuosos o tienen desventajas significativas aún no mencionadas, de modo que solo quede una solución (o ninguna), me gustaría saber dónde me equivoqué.
Si las fallas son menores y queda más de una solución después de tomarla en consideración, comente.
Como mínimo, me gustaría recibir comentarios sobre cuál es su solución preferida y por qué motivo (s).
fuente
IsInvalidated
necesita unprivate set
?class Foo : IFoo { private bool isInvalidated; public bool IsInvalidated { get { return isInvalidated; } } public void Invalidate() { isInvalidated = true; } }
InvalidStateException
s, por ejemplo, pero no estoy seguro de si esto es teóricamente posible. Sin embargo, sería bueno.Respuestas:
Preferiría la solución 3 sobre 1 y 2.
Mi problema con la solución 1 es : ¿qué sucede si no hay un
Invalidate
método? Asumir una interfaz con unaIsValid
propiedad que devuelveDateTime.Now > MyExpirationDate
; Es posible que no necesite unSetInvalid
método explícito aquí. ¿Qué pasa si un método afecta a múltiples propiedades? Suponga un tipo de conexión conIsOpen
yIsConnected
propiedades: ambos se ven afectados por elClose
método.Solución 2 : Si el único punto del evento es informar a los desarrolladores que una propiedad con un nombre similar puede devolver valores diferentes en cada llamada, entonces recomendaría encarecidamente que no lo haga. Debe mantener sus interfaces cortas y claras; Además, no todas las implementaciones pueden activar ese evento por usted. Reutilizaré mi
IsValid
ejemplo de arriba: tendrías que implementar un temporizador y disparar un evento cuando lleguesMyExpirationDate
. Después de todo, si ese evento es parte de su interfaz pública, los usuarios de la interfaz esperarán que ese evento funcione.Dicho esto sobre estas soluciones: no son malas. La presencia de un método o un evento indicará que una propiedad con un nombre similar puede devolver valores diferentes en cada llamada. Todo lo que digo es que solo ellos no son suficientes para transmitir siempre ese significado.
La solución 3 es lo que buscaría. Como se mencionó aviv, esto solo puede funcionar para desarrolladores de C #. Para mí, como C # -dev, el hecho de que
IsInvalidated
no sea una propiedad transmite de inmediato el significado de "eso no es solo un simple descriptor de acceso, algo está sucediendo aquí". Sin embargo, esto no se aplica a todos, y como señaló MainMa, el marco .NET en sí no es coherente aquí.Si desea utilizar la solución 3, le recomendaría declararla una convención y que todo el equipo la siga. La documentación también es esencial, creo. En mi opinión, en realidad es bastante simple insinuar los valores cambiantes: " devuelve verdadero si el valor sigue siendo válido ", " indica si la conexión ya se ha abierto " vs. " devuelve verdadero si este objeto es válido ". Sin embargo, no estaría de más ser más explícito en la documentación.
Entonces mi consejo sería:
Declare la solución 3 una convención y sígala consistentemente. Deje claro en la documentación de propiedades si una propiedad tiene o no valores cambiantes. Los desarrolladores que trabajan con propiedades simples supondrán correctamente que no cambian (es decir, solo cambian cuando se modifica el estado del objeto). Los desarrolladores que encuentran un método que suena como que podría ser una propiedad (
Count()
,Length()
,IsOpen()
) saben que la calle detrás que está pasando y (con suerte) leer los documentos de método para entender lo que hace exactamente el método y cómo se comporta.fuente
Hay una cuarta solución: confiar en la documentación .
¿Cómo sabes que la
string
clase es inmutable? Simplemente lo sabe, porque ha leído la documentación de MSDN.StringBuilder
, por otro lado, es mutable, porque, nuevamente, la documentación te dice eso.Su tercera solución no es mala, pero .NET Framework no la sigue . Por ejemplo:
son propiedades, pero no esperes que permanezcan constantes cada vez.
Lo que se usa constantemente en .NET Framework en sí es esto:
es un método, mientras que:
Es una propiedad. En el primer caso, hacer cosas puede requerir trabajo adicional, que incluye, por ejemplo, consultar la base de datos. Este trabajo adicional puede llevar algo de tiempo. En el caso de una propiedad, se espera que tome poco tiempo: de hecho, la longitud puede cambiar durante la vida útil de la lista, pero no se necesita prácticamente nada para devolverla.
Con las clases, solo mirar el código muestra claramente que el valor de una propiedad no cambiará durante la vida útil del objeto. Por ejemplo,
Está claro: el precio seguirá siendo el mismo.
Lamentablemente, no puede aplicar el mismo patrón a una interfaz, y dado que C # no es lo suficientemente expresivo en ese caso, se debe utilizar la documentación (incluida la documentación XML y los diagramas UML) para cerrar la brecha.
fuente
Lazy<T>.Value
puede tomar mucho tiempo calcular (cuando se accede por primera vez).Mis 2 centavos:
Opción 3: como no C # -er, no sería una buena idea; Sin embargo, a juzgar por la cita que trae, debe quedar claro para los programadores competentes de C # que esto significa algo. Sin embargo, aún debe agregar documentos sobre esto.
Opción 2: a menos que planee agregar eventos a todo lo que pueda cambiar, no vaya allí.
Otras opciones:
id
, que generalmente se supone que es inmutable incluso en objetos mutables.fuente
Otra opción sería dividir la interfaz: suponiendo que después de llamar a
Invalidate()
cada invocación deIsInvalidated
que devuelva el mismo valortrue
, parece que no hay razón para invocarIsInvalidated
la misma parte que invocóInvalidate()
.Entonces, sugiero, ya sea que verifique la invalidación o la cause , pero parece irracional hacer ambas cosas. Por lo tanto, tiene sentido ofrecer una interfaz que contenga la
Invalidate()
operación a la primera parte y otra interfaz para verificar (IsInvalidated
) a la otra. Como es bastante obvio a partir de la firma de la primera interfaz que hará que la instancia se invalide, la pregunta restante (para la segunda interfaz) es bastante general: cómo especificar si el tipo dado es inmutable .Lo sé, esto no responde la pregunta directamente, pero al menos la reduce a la pregunta de cómo marcar algún tipo inmutable , que es un problema común y comprensible. Por ejemplo, al suponer que todos los tipos son mutables a menos que se defina lo contrario, puede concluir que la segunda interfaz (
IsInvalidated
) es mutable y, por lo tanto, puede cambiar el valor deIsInvalidated
vez en cuando. Por otro lado, supongamos que la primera interfaz se ve así (pseudo-sintaxis):Como está marcado como inmutable, sabe que el Id no cambiará. Por supuesto, llamar a invalidar hará que cambie el estado de la instancia, pero este cambio no será observable a través de esta interfaz.
fuente
[Immutable]
siempre que abarque métodos cuyo único propósito sea causar una mutación. Me gustaría mover elInvalidate()
método a laInvalidatable
interfaz.Id
, obtendrá el mismo valor cada vez. Lo mismo se aplica aInvalidate()
(no obtienes nada, ya que su tipo de retorno esvoid
). Por lo tanto, el tipo es inmutable. Por supuesto, tendrá efectos secundarios , pero no podrá observarlos a través de la interfazIX
, por lo que no tendrán ningún impacto en los clientes deIX
.IX
es inmutable. Y que son efectos secundarios; simplemente se distribuyen entre dos interfaces divididas, de modo que los clientes no necesitan preocuparse (en el caso deIX
) o que es obvio cuál podría ser el efecto secundario (en el caso deInvalidatable
). Pero, ¿por qué no pasarInvalidate
aInvalidatable
? Eso se volveríaIX
inmutable y puro, yInvalidatable
no se volvería más mutable ni más impuro (porque ya son ambos).Invalidate()
yIsInvalidated
por el mismo consumidor de la interfaz . Si llamaInvalidate()
, debe saber que se invalida, entonces, ¿por qué verificarlo? Por lo tanto, puede proporcionar dos interfaces distintas para diferentes propósitos.