Como eu projetaria uma interface para que fique claro quais propriedades podem alterar seu valor e quais permanecerão constantes?


12

Estou com um problema de design em relação às propriedades do .NET.

interface IX
{
    Guid Id { get; }
    bool IsInvalidated { get; }
    void Invalidate();
}

Problema:

Essa interface possui duas propriedades somente leitura Ide IsInvalidated. O fato de serem somente leitura, no entanto, não é, por si só, garantia de que seus valores permanecerão constantes.

Digamos que era minha intenção deixar bem claro que ...

  • Id representa um valor constante (que pode, portanto, ser armazenado em cache com segurança), enquanto
  • IsInvalidatedpode alterar seu valor durante a vida útil de um IXobjeto (e, portanto, não deve ser armazenado em cache).

Como eu poderia modificar interface IXpara tornar esse contrato suficientemente explícito?

Minhas próprias três tentativas de solução:

  1. A interface já está bem projetada. A presença de um método chamado Invalidate()permite que um programador deduza que o valor da propriedade com nome semelhante IsInvalidatedpode ser afetado por ele.

    Esse argumento é válido apenas nos casos em que o método e a propriedade são nomeados de maneira semelhante.

  2. Aumente essa interface com um evento IsInvalidatedChanged:

    bool IsInvalidated { get; }
    event EventHandler IsInvalidatedChanged;
    

    A presença de um …Changedevento para IsInvalidatedafirma que essa propriedade pode alterar seu valor e a ausência de um evento semelhante para Idé uma promessa de que essa propriedade não alterará seu valor.

    Gosto dessa solução, mas há muitas coisas adicionais que podem não ser usadas.

  3. Substitua a propriedade IsInvalidatedpor um método IsInvalidated():

    bool IsInvalidated();

    Isso pode ser uma mudança muito sutil. Supõe-se que seja um indício de que um valor é calculado sempre a cada vez - o que não seria necessário se fosse uma constante. O tópico do MSDN "Escolhendo entre propriedades e métodos" tem a dizer sobre isso:

    Use um método, em vez de uma propriedade, nas seguintes situações. […] A operação retorna um resultado diferente cada vez que é chamada, mesmo que os parâmetros não sejam alterados.

Que tipo de respostas eu espero?

  • Estou mais interessado em soluções totalmente diferentes para o problema, juntamente com uma explicação de como elas venceram minhas tentativas acima.

  • Se minhas tentativas são logicamente falhas ou têm desvantagens significativas ainda não mencionadas, de tal forma que resta apenas uma solução (ou nenhuma), eu gostaria de ouvir sobre onde errei.

    Se as falhas forem mínimas e mais de uma solução permanecer após a consideração, por favor, comente.

  • No mínimo, gostaria de receber um feedback sobre qual é a sua solução preferida e por qual motivo.


Não IsInvalidatedprecisa de um private set?
James

2
@ James: Não necessariamente, nem na declaração de interface, nem em uma implementação:class Foo : IFoo { private bool isInvalidated; public bool IsInvalidated { get { return isInvalidated; } } public void Invalidate() { isInvalidated = true; } }
stakx

@James As propriedades nas interfaces não são iguais às propriedades automáticas, mesmo que elas usem a mesma sintaxe.
svick

Fiquei me perguntando: as interfaces "têm o poder" de impor esse comportamento? Esse comportamento não deve ser delegado para cada implementação? Penso que, se você deseja impor coisas a esse nível, considere criar uma classe abstrata para que os subtipos sejam herdados dela, em vez de implementar uma determinada interface. (a propósito, que o meu faz sentido racional?)
heltonbiker

@ heltonbiker Infelizmente, a maioria dos idiomas (eu sei) não permite expressar essas restrições nos tipos, mas eles definitivamente deveriam . Isso é uma questão de especificação, não de implementação. Na minha opinião, o que falta é a possibilidade de expressar invariantes de classe e pós-condição diretamente no idioma. Idealmente, nunca devemos nos preocupar com InvalidStateExceptions, por exemplo, mas não tenho certeza se isso é teoricamente possível. Seria bom, no entanto.
proskor

Respostas:


2

Eu preferiria a solução 3 ao invés de 1 e 2.

Meu problema com a solução 1 é : o que acontece se não houver Invalidatemétodo? Suponha uma interface com uma IsValidpropriedade que retorne DateTime.Now > MyExpirationDate; talvez você não precise de um SetInvalidmétodo explícito aqui. E se um método afetar várias propriedades? Suponha um tipo de conexão com IsOpene IsConnectedpropriedades - ambos são afetados pelo Closemétodo.

Solução 2 : se o único ponto do evento é informar aos desenvolvedores que uma propriedade com nome semelhante pode retornar valores diferentes em cada chamada, aconselho isso contra ela. Você deve manter suas interfaces curtas e claras; além disso, nem toda implementação poderá disparar esse evento para você. Vou reutilizar meu IsValidexemplo acima: você teria que implementar um Timer e disparar um evento quando chegar MyExpirationDate. Afinal, se esse evento fizer parte da sua interface pública, os usuários da interface esperam que esse evento funcione.

Dito isto sobre essas soluções: elas não são ruins. A presença de um método ou de um evento indica que uma propriedade com nome semelhante pode retornar valores diferentes em cada chamada. Tudo o que estou dizendo é que eles sozinhos não são suficientes para transmitir sempre esse significado.

Solução 3 é o que eu procuraria. Como aviv mencionado, isso pode funcionar apenas para desenvolvedores de C #. Para mim, como desenvolvedor de C #, o fato de IsInvalidatednão ser uma propriedade transmite imediatamente o significado de "isso não é apenas um acessador simples, algo está acontecendo aqui". Porém, isso não se aplica a todos e, como apontou a MainMa, o próprio .NET framework não é consistente aqui.

Se você quiser usar a solução 3, recomendo declarar uma convenção e fazer com que toda a equipe a siga. A documentação também é essencial, acredito. Na minha opinião, é realmente muito simples sugerir a alteração de valores: " retorna verdadeiro se o valor ainda for válido ", " indica se a conexão foi aberta " vs. " retorna verdadeiro se este objeto é válido ". Não faria mal, no entanto, ser mais explícito na documentação.

Então, meu conselho seria:

Declare a solução 3 como uma convenção e siga-a consistentemente. Deixe claro na documentação das propriedades se uma propriedade tem ou não valores alterados. Os desenvolvedores que trabalham com propriedades simples assumem corretamente que elas não são alteráveis ​​(ou seja, somente mudam quando o estado do objeto é modificado). Desenvolvedores encontrando um método que soa como ele poderia ser uma propriedade ( Count(), Length(), IsOpen()) sabem que someting está acontecendo e (espero) ler os docs método para compreender exatamente o que o método faz e como ele se comporta.


Esta pergunta recebeu respostas muito boas e é uma pena que eu possa aceitar apenas uma delas. Eu escolhi sua resposta porque ela fornece um bom resumo de alguns pontos levantados nas outras respostas e porque é mais ou menos o que eu decidi fazer. Obrigado a todos que postaram uma resposta!
stakx

8

Existe uma quarta solução: confiar na documentação .

Como você sabe que a stringclasse é imutável? Você simplesmente sabe disso, porque leu a documentação do MSDN. StringBuilder, por outro lado, é mutável, porque, novamente, a documentação diz isso.

/// <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();
}

Sua terceira solução não é ruim, mas o .NET Framework não a segue . Por exemplo:

StringBuilder.Length
DateTime.Now

são propriedades, mas não espere que elas permaneçam constantes o tempo todo.

O que é usado consistentemente no próprio .NET Framework é o seguinte:

IEnumerable<T>.Count()

é um método, enquanto:

IList<T>.Length

é uma propriedade. No primeiro caso, fazer coisas pode exigir trabalho adicional, incluindo, por exemplo, a consulta ao banco de dados. Este trabalho adicional pode levar algum tempo. No caso de uma propriedade, espera-se que demore um pouco : de fato, o comprimento pode mudar durante a vida útil da lista, mas não é necessário praticamente nada para devolvê-la.


Com as classes, apenas observar o código mostra claramente que o valor de uma propriedade não será alterado durante a vida útil do objeto. Por exemplo,

public class Product
{
    private readonly int price;

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

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

é claro: o preço permanecerá o mesmo.

Infelizmente, você não pode aplicar o mesmo padrão a uma interface e, como o C # não é suficientemente expressivo nesse caso, a documentação (incluindo documentação XML e diagramas UML) deve ser usada para fechar a lacuna.


Mesmo a diretriz “sem trabalho adicional” não é usada de maneira absolutamente consistente. Por exemplo, Lazy<T>.Valuepode levar muito tempo para calcular (quando é acessado pela primeira vez).
svick

1
Na minha opinião, não importa se a estrutura .NET segue a convenção da solução 3. Espero que os desenvolvedores sejam inteligentes o suficiente para saber se estão trabalhando com tipos internos ou de estrutura. Desenvolvedores que não sabem que não lêem documentos e, portanto, a solução 4 também não ajudariam. Se a solução 3 for implementada como uma convenção de empresa / equipe, acredito que não importa se outras APIs também a seguem (ou seja, seria muito apreciada, é claro).
enzi 30/08/2013

4

Meus 2 centavos:

Opção 3: como um não C #, não seria uma pista; No entanto, a julgar pela citação que você traz, deve ficar claro para os programadores de C # que isso significa alguma coisa. Você ainda deve adicionar documentos sobre isso.

Opção 2: a menos que você planeje adicionar eventos a tudo o que pode mudar, não vá lá.

Outras opções:

  • Renomeando a interface IXMutable, fica claro que ela pode mudar de estado. O único valor imutável no seu exemplo é o id, que geralmente é considerado imutável, mesmo em objetos mutáveis.
  • Sem mencionar isso. As interfaces não são tão boas para descrever o comportamento quanto gostaríamos que fossem; Em parte, isso ocorre porque o idioma não permite realmente descrever coisas como "Este método sempre retornará o mesmo valor para um objeto específico" ou "Chamar esse método com um argumento x <0 gerará uma exceção".
  • Exceto que existe um mecanismo para expandir o idioma e fornecer informações mais precisas sobre construções - Anotações / Atributos . Às vezes, eles precisam de algum trabalho, mas permitem que você especifique coisas arbitrariamente complexas sobre seus métodos. Encontre ou invente uma anotação que diz "O valor deste método / propriedade pode mudar ao longo da vida útil de um objeto" e adicione-o ao método. Pode ser necessário executar alguns truques para que os métodos de implementação o "herdem", e os usuários podem precisar ler o documento para essa anotação, mas será uma ferramenta que permitirá que você diga exatamente o que deseja dizer, em vez de sugerir Nisso.

3

Outra opção seria dividir a interface: assumindo que, após chamar Invalidate()cada chamada de IsInvalidated, retorne o mesmo valor true, parece não haver motivo para chamar IsInvalidateda mesma parte que foi chamada Invalidate().

Então, sugiro que você verifique a invalidação ou a causa , mas parece irracional fazer as duas coisas. Portanto, faz sentido oferecer uma interface contendo a Invalidate()operação para a primeira parte e outra para verificar ( IsInvalidated) na outra. Como é bastante óbvio pela assinatura da primeira interface que ela fará com que a instância seja invalidada, a questão restante (para a segunda interface) é realmente bastante geral: como especificar se o tipo fornecido é imutável .

interface Invalidatable
{
    bool IsInvalidated { get; }
}

Eu sei que isso não responde diretamente à pergunta, mas pelo menos a reduz à questão de como marcar algum tipo imutável , o que é silencioso como um problema comum e compreendido. Por exemplo, assumindo que todos os tipos são mutáveis, a menos que definido de outra forma, você pode concluir que a segunda interface ( IsInvalidated) é mutável e, portanto, pode alterar o valor de IsInvalidatedtempos em tempos. Por outro lado, suponha que a primeira interface tenha esta aparência (pseudo-sintaxe):

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

Como está marcado como imutável, você sabe que o ID não será alterado. Obviamente, chamar invalidez fará com que o estado da instância seja alterado, mas essa alteração não será observada por essa interface.


Não é uma má ideia! No entanto, eu argumentaria que é errado marcar uma interface [Immutable]desde que ela abranja métodos cujo único objetivo é causar uma mutação. Eu moveria o Invalidate()método para a Invalidatableinterface.
stakx

1
@stakx Eu discordo. :) Eu acho que você confunde os conceitos de imutabilidade e pureza (ausência de efeitos colaterais). De fato, do ponto de vista da interface IX proposta, não há mutação observável. Sempre que você ligar Id, você sempre terá o mesmo valor. O mesmo se aplica a Invalidate()(você não recebe nada, pois seu tipo de retorno é void). Portanto, o tipo é imutável. Obviamente, você terá efeitos colaterais , mas não poderá observá- los através da interface IX, para que eles não tenham nenhum impacto nos clientes IX.
proskor

OK, podemos concordar que IXé imutável. E que eles são efeitos colaterais; eles são distribuídos apenas entre duas interfaces divididas, para que os clientes não precisem se importar (no caso de IX) ou que seja óbvio qual pode ser o efeito colateral (no caso de Invalidatable). Mas por que não passar Invalidatepara Invalidatable? Isso tornaria IXimutável e puro, e Invalidatablenão se tornaria mais mutável, nem mais impuro (porque já são os dois).
precisa saber é

(Eu entendo que em um cenário do mundo real, a natureza da divisão de interface provavelmente depender mais casos de uso prático do que sobre questões técnicas, mas eu estou tentando entender totalmente o seu raciocínio aqui.)
stakx

1
@stakx Antes de tudo, você perguntou sobre outras possibilidades de tornar a semântica da sua interface mais explícita. Minha idéia era reduzir o problema à imutabilidade, o que exigia a divisão da interface. Mas não era apenas a divisão necessária para tornar uma interface imutável, mas também faria muito sentido, porque não há razão para chamar ambos Invalidate()e IsInvalidatedpelo mesmo consumidor da interface . Se você ligar Invalidate(), você deve saber que ele se torna inválido, então por que verificar? Assim, você pode fornecer duas interfaces distintas para diferentes propósitos.
proskor
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.