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 Id
e 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), enquantoIsInvalidated
pode alterar seu valor durante a vida útil de umIX
objeto (e, portanto, não deve ser armazenado em cache).
Como eu poderia modificar interface IX
para tornar esse contrato suficientemente explícito?
Minhas próprias três tentativas de solução:
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 semelhanteIsInvalidated
pode 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.
Aumente essa interface com um evento
IsInvalidatedChanged
:bool IsInvalidated { get; } event EventHandler IsInvalidatedChanged;
A presença de um
…Changed
evento paraIsInvalidated
afirma que essa propriedade pode alterar seu valor e a ausência de um evento semelhante paraId
é 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.
Substitua a propriedade
IsInvalidated
por um métodoIsInvalidated()
: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.
fonte
IsInvalidated
precisa de umprivate set
?class Foo : IFoo { private bool isInvalidated; public bool IsInvalidated { get { return isInvalidated; } } public void Invalidate() { isInvalidated = true; } }
InvalidStateException
s, por exemplo, mas não tenho certeza se isso é teoricamente possível. Seria bom, no entanto.Respostas:
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
Invalidate
método? Suponha uma interface com umaIsValid
propriedade que retorneDateTime.Now > MyExpirationDate
; talvez você não precise de umSetInvalid
método explícito aqui. E se um método afetar várias propriedades? Suponha um tipo de conexão comIsOpen
eIsConnected
propriedades - ambos são afetados peloClose
mé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
IsValid
exemplo acima: você teria que implementar um Timer e disparar um evento quando chegarMyExpirationDate
. 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
IsInvalidated
nã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 já 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.fonte
Existe uma quarta solução: confiar na documentação .
Como você sabe que a
string
classe é 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.Sua terceira solução não é ruim, mas o .NET Framework não a segue . Por exemplo:
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:
é um método, enquanto:
é 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,
é 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.
fonte
Lazy<T>.Value
pode levar muito tempo para calcular (quando é acessado pela primeira vez).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:
id
, que geralmente é considerado imutável, mesmo em objetos mutáveis.fonte
Outra opção seria dividir a interface: assumindo que, após chamar
Invalidate()
cada chamada deIsInvalidated
, retorne o mesmo valortrue
, parece não haver motivo para chamarIsInvalidated
a mesma parte que foi chamadaInvalidate()
.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 .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 deIsInvalidated
tempos em tempos. Por outro lado, suponha que a primeira interface tenha esta aparência (pseudo-sintaxe):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.
fonte
[Immutable]
desde que ela abranja métodos cujo único objetivo é causar uma mutação. Eu moveria oInvalidate()
método para aInvalidatable
interface.Id
, você sempre terá o mesmo valor. O mesmo se aplica aInvalidate()
(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 interfaceIX
, para que eles não tenham nenhum impacto nos clientesIX
.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 deIX
) ou que seja óbvio qual pode ser o efeito colateral (no caso deInvalidatable
). Mas por que não passarInvalidate
paraInvalidatable
? Isso tornariaIX
imutável e puro, eInvalidatable
não se tornaria mais mutável, nem mais impuro (porque já são os dois).Invalidate()
eIsInvalidated
pelo mesmo consumidor da interface . Se você ligarInvalidate()
, 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.