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

12

Tengo un problema de diseño relacionado con 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 por sí mismo que sus valores se mantendrán constantes.

Digamos que era mi intención dejar muy claro que ...

  • Id representa un valor constante (que, por lo tanto, puede ser almacenado en caché de manera segura), mientras que
  • IsInvalidated podría cambiar su valor durante la vida útil de un objeto IX (y, por lo tanto, no debería almacenarse en caché).

¿Cómo podría modificar interface IX para que ese contrato sea lo suficientemente explícito?

Mis tres intentos de solución:

  1. La interfaz ya está bien diseñada. La presencia de un método llamado Invalidate() le permite a un programador inferir que el valor de la propiedad de nombre similar IsInvalidated podría verse afectado por él.

    Este argumento se mantiene 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 evento …Changed para IsInvalidated indica que esta propiedad puede cambiar su valor, y la ausencia de un evento similar para Id es una promesa de que esa propiedad no cambiará su valor.

    Me gusta esta solución, pero es un montón de cosas adicionales que pueden no utilizarse en absoluto.

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

    bool IsInvalidated();
    

    Esto podría ser un cambio demasiado sutil. Se supone que es un indicio de que un valor se calcula cada vez más, 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:

      

    Use 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 totalmente 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 que aún no se han mencionado, 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.

  • Por lo menos, me gustaría recibir comentarios sobre cuál es su solución preferida y por qué motivo.

pregunta stakx 24.08.2013 - 14:32

4 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 método Invalidate ? Supongamos una interfaz con una propiedad IsValid que devuelve DateTime.Now > MyExpirationDate ; Es posible que no necesite un método explícito de SetInvalid aquí. ¿Qué pasa si un método afecta a múltiples propiedades? Supongamos un tipo de conexión con las propiedades IsOpen y IsConnected , ambas se ven afectadas por el método Close .

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 le recomiendo encarecidamente que no lo haga. Debes mantener tus interfaces cortas y claras; además, no todas las implementaciones pueden disparar ese evento por usted. Reutilizaré mi ejemplo IsValid de arriba: tendría que implementar un temporizador y disparar un evento cuando llegue a MyExpirationDate . Después de todo, si ese evento es parte de su interfaz pública, entonces los usuarios de la interfaz esperarán que ese evento funcione.

Dicho esto acerca de 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.

Solución 3 es a lo que iría. Sin embargo, como aviv mencionó, esto solo puede funcionar para los desarrolladores de C #. Para mí como un C # -dev, el hecho de que IsInvalidated no sea una propiedad transmite inmediatamente el significado de "eso no es solo un simple acceso, algo está pasando aquí". Sin embargo, esto no se aplica a todos, y como señala MainMa, el marco .NET en sí no es consistente aquí.

Si desea utilizar la solución 3, le recomendaría declararla como 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 valores cambiantes: " devuelve true si el valor es todavía válido ", " indica si la conexión tiene ya se ha abierto "en lugar de" devuelve verdadero si este objeto es válido ". Sin embargo, no dolería en absoluto ser más explícito en la documentación.

Así que mi consejo sería:

Declara la solución 3 como una convención y la sigue constantemente. Aclare en la documentación de las propiedades si una propiedad tiene o no valores cambiantes. Los desarrolladores que trabajan con propiedades simples asumirán que no cambian (es decir, solo cambian cuando se modifica el estado del objeto). Los desarrolladores que se encuentran con un método que parece que podría ser una propiedad ( Count() , Length() , IsOpen() ) saben que algo está sucediendo y (con suerte) leen los documentos del método para comprender qué hace exactamente el método y cómo se comporta.

    
respondido por el enzi 30.08.2013 - 13:46
8

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

¿Cómo sabes que la clase string es inmutable? Simplemente lo sabes, porque has leído la documentación de MSDN. StringBuilder , por otro lado, es mutable, porque, nuevamente, la documentación lo dice.

/// <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í mismo 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, incluyendo, 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 un tiempo corto : 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 mirando 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 tal caso, la documentación (incluida la documentación XML y los diagramas UML) debe usarse para cerrar la brecha.

    
respondido por el Arseni Mourzenko 24.08.2013 - 15:46
4

Mis 2 centavos:

Opción 3: como un no C # -er, no sería una gran pista; Sin embargo, a juzgar por la cita que trae, debería quedar claro para los programadores C # competentes que esto significa algo. Aún así, debes 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, para que quede 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 le permite realmente describir cosas como "Este método siempre devolverá el mismo valor para un objeto específico" o "Al llamar a este método con un argumento x < 0 se generará una excepción".
  • Excepto que hay un mecanismo para expandir el lenguaje y proporcionar información más precisa sobre las construcciones: Anotaciones / Atributos . A veces necesitan algo de trabajo, pero le permiten especificar cosas arbitrariamente complejas acerca de sus métodos. Encuentre 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 deban leer el documento para esa anotación, pero será una herramienta que le permitirá decir exactamente lo que quiere decir, en lugar de sugerir en ello.
respondido por el aviv 24.08.2013 - 15:19
3

Otra opción sería dividir la interfaz: asumiendo que después de llamar a Invalidate() cada invocación de IsInvalidated debería devolver el mismo valor true , no parece haber razón para invocar IsInvalidated para la misma parte que invocado Invalidate() .

Por lo tanto, sugiero que, ya sea que verifique para su invalidación o que lo haga , pero parece irrazonable hacer ambas cosas. Por lo tanto, tiene sentido ofrecer una interfaz que contenga la operación Invalidate() a la primera parte y otra interfaz para verificar ( IsInvalidated ) a la otra. Dado que es bastante obvio a partir de la firma de la primera interfaz que causará 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 a 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, suponiendo 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 IsInvalidated de vez en cuando. Por otro lado, supongamos que la primera interfaz se ve así (pseudo-sintaxis):

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

Ya que está marcado como inmutable, sabe que la identificación no cambiará. Por supuesto, si se invalida, el estado de la instancia cambiará, pero este cambio no será observable a través de esta interfaz.

    
respondido por el proskor 30.08.2013 - 16:38

Lea otras preguntas en las etiquetas