¿Cómo diseñaría una interfaz de modo que quede claro qué propiedades pueden cambiar su valor y cuáles permanecerán constantes?

12

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, Idy 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 que
  • IsInvalidatedpodría cambiar su valor durante la vida útil de un IXobjeto (y, por lo tanto, no debería almacenarse en caché).

¿Cómo podría modificar interface IXpara hacer ese contrato suficientemente explícito?

Mis propios tres intentos de solución:

  1. 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 similar IsInvalidatedpodrí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.

  2. Aumente esta interfaz con un evento IsInvalidatedChanged:

    bool IsInvalidated { get; }
    event EventHandler IsInvalidatedChanged;

    La presencia de un …Changedevento para los IsInvalidatedestados que esta propiedad puede cambiar su valor, y la ausencia de un evento similar para Ides 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.

  3. Reemplace la propiedad IsInvalidatedcon un método IsInvalidated():

    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).

stakx
fuente
No IsInvalidatednecesita un private set?
James
2
@James: No necesariamente, ni en la declaración de la interfaz, ni en una implementación:class Foo : IFoo { private bool isInvalidated; public bool IsInvalidated { get { return isInvalidated; } } public void Invalidate() { isInvalidated = true; } }
stakx
@James Las propiedades en las interfaces no son las mismas que las propiedades automáticas, aunque usan la misma sintaxis.
svick
Me preguntaba: ¿las interfaces "tienen el poder" para imponer ese comportamiento? ¿No debería delegarse este comportamiento en cada implementación? Creo que si desea imponer las cosas a ese nivel, debería considerar crear una clase abstracta para que los subtipos hereden de ella, en lugar de implementar una interfaz determinada. (por cierto, ¿mi lógica tiene sentido?)
heltonbiker
@heltonbiker Desafortunadamente, la mayoría de los idiomas (lo sé) no permiten expresar tales restricciones en los tipos, pero definitivamente deberían hacerlo . Esto es una cuestión de especificación, no de implementación. En mi opinión, lo que falta es la posibilidad de expresar invariantes de clase y post-condición directamente en el idioma. Idealmente, nunca deberíamos tener que preocuparnos por InvalidStateExceptions, por ejemplo, pero no estoy seguro de si esto es teóricamente posible. Sin embargo, sería bueno.
proskor

Respuestas:

2

Preferiría la solución 3 sobre 1 y 2.

Mi problema con la solución 1 es : ¿qué sucede si no hay un Invalidatemétodo? Asumir una interfaz con una IsValidpropiedad que devuelve DateTime.Now > MyExpirationDate; Es posible que no necesite un SetInvalidmétodo explícito aquí. ¿Qué pasa si un método afecta a múltiples propiedades? Suponga un tipo de conexión con IsOpeny IsConnectedpropiedades: ambos se ven afectados por el Closemé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 IsValidejemplo de arriba: tendrías que implementar un temporizador y disparar un evento cuando llegues MyExpirationDate. 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 IsInvalidatedno 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.

enzi
fuente
Esta pregunta ha recibido muy buenas respuestas, y es una pena que solo pueda aceptar una de ellas. Elegí su respuesta porque proporciona un buen resumen de algunos puntos planteados en las otras respuestas, y porque es más o menos lo que he decidido hacer. ¡Gracias a todos los que han publicado una respuesta!
stakx
8

Hay una cuarta solución: confiar en la documentación .

¿Cómo sabes que la stringclase 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.

/// <summary>
/// Represents an index of an element stored in the database.
/// </summary>
private interface IX
{
    /// <summary>
    /// Gets the immutable globally unique identifier of the index, the identifier being
    /// assigned by the database when the index is created.
    /// </summary>
    Guid Id { get; }

    /// <summary>
    /// Gets a value indicating whether the index is invalidated, which means that the
    /// indexed element is no longer valid and should be reloaded from the database. The
    /// index can be invalidated by calling the <see cref="Invalidate()" /> method.
    /// </summary>
    bool IsInvalidated { get; }

    /// <summary>
    /// Invalidates the index, without forcing the indexed element to be reloaded from the
    /// database.
    /// </summary>
    void Invalidate();
}

Su tercera solución no es mala, pero .NET Framework no la sigue . Por ejemplo:

StringBuilder.Length
DateTime.Now

son propiedades, pero no esperes que permanezcan constantes cada vez.

Lo que se usa constantemente en .NET Framework en sí es esto:

IEnumerable<T>.Count()

es un método, mientras que:

IList<T>.Length

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,

public class Product
{
    private readonly int price;

    public Product(int price)
    {
        this.price = price;
    }

    public int Price
    {
        get
        {
            return this.price;
        }
    }
}

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.

Arseni Mourzenko
fuente
Incluso la directriz “sin trabajo adicional” no se usa de manera absolutamente consistente. Por ejemplo, Lazy<T>.Valuepuede tomar mucho tiempo calcular (cuando se accede por primera vez).
svick
1
En mi opinión, no importa si .NET Framework sigue la convención de la solución 3. Espero que los desarrolladores sean lo suficientemente inteligentes como para saber si están trabajando con tipos internos o tipos de marcos. Los desarrolladores que no saben que no leen documentos y, por lo tanto, la solución 4 tampoco ayudaría. Si la solución 3 se implementa como una convención de empresa / equipo, entonces creo que no importa si otras API también la siguen (aunque, por supuesto, sería muy apreciada).
enzi
4

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:

  • Cambiar el nombre de la interfaz IXMutable, por lo que está claro que puede cambiar su estado. El único valor inmutable en su ejemplo es el id, que generalmente se supone que es inmutable incluso en objetos mutables.
  • Sin mencionarlo. Las interfaces no son tan buenas para describir el comportamiento como nos gustaría que fueran; En parte, eso se debe a que el lenguaje no te permite describir cosas como "Este método siempre devolverá el mismo valor para un objeto específico" o "Llamar a este método con un argumento x <0 arrojará una excepción".
  • Excepto que hay un mecanismo para expandir el lenguaje y proporcionar información más precisa sobre construcciones: Anotaciones / Atributos . A veces necesitan algo de trabajo, pero le permiten especificar cosas arbitrariamente complejas sobre sus métodos. Busque o invente una anotación que diga "El valor de este método / propiedad puede cambiar durante la vida útil de un objeto" y agréguelo al método. Es posible que deba jugar algunos trucos para que los métodos de implementación lo "hereden", y los usuarios pueden necesitar leer el documento para esa anotación, pero será una herramienta que le permitirá decir exactamente lo que quiere decir, en lugar de insinuar en eso
aviv
fuente
3

Otra opción sería dividir la interfaz: suponiendo que después de llamar a Invalidate()cada invocación de IsInvalidatedque devuelva el mismo valor true, parece que no hay razón para invocar IsInvalidatedla 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 .

interface Invalidatable
{
    bool IsInvalidated { get; }
}

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 de IsInvalidatedvez en cuando. Por otro lado, supongamos que la primera interfaz se ve así (pseudo-sintaxis):

[Immutable]
interface IX
{
    Guid Id { get; }
    void Invalidate();
}

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.

proskor
fuente
¡No es una mala idea! Sin embargo, diría que es incorrecto marcar una interfaz [Immutable]siempre que abarque métodos cuyo único propósito sea causar una mutación. Me gustaría mover el Invalidate()método a la Invalidatableinterfaz.
stakx
1
@stakx No estoy de acuerdo. :) Creo que confunde los conceptos de inmutabilidad y pureza (ausencia de efectos secundarios). De hecho, desde el punto de vista de la interfaz IX propuesta, no hay ninguna mutación observable en absoluto. Cada vez que llame Id, obtendrá el mismo valor cada vez. Lo mismo se aplica a Invalidate()(no obtienes nada, ya que su tipo de retorno es void). Por lo tanto, el tipo es inmutable. Por supuesto, tendrá efectos secundarios , pero no podrá observarlos a través de la interfaz IX, por lo que no tendrán ningún impacto en los clientes de IX.
proskor
OK, podemos estar de acuerdo en que IXes inmutable. Y que son efectos secundarios; simplemente se distribuyen entre dos interfaces divididas, de modo que los clientes no necesitan preocuparse (en el caso de IX) o que es obvio cuál podría ser el efecto secundario (en el caso de Invalidatable). Pero, ¿por qué no pasar Invalidatea Invalidatable? Eso se volvería IXinmutable y puro, y Invalidatableno se volvería más mutable ni más impuro (porque ya son ambos).
stakx
(Entiendo que en un escenario del mundo real, la naturaleza de la división interfaz probablemente dependería más de los casos de uso práctico que en cuestiones técnicas, pero estoy tratando de entender completamente su razonamiento aquí.)
stakx
1
@stakx En primer lugar, usted preguntó sobre otras posibilidades para hacer que la semántica de su interfaz sea más explícita. Mi idea era reducir el problema al de inmutabilidad, que requería dividir la interfaz. Pero no solo se requirió la división para hacer que una interfaz sea inmutable, sino que también tendría mucho sentido, porque no hay razón para invocar a ambos Invalidate()y IsInvalidatedpor el mismo consumidor de la interfaz . Si llama Invalidate(), debe saber que se invalida, entonces, ¿por qué verificarlo? Por lo tanto, puede proporcionar dos interfaces distintas para diferentes propósitos.
proskor