Não declare interfaces para objetos imutáveis

27

Não declare interfaces para objetos imutáveis

[EDIT] Onde os objetos em questão representam objetos de transferência de dados (DTOs) ou dados antigos simples (PODs)

Essa é uma orientação razoável?

Até agora, muitas vezes eu criei interfaces para classes seladas que são imutáveis ​​(os dados não podem ser alterados). Tentei tomar cuidado para não usar a interface em nenhum lugar em que me importe com a imutabilidade.

Infelizmente, a interface começa a invadir o código (e não é apenas com o meu código que estou preocupado). Você acaba passando por uma interface e, em seguida, querendo passar para algum código que realmente quer assumir que o que está sendo passado é imutável.

Devido a esse problema, estou pensando em nunca declarar interfaces para objetos imutáveis.

Isso pode ter ramificações em relação aos testes de unidade, mas, além disso, isso parece uma diretriz razoável?

Ou existe outro padrão que devo usar para evitar o problema da "interface de expansão" que estou vendo?

(Estou usando esses objetos imutáveis ​​por várias razões: Principalmente para segurança do encadeamento, pois escrevo muito código multiencadeado; mas também porque significa que posso evitar fazer cópias defensivas de objetos passadas para os métodos. O código se torna muito mais simples Em muitos casos, quando você sabe que algo é imutável - o que não acontece se você recebeu uma interface.De fato, muitas vezes você não pode sequer fazer uma cópia defensiva de um objeto referenciado por uma interface se ela não fornecer uma interface. operação de clone ou qualquer maneira de serializá-la ...)

[EDITAR]

Para fornecer muito mais contexto para minhas razões para querer tornar objetos imutáveis, consulte esta postagem de blog de Eric Lippert:

http://blogs.msdn.com/b/ericlippert/archive/tags/immutability/

Devo também salientar que estou trabalhando com alguns conceitos de nível inferior aqui, como itens que estão sendo manipulados / passados ​​em filas de tarefas com vários threads. Estes são essencialmente DTOs.

Joshua Bloch também recomenda o uso de objetos imutáveis ​​em seu livro Effective Java .


Acompanhamento

Obrigado pelo feedback, todos. Decidi seguir em frente e usar essa diretriz para os DTOs e seus afins. Está funcionando bem até agora, mas faz apenas uma semana ... Ainda assim, está parecendo bom.

Há algumas outras questões relacionadas a isso sobre as quais quero perguntar; notavelmente algo que estou chamando de "Imutabilidade profunda ou superficial" (nomenclatura que roubei da clonagem profunda e superficial) - mas essa é uma pergunta para outra hora.

Matthew Watson
fonte
3
+1 ótima pergunta. Por favor, explique por que é tão importante para você que o objeto seja dessa classe imutável em particular, e não algo que apenas implemente a mesma interface. É possível que seus problemas estejam enraizados em outro lugar. A injeção de dependência é extremamente importante para testes de unidade, mas possui muitos outros benefícios importantes que desaparecem se você forçar seu código a exigir uma determinada classe imutável.
9183 Steven Doggart
1
Estou um pouco confuso com o uso do termo imutável . Apenas para esclarecer, você quer dizer uma classe selada, que não pode ser herdada e substituída, ou que os dados expostos são somente leitura e não podem ser alterados depois que o objeto é criado. Em outras palavras, você deseja garantir que o objeto seja de um tipo específico ou que seu valor nunca será alterado. No meu primeiro comentário, presumi que quis dizer o primeiro, mas agora com a sua edição, parece mais com o último. Ou você está preocupado com os dois?
Steven Doggart
3
Estou confuso, por que não deixar os setters fora da interface? Mas, por outro lado, eu me canso de ver interfaces para objetos que são realmente objetos de domínio e / ou DTOs que exigem fábricas para esses objetos ... causa dissonância cognitiva para mim.
22313 Michael Michael
2
E quanto a interfaces para classes que são todos imutável? Por exemplo, de Java Número classe permite definir um List<Number>que pode conter Integer, Float, Long, BigDecimal, etc ... Todos os quais são imutáveis si.
1
@ MikeBrown Eu acho que só porque a interface implica imutabilidade não significa que a implementação a imponha. Você pode deixar isso para a convenção (por exemplo, documentar a interface que imutabilidade é um requisito), mas pode acabar com alguns problemas realmente desagradáveis ​​se alguém violar a convenção.
vaughandroid

Respostas:

17

Na minha opinião, sua regra é boa (ou pelo menos não é ruim), mas apenas por causa da situação que você está descrevendo. Eu não diria que concordo com isso em todas as situações; portanto, do ponto de vista do meu pedante interior, devo dizer que sua regra é tecnicamente muito ampla.

Normalmente, você não definiria objetos imutáveis, a menos que eles sejam essencialmente usados ​​como objetos de transferência de dados (DTO), o que significa que eles contêm propriedades de dados, mas pouca lógica e nenhuma dependência. Se for esse o caso, como parece aqui, eu diria que você pode usar os tipos concretos diretamente, em vez de interfaces.

Tenho certeza de que alguns puristas de teste de unidade discordarão, mas, na minha opinião, as classes de DTO podem ser excluídas com segurança dos requisitos de teste de unidade e injeção de dependência. Não há necessidade de usar uma fábrica para criar um DTO, pois ele não possui dependências. Se tudo criar os DTOs diretamente, conforme necessário, realmente não há como injetar um tipo diferente, portanto, não há necessidade de uma interface. E, como eles não contêm lógica, não há nada para testar a unidade. Mesmo que eles contenham alguma lógica, desde que não tenham dependências, deve ser trivial testar a lógica da unidade, se necessário.

Como tal, acho que a regra de que todas as classes de DTO não devem implementar uma interface, embora potencialmente desnecessária, não prejudicará o design do software. Como você tem esse requisito de que os dados precisam ser imutáveis ​​e você não pode impor isso por meio de uma interface, eu diria que é totalmente legítimo estabelecer essa regra como um padrão de codificação.

O problema maior, no entanto, é a necessidade de impor estritamente uma camada limpa de DTO. Enquanto suas classes imutáveis ​​sem interface existirem apenas na camada DTO e sua camada DTO permanecer livre de lógica e dependências, você estará seguro. Porém, se você começar a misturar suas camadas e tiver classes sem interface que funcionam como classes de camadas de negócios, acho que começará a ter muitos problemas.

Steven Doggart
fonte
O código que espera especificamente lidar com um DTO ou objeto de valor imutável deve usar os recursos desse tipo em vez dos de uma interface, mas isso não significa que não haveria casos de uso para uma interface implementada por essa classe e também por outras classes, com métodos que prometem retornar valores válidos por um tempo mínimo (por exemplo, se a interface for passada para uma função, os métodos deverão retornar valores válidos até que a função retorne). Tal interface pode ser útil se houver um número de tipos que encapsulam dados semelhantes e um desejo ...
supercat
... ter um meio de copiar dados de um para o outro. Se todos os tipos que incluem determinados dados implementam a mesma interface para lê-los, esses dados podem ser facilmente copiados entre todos os tipos, sem exigir que cada um saiba como importar de cada um dos outros.
supercat 22/02
Nesse ponto, você também pode eliminar seus getters (e setters, se seus DTOs são mutáveis ​​por algum motivo estúpido) e tornar os campos públicos. Você nunca vai colocar nenhuma lógica aí, certo?
Kevin
@ Kevin eu suponho, mas com a conveniência moderna das propriedades de automóveis, é tão fácil torná-las propriedades, por que não? Suponho que se o desempenho e a eficiência são da maior preocupação, então talvez isso importe. De qualquer forma, a questão é: qual nível de "lógica" você permite em um DTO? Mesmo se você colocar alguma lógica de validação em suas propriedades, não haveria problemas para testá-lo unitariamente, desde que não houvesse dependências que exigiriam zombaria. Desde que o DTO não use nenhum objeto de negócios de dependência, é seguro ter um pouco de lógica incorporada a eles, porque eles ainda poderão ser testados.
Steven Doggart
Pessoalmente, prefiro mantê-las o mais limpas possível, mas sempre há momentos em que as regras são necessárias e, portanto, é melhor deixar a porta aberta para isso.
Steven Doggart
7

Eu costumava fazer uma grande confusão sobre tornar meu código invulnerável ao uso indevido. Criei interfaces somente leitura para ocultar membros mutantes, adicionei muitas restrições às minhas assinaturas genéricas etc. etc. Aconteceu que na maioria das vezes eu tomava decisões de design porque não confiava em meus colegas de trabalho imaginários. "Talvez um dia eles contratem um novo funcionário e ele não saberá que a classe XYZ não pode atualizar o DTO ABC. Ah, não!" Outras vezes, eu estava focando no problema errado - ignorando a solução óbvia - não vendo a floresta através das árvores.

Eu nunca mais crio interfaces para meus DTOs. Trabalho com a suposição de que as pessoas que tocam no meu código (principalmente eu) sabem o que é permitido e o que faz sentido. Se eu continuo cometendo o mesmo erro estúpido, geralmente não tento fortalecer minhas interfaces. Agora passo a maior parte do tempo tentando entender por que continuo cometendo o mesmo erro. Geralmente é porque estou analisando demais algo ou perdendo um conceito-chave. Meu código ficou muito mais fácil de trabalhar desde que deixei de ser paranóico. Também acabo com menos "estruturas" que exigiam o conhecimento de alguém interno para trabalhar no sistema.

Minha conclusão foi encontrar a coisa mais simples que funciona. A complexidade adicional de criar interfaces seguras desperdiça tempo de desenvolvimento e complica o código simples. Preocupe-se com coisas assim quando você tiver 10.000 desenvolvedores usando suas bibliotecas. Acredite, isso o salvará de muita tensão desnecessária.

Travis Parks
fonte
Normalmente guardo interfaces para quando preciso de injeção de dependência para testes de unidade.
Parques Travis
5

Parece uma boa orientação, mas por razões estranhas. Já tive vários lugares em que uma interface (ou classe base abstrata) fornece acesso uniforme a uma série de objetos imutáveis. Estratégias tendem a cair aqui. Objetos de estado tendem a cair aqui. Não acho que seja excessivo moldar uma interface para parecer imutável e documentá-la como tal em sua API.

Dito isto, as pessoas tendem a fazer uma interface excessiva de objetos Plain Old Data (doravante, POD) e até estruturas simples (geralmente imutáveis). Se o seu código não tem alternativas sãs para alguma estrutura fundamental, ele não precisa de uma interface. Não, o teste de unidade não é motivo suficiente para alterar seu design (zombar do acesso ao banco de dados não é o motivo pelo qual você fornece uma interface, é flexibilidade para mudanças futuras) - não é o fim do mundo se seus testes use essa estrutura fundamental básica como está.

Telastyn
fonte
2

O código se torna muito mais simples em muitos casos quando você sabe que algo é imutável - o que você não faz se tiver recebido uma interface.

Eu não acho que essa seja uma preocupação com a qual o implementador de acessores precisa se preocupar. Se interface Xé para ser imutável, não é responsabilidade do implementador da interface garantir que eles implementem a interface de maneira imutável?

No entanto, na minha opinião, não existe uma interface imutável - o contrato de código especificado por uma interface se aplica apenas aos métodos expostos de um objeto, e nada sobre as partes internas de um objeto.

É muito mais comum ver a imutabilidade implementada como um decorador do que como uma interface, mas a viabilidade dessa solução realmente depende da estrutura do seu objeto e da complexidade da sua implementação.

Jonathan Rich
fonte
1
Você poderia fornecer um exemplo de implementação da imutabilidade como decorador?
vaughandroid
Não trivialmente, eu não acho, mas é um exercício de pensamento relativamente simples - se MutableObjecttiver nmétodos que alteram estado e mmétodos que retornam estado, ImmutableDecoratorpodem continuar a expor os métodos que retornam state ( m) e, dependendo do ambiente, afirmar ou lança uma exceção quando um dos métodos mutáveis ​​é chamado.
Jonathan Rico
Eu realmente não gosto de conversão de certeza em tempo de compilação para a possibilidade de excepções em tempo de execução ...
Matthew Watson
OK, mas como o ImmutableDecorator sabe se um determinado método muda de estado ou não?
vaughandroid
Você não pode ter certeza, em nenhum caso, se uma classe que implementa sua interface é imutável com relação aos métodos definidos por sua interface. @Baqueta O decorador deve ter conhecimento da implementação da classe base.
Jonathan Rico
0

Você acaba passando por uma interface e, em seguida, querendo passar para algum código que realmente quer assumir que o que está sendo passado é imutável.

No C #, um método que espera um objeto de um tipo "imutável" não deve ter um parâmetro de um tipo de interface, porque as interfaces C # não podem especificar o contrato de imutabilidade. Portanto, a diretriz que você está propondo não tem sentido em C # porque você não pode fazê-lo em primeiro lugar (é improvável que versões futuras dos idiomas o permitam).

Sua pergunta deriva de um sutil mal-entendido dos artigos de Eric Lippert sobre imutabilidade. Eric não definiu as interfaces IStack<T>e IQueue<T>especificou contratos para pilhas e filas imutáveis. Eles não. Ele os definiu por conveniência. Essas interfaces lhe permitiram definir tipos diferentes para pilhas e filas vazias. Podemos propor um design e implementação diferentes de uma pilha imutável usando um único tipo sem exigir uma interface ou um tipo separado para representar a pilha vazia, mas o código resultante não parecerá tão limpo e seria um pouco menos eficiente.

Agora, vamos nos ater ao design de Eric. Um método que requer uma pilha imutável deve ter um parâmetro do tipo Stack<T>em vez da interface geral IStack<T>que representa o tipo de dados abstratos de uma pilha no sentido geral. Não é óbvio como fazer isso ao usar a pilha imutável de Eric e ele não discutiu isso em seus artigos, mas é possível. O problema está no tipo da pilha vazia. Você pode resolver esse problema, certificando-se de que nunca obtém uma pilha vazia. Isso pode ser garantido pressionando um valor fictício como o primeiro valor na pilha e nunca exibindo-o. Dessa forma, você pode transmitir com segurança os resultados de Pushe Poppara Stack<T>.

Ter Stack<T>implementos IStack<T>pode ser útil. Você pode definir métodos que exijam uma pilha, qualquer pilha, não necessariamente uma pilha imutável. Estes métodos podem ter um parâmetro do tipo IStack<T>. Isso permite que você passe pilhas imutáveis ​​para ele. Idealmente, IStack<T>faria parte da própria biblioteca padrão. No .NET, não existe IStack<T>, mas existem outras interfaces padrão que a pilha imutável pode implementar, tornando o tipo mais útil.

O design e implementação alternativos da pilha imutável a que me referi anteriormente usam uma interface chamada IImmutableStack<T>. Obviamente, inserir "Imutável" no nome da interface não torna imutável todo tipo de implementação. No entanto, nessa interface, o contrato de imutabilidade é apenas verbal. Um bom desenvolvedor deve respeitá-lo.

Se você estiver desenvolvendo uma pequena biblioteca interna, poderá concordar com todos da equipe em honrar este contrato e usar IImmutableStack<T>como o tipo de parâmetros. Caso contrário, você não deve usar o tipo de interface.

Gostaria de acrescentar, desde que você marcou a pergunta C #, que na especificação C # não existem DTOs e PODs. Portanto, descartá-los ou defini-los com precisão melhora a questão.

Hadi Brais
fonte